Open source some packages tests.

--
MOS_MIGRATED_REVID=92727637
diff --git a/src/test/java/BUILD b/src/test/java/BUILD
index ef8e053..30f2bcc 100644
--- a/src/test/java/BUILD
+++ b/src/test/java/BUILD
@@ -53,6 +53,10 @@
         "com/google/devtools/build/lib/vfs/util/*.java",
         "com/google/devtools/build/lib/events/util/*.java",
     ]),
+    data = [
+        "//src/main/native:libunix.dylib",
+        "//src/main/native:libunix.so",
+    ],
     deps = [
         ":testutil",
         "//src/main/java:bazel-core",
@@ -102,10 +106,7 @@
         ],
     ),
     args = ["com.google.devtools.build.lib.AllTests"],
-    data = glob([test_prefix + "/vfs/*.zip"]) + [
-        "//src/main/native:libunix.dylib",
-        "//src/main/native:libunix.so",
-    ],
+    data = glob([test_prefix + "/vfs/*.zip"]),
     deps = [
         ":foundations_testutil",
         ":test_runner",
@@ -131,10 +132,6 @@
     args = [
         "com.google.devtools.build.lib.AllTests",
     ],
-    data = [
-        "//src/main/native:libunix.dylib",
-        "//src/main/native:libunix.so",
-    ],
     jvm_flags = ["-Dblaze.os=Windows"],
     deps = [
         ":foundations_testutil",
@@ -174,10 +171,6 @@
         "com/google/devtools/build/lib/actions/*.java",
     ]),
     args = ["com.google.devtools.build.lib.AllTests"],
-    data = [
-        "//src/main/native:libunix.dylib",
-        "//src/main/native:libunix.so",
-    ],
     deps = [
         ":actions_testutil",
         ":foundations_testutil",
@@ -200,20 +193,15 @@
         "com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java",
         "com/google/devtools/build/lib/analysis/util/*.java",
         "com/google/devtools/build/lib/exec/util/*.java",
-        "com/google/devtools/build/lib/packages/util/*.java",
         "com/google/devtools/build/lib/skyframe/util/*.java",
     ]),
-    data = [
-        "//src/main/native:libunix.dylib",
-        "//src/main/native:libunix.so",
-    ],
     resources = [
         "MOCK_CROSSTOOL",
     ],
     deps = [
         ":actions_testutil",
         ":foundations_testutil",
-        ":test_runner",
+        ":packages_testutil",
         ":testutil",
         "//src/main/java:bazel-core",
         "//src/main/java:concurrent",
@@ -302,6 +290,7 @@
         ":actions_testutil",
         ":analysis_testutil",
         ":foundations_testutil",
+        ":packages_testutil",
         ":test_runner",
         ":testutil",
         "//src/main/java:bazel-core",
@@ -332,6 +321,46 @@
     ],
 )
 
+java_library(
+    name = "packages_testutil",
+    srcs = glob([
+        "com/google/devtools/build/lib/packages/util/*.java",
+    ]),
+    deps = [
+        ":foundations_testutil",
+        ":testutil",
+        "//src/main/java:bazel-core",
+        "//src/main/protobuf:proto_extra_actions_base",
+        "//third_party:guava",
+        "//third_party:guava-testlib",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:mockito",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
+    name = "packages_test",
+    srcs = glob([
+        "com/google/devtools/build/lib/packages/*.java",
+    ]),
+    args = ["com.google.devtools.build.lib.AllTests"],
+    deps = [
+        ":actions_testutil",
+        ":foundations_testutil",
+        ":packages_testutil",
+        ":test_runner",
+        ":testutil",
+        "//src/main/java:bazel-core",
+        "//third_party:guava",
+        "//third_party:guava-testlib",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
 cc_binary(
     name = "com/google/devtools/build/lib/shell/killmyself",
     srcs = ["com/google/devtools/build/lib/shell/killmyself.cc"],
@@ -345,8 +374,6 @@
     args = ["com.google.devtools.build.lib.AllTests"],
     data = [
         ":com/google/devtools/build/lib/shell/killmyself",
-        "//src/main/native:libunix.dylib",
-        "//src/main/native:libunix.so",
     ],
     deps = [
         ":foundations_testutil",
@@ -369,10 +396,6 @@
         "com/google/devtools/build/lib/syntax/*.java",
     ]),
     args = ["com.google.devtools.build.lib.AllTests"],
-    data = [
-        "//src/main/native:libunix.dylib",
-        "//src/main/native:libunix.so",
-    ],
     deps = [
         ":foundations_testutil",
         ":test_runner",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
index 0bc8e87..a2cfe08 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
@@ -1264,7 +1264,7 @@
         + depRuleType + " rule '" + depRuleName + "' is misplaced here";
   }
 
-  public static String getErrorMsgNonEmptyList(String attrName, String ruleType, String ruleName) {
+  protected String getErrorMsgNonEmptyList(String attrName, String ruleType, String ruleName) {
     return "non empty attribute '" + attrName + "' in '" + ruleType
         + "' rule '" + ruleName + "' has to have at least one value";
   }
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
new file mode 100644
index 0000000..c15e9ac
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
@@ -0,0 +1,248 @@
+// Copyright 2006-2015 Google Inc. 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.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Predicates;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests of Attribute code.
+ */
+@RunWith(JUnit4.class)
+public class AttributeTest {
+
+  private void assertDefaultValue(Object expected, Attribute attr) {
+    assertEquals(expected, attr.getDefaultValue(null));
+  }
+
+  private void assertType(Type<?> expectedType, Attribute attr) {
+    assertEquals(expectedType, attr.getType());
+  }
+
+  @Test
+  public void testBasics() throws Exception {
+    Attribute attr = attr("foo", Type.INTEGER).mandatory().value(3).build();
+    assertEquals("foo", attr.getName());
+    assertEquals(3, attr.getDefaultValue(null));
+    assertEquals(Type.INTEGER, attr.getType());
+    assertTrue(attr.isMandatory());
+    assertTrue(attr.isDocumented());
+    attr = attr("$foo", Type.INTEGER).build();
+    assertFalse(attr.isDocumented());
+  }
+
+  @Test
+  public void testNonEmptyReqiresListType() throws Exception {
+    try {
+      attr("foo", Type.INTEGER).nonEmpty().value(3).build();
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessage("attribute 'foo' must be a list");
+    }
+  }
+
+  @Test
+  public void testNonEmpty() throws Exception {
+    Attribute attr = attr("foo", Type.LABEL_LIST).nonEmpty().legacyAllowAnyFileType().build();
+    assertEquals("foo", attr.getName());
+    assertEquals(Type.LABEL_LIST, attr.getType());
+    assertTrue(attr.isNonEmpty());
+  }
+
+  @Test
+  public void testSingleArtifactReqiresLabelType() throws Exception {
+    try {
+      attr("foo", Type.INTEGER).singleArtifact().value(3).build();
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessage("attribute 'foo' must be a label-valued type");
+    }
+  }
+
+  @Test
+  public void testDoublePropertySet() {
+    Attribute.Builder<String> builder = attr("x", STRING).mandatory().cfg(HOST).undocumented("")
+        .value("y");
+    try {
+      builder.mandatory();
+      fail();
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.cfg(HOST);
+      fail();
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.undocumented("");
+      fail();
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.value("z");
+      fail();
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+
+    builder = attr("$x", STRING);
+    try {
+      builder.undocumented("");
+      fail();
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+  }
+
+  /**
+   *  Tests the "convenience factories" (string, label, etc) for default
+   *  values.
+   */
+  @Test
+  public void testConvenienceFactoriesDefaultValues() throws Exception {
+    assertDefaultValue(0,
+                       attr("x", INTEGER).build());
+    assertDefaultValue(42,
+                       attr("x", INTEGER).value(42).build());
+
+    assertDefaultValue("",
+                       attr("x", STRING).build());
+    assertDefaultValue("foo",
+                       attr("x", STRING).value("foo").build());
+
+    Label label = Label.parseAbsolute("//foo:bar");
+    assertDefaultValue(null,
+                       attr("x", LABEL).legacyAllowAnyFileType().build());
+    assertDefaultValue(label,
+                       attr("x", LABEL).legacyAllowAnyFileType().value(label).build());
+
+    List<String> slist = Arrays.asList("foo", "bar");
+    assertDefaultValue(Collections.emptyList(),
+                       attr("x", STRING_LIST).build());
+    assertDefaultValue(slist,
+                       attr("x", STRING_LIST).value(slist).build());
+
+    List<Label> llist = Arrays.asList(Label.parseAbsolute("//foo:bar"),
+                                      Label.parseAbsolute("//foo:wiz"));
+    assertDefaultValue(Collections.emptyList(),
+                       attr("x", LABEL_LIST).legacyAllowAnyFileType().build());
+    assertDefaultValue(llist,
+                       attr("x", LABEL_LIST).legacyAllowAnyFileType().value(llist).build());
+  }
+
+  /**
+   *  Tests the "convenience factories" (string, label, etc) for types.
+   */
+  @Test
+  public void testConvenienceFactoriesTypes() throws Exception {
+    assertType(INTEGER,
+               attr("x", INTEGER).build());
+    assertType(INTEGER,
+               attr("x", INTEGER).value(42).build());
+
+    assertType(STRING,
+               attr("x", STRING).build());
+    assertType(STRING,
+               attr("x", STRING).value("foo").build());
+
+    Label label = Label.parseAbsolute("//foo:bar");
+    assertType(LABEL,
+                       attr("x", LABEL).legacyAllowAnyFileType().build());
+    assertType(LABEL,
+               attr("x", LABEL).legacyAllowAnyFileType().value(label).build());
+
+    List<String> slist = Arrays.asList("foo", "bar");
+    assertType(STRING_LIST,
+               attr("x", STRING_LIST).build());
+    assertType(STRING_LIST,
+               attr("x", STRING_LIST).value(slist).build());
+
+    List<Label> llist = Arrays.asList(Label.parseAbsolute("//foo:bar"),
+                                      Label.parseAbsolute("//foo:wiz"));
+    assertType(LABEL_LIST,
+               attr("x", LABEL_LIST).legacyAllowAnyFileType().build());
+    assertType(LABEL_LIST,
+               attr("x", LABEL_LIST).legacyAllowAnyFileType().value(llist).build());
+  }
+
+  @Test
+  public void testCloneBuilder() {
+    FileTypeSet txtFiles = FileTypeSet.of(FileType.of("txt"));
+    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());
+    assertEquals(Predicates.alwaysTrue(), childAttr1.getAllowedRuleClassesPredicate());
+    assertTrue(childAttr1.isMandatory());
+    assertFalse(childAttr1.isNonEmpty());
+
+    Attribute childAttr2 = parentAttr.cloneBuilder()
+        .nonEmpty()
+        .allowedRuleClasses(ruleClasses)
+        .build();
+    assertEquals("x", childAttr2.getName());
+    assertEquals(txtFiles, childAttr2.getAllowedFileTypesPredicate());
+    assertEquals(ruleClasses, childAttr2.getAllowedRuleClassesPredicate());
+    assertTrue(childAttr2.isMandatory());
+    assertTrue(childAttr2.isNonEmpty());
+
+    //Check if the parent attribute is unchanged
+    assertFalse(parentAttr.isNonEmpty());
+    assertEquals(Predicates.alwaysTrue(), parentAttr.getAllowedRuleClassesPredicate());
+  }
+
+  /**
+   * Tests that configurability settings are properly received.
+   */
+  @Test
+  public void testConfigurability() {
+    assertTrue(attr("foo_configurable", Type.LABEL_LIST).legacyAllowAnyFileType().build()
+        .isConfigurable());
+    assertFalse(attr("foo_nonconfigurable", Type.LABEL_LIST).legacyAllowAnyFileType()
+        .nonconfigurable("test").build().isConfigurable());
+  }
+}
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
new file mode 100644
index 0000000..c5f5c79
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
@@ -0,0 +1,103 @@
+// Copyright 2006-2015 Google Inc. 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 static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+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.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A test for the {@code exports_files} function defined in
+ * {@link PackageFactory}.
+ */
+@RunWith(JUnit4.class)
+public class ExportsFilesTest {
+
+  private Scratch scratch = new Scratch("/workspace");
+  private EventCollectionApparatus events = new EventCollectionApparatus();
+  private PackageFactoryApparatus packages = new PackageFactoryApparatus(events, scratch);
+
+  private Package pkg() throws Exception {
+    Path buildFile = scratch.file("pkg/BUILD",
+                                  "exports_files(['foo.txt', 'bar.txt'])");
+    return packages.createPackage("pkg", buildFile);
+  }
+
+  @Test
+  public void testExportsFilesRegistersFilesWithPackage() throws Exception {
+    List<String> names = getFileNamesOf(pkg());
+    String expected = "//pkg:BUILD //pkg:bar.txt //pkg:foo.txt";
+    assertEquals(expected, Joiner.on(' ').join(names));
+  }
+
+  /**
+   * Returns the names of the input files that are known to pkg.
+   */
+  private static List<String> getFileNamesOf(Package pkg) {
+    List<String> names = new ArrayList<>();
+    for (FileTarget target : pkg.getTargets(FileTarget.class)) {
+      names.add(target.getLabel().toString());
+    }
+    Collections.sort(names);
+    return names;
+  }
+
+  @Test
+  public void testFileThatsNotRegisteredYieldsUnknownTargetException() throws Exception {
+    try {
+      pkg().getTarget("baz.txt");
+      fail();
+    } catch (NoSuchTargetException e) {
+      assertThat(e).hasMessage("no such target '//pkg:baz.txt':"
+          + " target 'baz.txt' not declared in package 'pkg' defined by /workspace/pkg/BUILD");
+    }
+  }
+
+  @Test
+  public void testRegisteredFilesAreRetrievable() throws Exception {
+    Package pkg = pkg();
+    assertEquals("foo.txt", pkg.getTarget("foo.txt").getName());
+    assertEquals("bar.txt", pkg.getTarget("bar.txt").getName());
+  }
+
+  @Test
+  public void testExportsFilesAndRuleNameConflict() throws Exception {
+    events.setFailFast(false);
+
+    Path buildFile = scratch.file("pkg2/BUILD",
+        "exports_files(['foo'])",
+        "genrule(name = 'foo', srcs = ['bar'], outs = [],",
+        "        cmd = '/bin/true')");
+    Package pkg = packages.createPackage("pkg2", buildFile);
+    events.assertContainsEvent("rule 'foo' in package 'pkg2' conflicts with "
+                               + "existing source file");
+    assertTrue(pkg.getTarget("foo") instanceof InputFile);
+  }
+
+}
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
new file mode 100644
index 0000000..bd8d536
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
@@ -0,0 +1,254 @@
+// Copyright 2008-2015 Google Inc. 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.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.devtools.build.lib.packages.GlobCache.BadGlobException;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import org.junit.After;
+import org.junit.Before;
+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.Collections;
+import java.util.List;
+
+/**
+ * Tests for {@link GlobCache}
+ */
+@RunWith(JUnit4.class)
+public class GlobCacheTest {
+
+  private static final List<String> NONE = Collections.emptyList();
+
+  private Scratch scratch = new Scratch("/workspace");
+
+  private Path packageDirectory;
+  private Path buildFile;
+  private GlobCache cache;
+
+  @Before
+  public void setUp() throws Exception {
+    buildFile = scratch.file("isolated/BUILD",
+        "# contents don't matter in this test");
+    scratch.file("isolated/sub/BUILD",
+        "# contents don't matter in this test");
+
+    packageDirectory = buildFile.getParentDirectory();
+
+    scratch.file("isolated/first.txt",
+        "# this is first.txt");
+
+    scratch.file("isolated/second.txt",
+        "# this is second.txt");
+
+    scratch.file("isolated/first.js",
+        "# this is first.js");
+
+    scratch.file("isolated/second.js",
+        "# this is second.js");
+
+    // Files in subdirectories
+
+    scratch.file("isolated/foo/first.js",
+        "# this is foo/first.js");
+
+    scratch.file("isolated/foo/second.js",
+        "# this is foo/second.js");
+
+    scratch.file("isolated/bar/first.js",
+        "# this is bar/first.js");
+
+    scratch.file("isolated/bar/second.js",
+        "# this is bar/second.js");
+
+    scratch.file("isolated/sub/sub.js",
+        "# this is sub/sub.js");
+
+    cache = new GlobCache(packageDirectory, PackageIdentifier.createInDefaultRepo("isolated"),
+        new CachingPackageLocator() {
+      @Override
+      public Path getBuildFileForPackage(String packageName) {
+        if (packageName.equals("isolated")) {
+          return scratch.resolve("isolated/BUILD");
+        } else if (packageName.equals("isolated/sub")) {
+          return scratch.resolve("isolated/sub/BUILD");
+        } else {
+          return null;
+        }
+      }
+    }, null, TestUtils.getPool());
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    FileSystemUtils.deleteTreesBelow(scratch.getFileSystem().getRootDirectory());
+  }
+
+  @Test
+  public void testSafeGlob() throws Exception {
+    List<Path> paths = cache.safeGlob("*.js", false).get();
+    assertPathsAre(paths,
+        "/workspace/isolated/first.js", "/workspace/isolated/second.js");
+  }
+
+  @Test
+  public void testSafeGlobInvalidPatterns() throws Exception {
+    for (String pattern : new String[] {
+        "Foo?.txt", "List{Test}.py", "List(Test).py" }) {
+      try {
+        cache.safeGlob(pattern, false);
+        fail("Expected pattern " + pattern + " to fail");
+      } catch (BadGlobException expected) {
+      }
+    }
+  }
+
+  @Test
+  public void testGetGlob() throws Exception {
+    List<String> glob = cache.getGlob("*.js");
+    assertThat(glob).containsExactly("first.js", "second.js");
+  }
+
+  @Test
+  public void testGetGlob_subdirectory() throws Exception {
+    List<String> glob = cache.getGlob("foo/*.js");
+    assertThat(glob).containsExactly("foo/first.js", "foo/second.js");
+  }
+
+  @Test
+  public void testGetKeySet() throws Exception {
+    assertThat(cache.getKeySet()).isEmpty();
+
+    cache.getGlob("*.java");
+    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false));
+
+    cache.getGlob("*.java");
+    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false));
+
+    cache.getGlob("*.js");
+    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false));
+
+    cache.getGlob("*.java", true);
+    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false),
+        Pair.of("*.java", true));
+
+    try {
+      cache.getGlob("invalid?");
+      fail("Expected an invalid regex exception");
+    } catch (BadGlobException expected) {
+    }
+    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false),
+        Pair.of("*.java", true));
+
+    cache.getGlob("foo/first.*");
+    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.java", true),
+        Pair.of("*.js", false), Pair.of("foo/first.*", false));
+  }
+
+  @Test
+  public void testGlob() throws Exception {
+    assertEmpty(cache.glob(list("*.java"), NONE, false));
+
+    assertThat(cache.glob(list("*.*"), NONE, false)).containsExactly("first.js", "first.txt",
+        "second.js", "second.txt").inOrder();
+
+    assertThat(cache.glob(list("*.*"), list("first.js"), false)).containsExactly("first.txt",
+        "second.js", "second.txt").inOrder();
+
+    assertThat(cache.glob(list("*.txt", "first.*"), NONE, false)).containsExactly("first.txt",
+        "second.txt", "first.js").inOrder();
+  }
+
+  @Test
+  public void testSetGlobPaths() throws Exception {
+    // This pattern matches no files.
+    String pattern = "fake*.java";
+    assertThat(cache.getKeySet()).doesNotContain(pattern);
+
+    List<String> results = cache.getGlob(pattern, false);
+
+    assertThat(cache.getKeySet()).contains(Pair.of(pattern, false));
+    assertThat(results).isEmpty();
+
+    cache.setGlobPaths(pattern, false, Futures.<List<Path>>immediateFuture(Lists.newArrayList(
+        scratch.resolve("isolated/fake.txt"),
+        scratch.resolve("isolated/fake.py"))));
+
+    assertThat(cache.getGlob(pattern, false)).containsExactly("fake.py", "fake.txt");
+  }
+
+  @Test
+  public void testGlobsUpToDate() throws Exception {
+    assertTrue(cache.globsUpToDate());
+
+    // Initialize the cache
+    cache.getGlob("*.txt");
+    assertTrue(cache.globsUpToDate());
+
+    cache.getGlob("*.js");
+    assertTrue(cache.globsUpToDate());
+
+    // Change the filesystem
+    scratch.file("isolated/third.txt",
+        "# this is third.txt");
+    assertFalse(cache.globsUpToDate());
+
+    // Fool the cache to observe the method's behavior.
+    cache.setGlobPaths("*.txt", false, Futures.<List<Path>>immediateFuture(Lists.newArrayList(
+        scratch.resolve("isolated/first.txt"),
+        scratch.resolve("isolated/second.txt"),
+        scratch.resolve("isolated/third.txt"))));
+    assertTrue(cache.globsUpToDate());
+  }
+
+  @Test
+  public void testRecursiveGlobDoesNotMatchSubpackage() throws Exception {
+    List<String> glob = cache.getGlob("**/*.js");
+    assertThat(glob).containsExactly("first.js", "second.js", "foo/first.js", "bar/first.js",
+        "foo/second.js", "bar/second.js");
+  }
+
+  private void assertEmpty(Collection<?> glob) {
+    assertThat(glob).isEmpty();
+  }
+
+  private void assertPathsAre(List<Path> paths, String... strings) {
+    List<String> pathStrings = new ArrayList<>();
+    for (Path path : paths) {
+      pathStrings.add(path.getPathString());
+    }
+    assertThat(pathStrings).containsExactlyElementsIn(Arrays.asList(strings));
+  }
+
+  /* syntactic shorthand for Lists.newArrayList(strings) */
+  private List<String> list(String... strings) {
+    return Lists.newArrayList(strings);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RuleClassTest.java b/src/test/java/com/google/devtools/build/lib/packages/RuleClassTest.java
new file mode 100644
index 0000000..7eb0ed4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/RuleClassTest.java
@@ -0,0 +1,830 @@
+// Copyright 2006-2015 Google Inc. 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.common.truth.Truth.assertWithMessage;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.substitutePlaceholderIntoTemplate;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventCollector;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.Location.LineAndColumn;
+import com.google.devtools.build.lib.packages.Attribute.ValidityPredicate;
+import com.google.devtools.build.lib.packages.Package.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tests for {@link RuleClass}.
+ */
+public class RuleClassTest 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();
+        }
+  };
+
+  private static final class DummyFragment extends BuildConfiguration.Fragment {
+    @Override
+    public String getName() {
+      return "dummy-for-testing";
+    }
+
+    @Override
+    public String cacheKey() {
+      return "some cache key";
+    }
+  }
+
+  private static final Predicate<String> PREFERRED_DEPENDENCY_PREDICATE = Predicates.alwaysFalse();
+
+  private static RuleClass createRuleClassA() throws Label.SyntaxException {
+    return new RuleClass("ruleA", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE,
+        DUMMY_CONFIGURED_TARGET_FACTORY, PredicatesWithMessage.<Rule>alwaysTrue(),
+        PREFERRED_DEPENDENCY_PREDICATE, ImmutableSet.<Class<?>>of(), null, null,
+        ImmutableSet.<Class<?>>of(), false, true,
+        attr("my-string-attr", STRING).mandatory().build(),
+        attr("my-label-attr", LABEL).mandatory().legacyAllowAnyFileType()
+            .value(Label.parseAbsolute("//default:label")).build(),
+        attr("my-labellist-attr", LABEL_LIST).mandatory().legacyAllowAnyFileType().build(),
+        attr("my-integer-attr", INTEGER).value(42).build(),
+        attr("my-string-attr2", STRING).mandatory().value((String) null).build(),
+        attr("my-stringlist-attr", STRING_LIST).build(),
+        attr("my-sorted-stringlist-attr", STRING_LIST).orderIndependent().build());
+  }
+
+  private static RuleClass createRuleClassB(RuleClass ruleClassA) {
+    // emulates attribute inheritance
+    List<Attribute> attributes = new ArrayList<>(ruleClassA.getAttributes());
+    attributes.add(attr("another-string-attr", STRING).mandatory().build());
+    return new RuleClass("ruleB", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attributes.toArray(new Attribute[0]));
+  }
+
+  public void testRuleClassBasics() throws Exception {
+    RuleClass ruleClassA = createRuleClassA();
+
+    assertEquals("ruleA", ruleClassA.getName());
+    assertEquals(7, ruleClassA.getAttributeCount());
+
+    assertEquals(0, (int) ruleClassA.getAttributeIndex("my-string-attr"));
+    assertEquals(1, (int) ruleClassA.getAttributeIndex("my-label-attr"));
+    assertEquals(2, (int) ruleClassA.getAttributeIndex("my-labellist-attr"));
+    assertEquals(3, (int) ruleClassA.getAttributeIndex("my-integer-attr"));
+    assertEquals(4, (int) ruleClassA.getAttributeIndex("my-string-attr2"));
+    assertEquals(5, (int) ruleClassA.getAttributeIndex("my-stringlist-attr"));
+    assertEquals(6, (int) ruleClassA.getAttributeIndex("my-sorted-stringlist-attr"));
+
+    assertEquals(ruleClassA.getAttribute(0),
+                 ruleClassA.getAttributeByName("my-string-attr"));
+    assertEquals(ruleClassA.getAttribute(1),
+                 ruleClassA.getAttributeByName("my-label-attr"));
+    assertEquals(ruleClassA.getAttribute(2),
+                 ruleClassA.getAttributeByName("my-labellist-attr"));
+    assertEquals(ruleClassA.getAttribute(3),
+                 ruleClassA.getAttributeByName("my-integer-attr"));
+    assertEquals(ruleClassA.getAttribute(4),
+                 ruleClassA.getAttributeByName("my-string-attr2"));
+    assertEquals(ruleClassA.getAttribute(5),
+                 ruleClassA.getAttributeByName("my-stringlist-attr"));
+    assertEquals(ruleClassA.getAttribute(6),
+                 ruleClassA.getAttributeByName("my-sorted-stringlist-attr"));
+
+    assertEquals("", // default based on type
+                 ruleClassA.getAttribute(0).getDefaultValue(null));
+    assertEquals(Label.parseAbsolute("//default:label"),
+                 ruleClassA.getAttribute(1).getDefaultValue(null));
+    assertEquals(Collections.emptyList(),
+                 ruleClassA.getAttribute(2).getDefaultValue(null));
+    assertEquals(42,
+                 ruleClassA.getAttribute(3).getDefaultValue(null));
+    assertEquals(null, // default explicitly specified
+                 ruleClassA.getAttribute(4).getDefaultValue(null));
+    assertEquals(Collections.emptyList(),
+                 ruleClassA.getAttribute(5).getDefaultValue(null));
+    assertEquals(Collections.emptyList(),
+                 ruleClassA.getAttribute(6).getDefaultValue(null));
+  }
+
+  public void testRuleClassInheritance() throws Exception {
+    RuleClass ruleClassA = createRuleClassA();
+    RuleClass ruleClassB = createRuleClassB(ruleClassA);
+
+    assertEquals("ruleB", ruleClassB.getName());
+    assertEquals(8, ruleClassB.getAttributeCount());
+
+    assertEquals(0, (int) ruleClassB.getAttributeIndex("my-string-attr"));
+    assertEquals(1, (int) ruleClassB.getAttributeIndex("my-label-attr"));
+    assertEquals(2, (int) ruleClassB.getAttributeIndex("my-labellist-attr"));
+    assertEquals(3, (int) ruleClassB.getAttributeIndex("my-integer-attr"));
+    assertEquals(4, (int) ruleClassB.getAttributeIndex("my-string-attr2"));
+    assertEquals(5, (int) ruleClassB.getAttributeIndex("my-stringlist-attr"));
+    assertEquals(6, (int) ruleClassB.getAttributeIndex("my-sorted-stringlist-attr"));
+    assertEquals(7, (int) ruleClassB.getAttributeIndex("another-string-attr"));
+
+    assertEquals(ruleClassB.getAttribute(0),
+                 ruleClassB.getAttributeByName("my-string-attr"));
+    assertEquals(ruleClassB.getAttribute(1),
+                 ruleClassB.getAttributeByName("my-label-attr"));
+    assertEquals(ruleClassB.getAttribute(2),
+                 ruleClassB.getAttributeByName("my-labellist-attr"));
+    assertEquals(ruleClassB.getAttribute(3),
+                 ruleClassB.getAttributeByName("my-integer-attr"));
+    assertEquals(ruleClassB.getAttribute(4),
+                 ruleClassB.getAttributeByName("my-string-attr2"));
+    assertEquals(ruleClassB.getAttribute(5),
+                 ruleClassB.getAttributeByName("my-stringlist-attr"));
+    assertEquals(ruleClassB.getAttribute(6),
+                 ruleClassB.getAttributeByName("my-sorted-stringlist-attr"));
+    assertEquals(ruleClassB.getAttribute(7),
+                 ruleClassB.getAttributeByName("another-string-attr"));
+  }
+
+  private static final String TEST_PACKAGE_NAME = "testpackage";
+
+  private static final String TEST_RULE_NAME = "my-rule-A";
+
+  private static final int TEST_RULE_DEFINED_AT_LINE = 42;
+
+  private static final String TEST_RULE_LABEL = "//" + TEST_PACKAGE_NAME + ":" + TEST_RULE_NAME;
+
+  private Path testBuildfilePath;
+  private Location testRuleLocation;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    testBuildfilePath = scratch.resolve("/home/user/workspace/testpackage/BUILD");
+    testRuleLocation = Location.fromPathAndStartColumn(
+        testBuildfilePath, 0, 0, new LineAndColumn(TEST_RULE_DEFINED_AT_LINE, 0));
+  }
+
+  private Package.Builder createDummyPackageBuilder() {
+    return new Builder(PackageIdentifier.createInDefaultRepo(TEST_PACKAGE_NAME))
+        .setFilename(testBuildfilePath)
+        .setMakeEnv(new MakeEnvironment.Builder());
+  }
+
+  public void testDuplicatedDeps() throws Exception {
+    RuleClass depsRuleClass = new RuleClass("ruleDeps", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attr("list1", LABEL_LIST).mandatory().legacyAllowAnyFileType().build(),
+        attr("list2", LABEL_LIST).mandatory().legacyAllowAnyFileType().build(),
+        attr("list3", LABEL_LIST).mandatory().legacyAllowAnyFileType().build());
+
+    // LinkedHashMap -> predictable iteration order for testing
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    attributeValues.put("list1", Lists.newArrayList("//testpackage:dup1", ":dup1", ":nodup"));
+    attributeValues.put("list2", Lists.newArrayList(":nodup1", ":nodup2"));
+    attributeValues.put("list3", Lists.newArrayList(":dup1", ":dup1", ":dup2", ":dup2"));
+
+    reporter.removeHandler(failFastHandler);
+    createRule(depsRuleClass, "depsRule", attributeValues, testRuleLocation);
+
+    assertSame(3, eventCollector.count());
+    assertDupError("//testpackage:dup1", "list1", "depsRule");
+    assertDupError("//testpackage:dup1", "list3", "depsRule");
+    assertDupError("//testpackage:dup2", "list3", "depsRule");
+  }
+
+  private void assertDupError(String label, String attrName, String ruleName) {
+    assertContainsEvent(String.format("Label '%s' is duplicated in the '%s' attribute of rule '%s'",
+        label, attrName, ruleName));
+  }
+
+  public void testCreateRuleWithLegacyPublicVisibility() throws Exception {
+    RuleClass ruleClass = new RuleClass("ruleVis", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attr("visibility", LABEL_LIST).legacyAllowAnyFileType().build());
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("visibility", Arrays.asList("//visibility:legacy_public"));
+
+    reporter.removeHandler(failFastHandler);
+    EventCollector collector = new EventCollector(EventKind.ERRORS);
+    reporter.addHandler(collector);
+
+    createRule(ruleClass, TEST_RULE_NAME, attributeValues, testRuleLocation);
+
+    assertContainsEvent("//visibility:legacy_public only allowed in package declaration");
+  }
+
+  public void testCreateRule() throws Exception {
+    RuleClass ruleClassA = createRuleClassA();
+
+    // LinkedHashMap -> predictable iteration order for testing
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    attributeValues.put("my-labellist-attr", "foobar"); // wrong type
+    attributeValues.put("bogus-attr", "foobar"); // no such attr
+    attributeValues.put("my-stringlist-attr", Arrays.asList("foo", "bar"));
+
+    reporter.removeHandler(failFastHandler);
+    EventCollector collector = new EventCollector(EventKind.ERRORS);
+    reporter.addHandler(collector);
+
+    Rule rule = createRule(ruleClassA, TEST_RULE_NAME, attributeValues, testRuleLocation);
+
+    // TODO(blaze-team): (2009) refactor to use assertContainsEvent
+    Iterator<String> expectedMessages = Arrays.asList(
+        "expected value of type 'list(label)' for attribute 'my-labellist-attr' "
+        + "in 'ruleA' rule, but got 'foobar' (string)",
+        "no such attribute 'bogus-attr' in 'ruleA' rule",
+        "missing value for mandatory "
+        + "attribute 'my-string-attr' in 'ruleA' rule",
+        "missing value for mandatory attribute 'my-label-attr' in 'ruleA' rule",
+        "missing value for mandatory "
+        + "attribute 'my-labellist-attr' in 'ruleA' rule",
+        "missing value for mandatory "
+        + "attribute 'my-string-attr2' in 'ruleA' rule"
+    ).iterator();
+
+    for (Event event : collector) {
+      assertEquals(TEST_RULE_DEFINED_AT_LINE,
+          event.getLocation().getStartLineAndColumn().getLine());
+      assertEquals(testBuildfilePath.asFragment(), event.getLocation().getPath());
+      assertEquals(TEST_RULE_LABEL + ": " + expectedMessages.next(), event.getMessage());
+    }
+
+    // Test basic rule properties:
+    assertEquals("ruleA",
+                 rule.getRuleClass());
+    assertEquals(TEST_RULE_NAME,
+                 rule.getName());
+    assertEquals(TEST_RULE_LABEL,
+                 rule.getLabel().toString());
+
+    // Test attribute access:
+    AttributeMap attributes = RawAttributeMapper.of(rule);
+    assertEquals("//default:label",
+                 attributes.get("my-label-attr", Type.LABEL).toString());
+    assertEquals(42,
+                 attributes.get("my-integer-attr", Type.INTEGER).intValue());
+    assertEquals("",  // missing attribute -> default chosen based on type
+                 attributes.get("my-string-attr", Type.STRING));
+    assertThat(attributes.get("my-labellist-attr", Type.LABEL_LIST)).isEmpty();
+    assertEquals(Arrays.asList("foo", "bar"),
+                 attributes.get("my-stringlist-attr", Type.STRING_LIST));
+    try {
+      attributes.get("my-labellist-attr", Type.STRING); // wrong type
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Attribute my-labellist-attr is not of type string "
+          + "in rule //testpackage:my-rule-A");
+    }
+  }
+
+  public void testImplicitOutputs() throws Exception {
+    RuleClass ruleClassC = new RuleClass("ruleC", false, false, false, false, false, false,
+        ImplicitOutputsFunction.fromTemplates("foo-%{name}.bar",
+                                              "lib%{name}-wazoo-%{name}.mumble",
+                                              "stuff-%{outs}-bar"),
+        RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attr("outs", OUTPUT_LIST).build());
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("outs", Collections.singletonList("explicit_out"));
+
+    Rule rule = createRule(ruleClassC, "myrule", attributeValues, testRuleLocation);
+
+    Set<String> set = new HashSet<>();
+    for (OutputFile outputFile : rule.getOutputFiles()) {
+      set.add(outputFile.getName());
+      assertSame(rule, outputFile.getGeneratingRule());
+    }
+    assertThat(set).containsExactly("foo-myrule.bar", "libmyrule-wazoo-myrule.mumble",
+        "stuff-explicit_out-bar", "explicit_out");
+  }
+
+  public void testImplicitOutsWithBasenameDirname() throws Exception {
+    RuleClass ruleClass = new RuleClass("ruleClass", false, false, false, false, false, false,
+        ImplicitOutputsFunction.fromTemplates("%{dirname}lib%{basename}.bar"), RuleClass.NO_CHANGE,
+        DUMMY_CONFIGURED_TARGET_FACTORY, PredicatesWithMessage.<Rule>alwaysTrue(),
+        PREFERRED_DEPENDENCY_PREDICATE, ImmutableSet.<Class<?>>of(), null, null,
+        ImmutableSet.<Class<?>>of(), false, true);
+
+    Rule rule = createRule(ruleClass, "myRule", Collections.<String, Object>emptyMap(),
+        testRuleLocation);
+    assertEquals("libmyRule.bar", Iterables.getOnlyElement(rule.getOutputFiles()).getName());
+
+    Rule ruleWithSlash = createRule(ruleClass, "myRule/with/slash",
+        Collections.<String, Object>emptyMap(), testRuleLocation);
+    assertEquals("myRule/with/libslash.bar",
+                 Iterables.getOnlyElement(ruleWithSlash.getOutputFiles()).getName());
+  }
+
+  /**
+   * Helper routine that instantiates a rule class with the given computed default and supporting
+   * attributes for the default to reference.
+   */
+  private static RuleClass getRuleClassWithComputedDefault(Attribute computedDefault) {
+    return new RuleClass("ruleClass", false, false, false, false, false, false,
+        ImplicitOutputsFunction.fromTemplates("empty"), RuleClass.NO_CHANGE,
+        DUMMY_CONFIGURED_TARGET_FACTORY, PredicatesWithMessage.<Rule>alwaysTrue(),
+        PREFERRED_DEPENDENCY_PREDICATE, ImmutableSet.<Class<?>>of(), null, null,
+        ImmutableSet.<Class<?>>of(), false, true,
+        attr("condition", BOOLEAN).value(false).build(),
+        attr("declared1", BOOLEAN).value(false).build(),
+        attr("declared2", BOOLEAN).value(false).build(),
+        attr("nonconfigurable", BOOLEAN).nonconfigurable("test").value(false).build(),
+        computedDefault);
+  }
+
+  /**
+   * Helper routine that checks that a computed default is valid and bound to the expected value.
+   */
+  private void checkValidComputedDefault(Object expectedValue, Attribute computedDefault,
+      ImmutableMap<String, Object> attrValueMap) throws Exception {
+    assertTrue(computedDefault.getDefaultValueForTesting() instanceof Attribute.ComputedDefault);
+    Rule rule = createRule(getRuleClassWithComputedDefault(computedDefault), "myRule",
+        attrValueMap, testRuleLocation);
+    AttributeMap attributes = RawAttributeMapper.of(rule);
+    assertEquals(
+        expectedValue,
+        attributes.get(computedDefault.getName(), computedDefault.getType()));
+  }
+
+  /**
+   * Helper routine that checks that a computed default is invalid due to declared dependency
+   * issues and fails with the expected message.
+   */
+  private void checkInvalidComputedDefault(Attribute computedDefault, String expectedMessage)
+      throws Exception {
+    try {
+      createRule(getRuleClassWithComputedDefault(computedDefault), "myRule",
+              ImmutableMap.<String, Object>of(), testRuleLocation);
+      fail("Expected computed default \"" + computedDefault.getName() + "\" to fail with "
+          + "declaration errors");
+    } catch (IllegalArgumentException e) {
+      // Expected outcome.
+      assertThat(e).hasMessage(expectedMessage);
+    }
+  }
+
+  /**
+   * Tests computed default values are computed as expected.
+   */
+  public void testComputedDefault() throws Exception {
+    Attribute computedDefault =
+        attr("$result", BOOLEAN).value(new Attribute.ComputedDefault("condition") {
+          @Override
+          public Object getDefault(AttributeMap rule) {
+            return rule.get("condition", Type.BOOLEAN);
+          }
+        }).build();
+
+    checkValidComputedDefault(Boolean.FALSE, computedDefault,
+        ImmutableMap.<String, Object>of("condition", Boolean.FALSE));
+    checkValidComputedDefault(Boolean.TRUE, computedDefault,
+        ImmutableMap.<String, Object>of("condition", Boolean.TRUE));
+  }
+
+  /**
+   * Tests that computed defaults can only read attribute values for configurable attributes that
+   * have been explicitly declared.
+   */
+  public void testComputedDefaultDeclarations() throws Exception {
+    checkValidComputedDefault(
+        Boolean.FALSE,
+        attr("$good_default_no_declares", BOOLEAN).value(
+            new Attribute.ComputedDefault() {
+              @Override public Object getDefault(AttributeMap rule) {
+                // OK: not a value check:
+                return rule.isAttributeValueExplicitlySpecified("undeclared");
+              }
+        }).build(),
+        ImmutableMap.<String, Object>of());
+
+    checkValidComputedDefault(
+        Boolean.FALSE,
+        attr("$good_default_one_declare", BOOLEAN).value(
+            new Attribute.ComputedDefault("declared1") {
+              @Override public Object getDefault(AttributeMap rule) {
+                return rule.get("declared1", Type.BOOLEAN);
+              }
+        }).build(),
+        ImmutableMap.<String, Object>of());
+
+    checkValidComputedDefault(
+        Boolean.FALSE,
+        attr("$good_default_two_declares", BOOLEAN).value(
+            new Attribute.ComputedDefault("declared1", "declared2") {
+              @Override public Object getDefault(AttributeMap rule) {
+                return rule.get("declared1", Type.BOOLEAN) && rule.get("declared2", Type.BOOLEAN);
+              }
+        }).build(),
+        ImmutableMap.<String, Object>of());
+
+    checkInvalidComputedDefault(
+        attr("$bad_default_no_declares", BOOLEAN).value(
+            new Attribute.ComputedDefault() {
+              @Override public Object getDefault(AttributeMap rule) {
+                return rule.get("declared1", Type.BOOLEAN);
+              }
+        }).build(),
+        "attribute \"declared1\" isn't available in this computed default context");
+
+    checkInvalidComputedDefault(
+        attr("$bad_default_one_declare", BOOLEAN).value(
+            new Attribute.ComputedDefault("declared1") {
+              @Override public Object getDefault(AttributeMap rule) {
+                return rule.get("declared1", Type.BOOLEAN) || rule.get("declared2", Type.BOOLEAN);
+              }
+        }).build(),
+        "attribute \"declared2\" isn't available in this computed default context");
+
+    checkInvalidComputedDefault(
+        attr("$bad_default_two_declares", BOOLEAN).value(
+            new Attribute.ComputedDefault("declared1", "declared2") {
+              @Override public Object getDefault(AttributeMap rule) {
+                return rule.get("condition", Type.BOOLEAN);
+              }
+        }).build(),
+        "attribute \"condition\" isn't available in this computed default context");
+  }
+
+  /**
+   * Tests that computed defaults *can* read attribute values for non-configurable attributes
+   * without needing to explicitly declare them.
+   */
+  public void testComputedDefaultWithNonConfigurableAttributes() throws Exception {
+    checkValidComputedDefault(
+        Boolean.FALSE,
+        attr("$good_default_reading_undeclared_nonconfigurable_attribute", BOOLEAN).value(
+            new Attribute.ComputedDefault() {
+              @Override public Object getDefault(AttributeMap rule) {
+                return rule.get("nonconfigurable", Type.BOOLEAN);
+              }
+        }).build(),
+        ImmutableMap.<String, Object>of());
+  }
+
+  public void testOutputsAreOrdered() throws Exception {
+    RuleClass ruleClassC = new RuleClass("ruleC", false, false, false, false, false, false,
+        ImplicitOutputsFunction.fromTemplates("first-%{name}", "second-%{name}", "out-%{outs}"),
+        RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attr("outs", OUTPUT_LIST).build());
+
+    Map<String, Object> attributeValues = new HashMap<>();
+    attributeValues.put("outs", ImmutableList.of("third", "fourth"));
+
+    Rule rule = createRule(ruleClassC, "myrule", attributeValues, testRuleLocation);
+
+    List<String> actual = new ArrayList<>();
+    for (OutputFile outputFile : rule.getOutputFiles()) {
+      actual.add(outputFile.getName());
+      assertSame(rule, outputFile.getGeneratingRule());
+    }
+    assertWithMessage("unexpected output set").that(actual).containsExactly("first-myrule",
+        "second-myrule", "out-third", "out-fourth", "third", "fourth");
+    assertWithMessage("invalid output ordering").that(actual).containsExactly("first-myrule",
+        "second-myrule", "out-third", "out-fourth", "third", "fourth").inOrder();
+  }
+
+  public void testSubstitutePlaceholderIntoTemplate() throws Exception {
+    RuleClass ruleClass = new RuleClass("ruleA", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attr("a", STRING_LIST).mandatory().build(),
+        attr("b", STRING_LIST).mandatory().build(),
+        attr("c", STRING_LIST).mandatory().build(),
+        attr("baz", STRING_LIST).mandatory().build(),
+        attr("empty", STRING_LIST).build());
+
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    attributeValues.put("a", ImmutableList.of("a", "A"));
+    attributeValues.put("b", ImmutableList.of("b", "B"));
+    attributeValues.put("c", ImmutableList.of("c", "C"));
+    attributeValues.put("baz", ImmutableList.of("baz", "BAZ"));
+    attributeValues.put("empty", ImmutableList.<String>of());
+
+    AttributeMap rule = RawAttributeMapper.of(
+        createRule(ruleClass, "testrule", attributeValues, testRuleLocation));
+
+    assertThat(substitutePlaceholderIntoTemplate("foo", rule)).containsExactly("foo");
+    assertThat(substitutePlaceholderIntoTemplate("foo-%{baz}-bar", rule)).containsExactly(
+        "foo-baz-bar", "foo-BAZ-bar").inOrder();
+    assertThat(substitutePlaceholderIntoTemplate("%{a}-%{b}-%{c}", rule)).containsExactly("a-b-c",
+        "a-b-C", "a-B-c", "a-B-C", "A-b-c", "A-b-C", "A-B-c", "A-B-C").inOrder();
+    assertThat(substitutePlaceholderIntoTemplate("%{a", rule)).containsExactly("%{a");
+    assertThat(substitutePlaceholderIntoTemplate("%{a}}", rule)).containsExactly("a}", "A}")
+        .inOrder();
+    assertThat(substitutePlaceholderIntoTemplate("x%{a}y%{empty}", rule)).isEmpty();
+  }
+
+  public void testOrderIndependentAttribute() throws Exception {
+    RuleClass ruleClassA = createRuleClassA();
+
+    List<String> list = Arrays.asList("foo", "bar", "baz");
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    // mandatory values
+    attributeValues.put("my-string-attr", "");
+    attributeValues.put("my-label-attr", "//project");
+    attributeValues.put("my-string-attr2", "");
+    attributeValues.put("my-labellist-attr", Collections.emptyList());
+    // to compare the effect of .orderIndependent()
+    attributeValues.put("my-stringlist-attr", list);
+    attributeValues.put("my-sorted-stringlist-attr", list);
+
+    Rule rule = createRule(ruleClassA, "testrule", attributeValues, testRuleLocation);
+    AttributeMap attributes = RawAttributeMapper.of(rule);
+
+    assertEquals(list,
+                 attributes.get("my-stringlist-attr", Type.STRING_LIST));
+    assertEquals(Arrays.asList("bar", "baz", "foo"),
+                 attributes.get("my-sorted-stringlist-attr", Type.STRING_LIST));
+  }
+
+  public void testNonEmptyGood() throws Exception {
+    RuleClass mneRuleClass = setupNonEmpty(
+        attr("list1", LABEL_LIST).mandatory().legacyAllowAnyFileType().build(),
+        attr("list2", LABEL_LIST).nonEmpty().legacyAllowAnyFileType().build(),
+        attr("list3", STRING_LIST).nonEmpty().build());
+
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    attributeValues.put("list1", Lists.newArrayList());
+    attributeValues.put("list2", Lists.newArrayList(":nodup1", ":nodup2"));
+    attributeValues.put("list3", Lists.newArrayList("val1", "val2"));
+
+    createRule(mneRuleClass, "ruleTestMNE", attributeValues, testRuleLocation);
+  }
+
+  public void testNonEmptyFail() throws Exception {
+    RuleClass mandNonEmptyRuleClass = setupNonEmpty(
+        attr("list", LABEL_LIST).nonEmpty().legacyAllowAnyFileType().build());
+
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    attributeValues.put("list", Lists.newArrayList());
+
+    reporter.removeHandler(failFastHandler);
+    createRule(mandNonEmptyRuleClass, "ruleTestMNE", attributeValues, testRuleLocation);
+
+    assertSame(1, eventCollector.count());
+    assertContainsEvent(getErrorMsgNonEmptyList(
+        "list", "ruleMNE", "//testpackage:ruleTestMNE"));
+  }
+
+  private RuleClass setupNonEmpty(Attribute... attributes) {
+    RuleClass mandNonEmptyRuleClass = new RuleClass(
+        "ruleMNE", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attributes);
+    return mandNonEmptyRuleClass;
+  }
+
+  public void testNonEmptyWrongDefVal() throws Exception {
+    List<Label> emptyList = ImmutableList.of();
+    RuleClass mandNonEmptyRuleClass = new RuleClass(
+        "ruleMNE", false, false, false, false, false, false,
+        ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(), false, true,
+        attr("list", LABEL_LIST).nonEmpty().legacyAllowAnyFileType().value(emptyList).build());
+
+    Map<String, Object> attributeValues = new LinkedHashMap<>();
+    reporter.removeHandler(failFastHandler);
+    createRule(mandNonEmptyRuleClass, "ruleTestMNE", attributeValues, testRuleLocation);
+
+    assertSame(1, eventCollector.count());
+
+    assertContainsEvent(getErrorMsgNonEmptyList(
+        "list", "ruleMNE", "//testpackage:ruleTestMNE"));
+  }
+
+  private Rule createRule(RuleClass ruleClass, String name, Map<String, Object> attributeValues,
+      Location location) throws SyntaxException {
+    Package.Builder pkgBuilder = createDummyPackageBuilder();
+    Label ruleLabel;
+    try {
+      ruleLabel = pkgBuilder.createLabel(name);
+    } catch (Label.SyntaxException e) {
+      throw new IllegalArgumentException("Rule has illegal label");
+    }
+    return ruleClass.createRuleWithLabel(pkgBuilder, ruleLabel, attributeValues,
+         reporter, null, location);
+  }
+
+  public void testOverrideWithWrongType() {
+    try {
+      RuleClass parentRuleClass = createParentRuleClass();
+
+      RuleClass.Builder childRuleClassBuilder = new RuleClass.Builder(
+          "child_rule", RuleClassType.NORMAL, false, parentRuleClass);
+      childRuleClassBuilder.override(attr("attr", INTEGER));
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessage("The type of the new attribute 'int' is different from "
+          + "the original one 'string'.");
+    }
+  }
+
+  public void testOverrideWithRightType() {
+    RuleClass parentRuleClass = createParentRuleClass();
+
+    RuleClass.Builder childRuleClassBuilder = new RuleClass.Builder(
+      "child_rule", RuleClassType.NORMAL, false, parentRuleClass);
+      childRuleClassBuilder.override(attr("attr", STRING));
+  }
+
+  public void testCopyAndOverrideAttribute() throws Exception {
+    RuleClass parentRuleClass = createParentRuleClass();
+    RuleClass childRuleClass = createChildRuleClass(parentRuleClass);
+
+    Map<String, Object> parentValues = new LinkedHashMap<>();
+    Map<String, Object> childValues = new LinkedHashMap<>();
+    childValues.put("attr", "somevalue");
+    createRule(parentRuleClass, "parent_rule", parentValues, testRuleLocation);
+    createRule(childRuleClass, "child_rule", childValues, testRuleLocation);
+  }
+
+  public void testCopyAndOverrideAttributeMandatoryMissing() throws Exception {
+    RuleClass parentRuleClass = createParentRuleClass();
+    RuleClass childRuleClass = createChildRuleClass(parentRuleClass);
+
+    Map<String, Object> childValues = new LinkedHashMap<>();
+    reporter.removeHandler(failFastHandler);
+    createRule(childRuleClass, "child_rule", childValues, testRuleLocation);
+
+    assertSame(1, eventCollector.count());
+    assertContainsEvent("//testpackage:child_rule: missing value for mandatory "
+        + "attribute 'attr' in 'child_rule' rule");
+  }
+
+  public void testRequiredFragmentInheritance() throws Exception {
+    RuleClass parentRuleClass = createParentRuleClass();
+    RuleClass childRuleClass = createChildRuleClass(parentRuleClass);
+    assertThat(parentRuleClass.getRequiredConfigurationFragments())
+        .containsExactly(DummyFragment.class);
+    assertThat(childRuleClass.getRequiredConfigurationFragments())
+        .containsExactly(DummyFragment.class);
+  }
+
+  private RuleClass createParentRuleClass() {
+    RuleClass parentRuleClass = new RuleClass("parent_rule", false, false, false, false, false,
+        false, ImplicitOutputsFunction.NONE, RuleClass.NO_CHANGE, DUMMY_CONFIGURED_TARGET_FACTORY,
+        PredicatesWithMessage.<Rule>alwaysTrue(), PREFERRED_DEPENDENCY_PREDICATE,
+        ImmutableSet.<Class<?>>of(), null, null, ImmutableSet.<Class<?>>of(DummyFragment.class),
+        false, true, attr("attr", STRING).build());
+    return parentRuleClass;
+  }
+
+  private RuleClass createChildRuleClass(RuleClass parentRuleClass) {
+    RuleClass.Builder childRuleClassBuilder = new RuleClass.Builder(
+        "child_rule", RuleClassType.NORMAL, false, parentRuleClass);
+    return childRuleClassBuilder.override(
+        childRuleClassBuilder
+          .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+          .copy("attr").mandatory())
+          .add(attr("tags", STRING_LIST))
+          .build();
+  }
+
+  public void testValidityChecker() throws Exception {
+    RuleClass depClass = new RuleClass.Builder("dep", RuleClassType.NORMAL, false)
+        .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+        .add(attr("tags", STRING_LIST))
+        .build();
+    final Rule dep1 = createRule(depClass, "dep1", Collections.<String, Object>emptyMap(),
+        testRuleLocation);
+    final Rule dep2 = createRule(depClass, "dep2", Collections.<String, Object>emptyMap(),
+        testRuleLocation);
+
+    ValidityPredicate checker = new ValidityPredicate() {
+      @Override
+      public String checkValid(Rule from, Rule to) {
+        assertEquals("top", from.getName());
+        if (to.getName().equals("dep1")) {
+          return "pear";
+        } else if (to.getName().equals("dep2")) {
+          return null;
+        } else {
+          fail("invalid dependency");
+          return null;
+        }
+      }
+    };
+
+    RuleClass topClass = new RuleClass.Builder("top", RuleClassType.NORMAL, false)
+        .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+        .add(attr("tags", STRING_LIST))
+        .add(attr("deps", LABEL_LIST).legacyAllowAnyFileType()
+              .validityPredicate(checker))
+        .build();
+
+    Rule topRule = createRule(topClass, "top", Collections.<String, Object>emptyMap(),
+        testRuleLocation);
+
+    assertEquals("pear", topClass.getAttributeByName("deps").getValidityPredicate().checkValid(
+        topRule, dep1));
+    assertEquals(null, topClass.getAttributeByName("deps").getValidityPredicate().checkValid(
+        topRule, dep2));
+  }
+
+  /**
+   * Tests structure for making certain rules "preferential choices" for certain files
+   * under --compile_one_dependency.
+   */
+  public void testPreferredDependencyChecker() throws Exception {
+    final String cppFile = "file.cc";
+    final String textFile = "file.txt";
+
+    // Default: not preferred for anything.
+    RuleClass defaultClass = new RuleClass.Builder("defaultClass", RuleClassType.NORMAL, false)
+        .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+        .add(attr("tags", STRING_LIST))
+        .build();
+    final Rule defaultRule = createRule(defaultClass, "defaultRule",
+        Collections.<String, Object>emptyMap(), testRuleLocation);
+    assertFalse(defaultRule.getRuleClassObject().isPreferredDependency(cppFile));
+    assertFalse(defaultRule.getRuleClassObject().isPreferredDependency(textFile));
+
+    // Make a rule that's preferred for C++ sources.
+    RuleClass cppClass = new RuleClass.Builder("cppClass", RuleClassType.NORMAL, false)
+        .factory(DUMMY_CONFIGURED_TARGET_FACTORY)
+        .add(attr("tags", STRING_LIST))
+        .setPreferredDependencyPredicate(new Predicate<String>() {
+          @Override
+          public boolean apply(String filename) {
+            return filename.endsWith(".cc");
+          }
+        })
+        .build();
+    final Rule cppRule = createRule(cppClass, "cppRule",
+        Collections.<String, Object>emptyMap(), testRuleLocation);
+    assertTrue(cppRule.getRuleClassObject().isPreferredDependency(cppFile));
+    assertFalse(cppRule.getRuleClassObject().isPreferredDependency(textFile));
+  }
+
+  public void testBadRuleClassNames() {
+    expectError(RuleClassType.NORMAL, "8abc");
+    expectError(RuleClassType.NORMAL, "_abc");
+    expectError(RuleClassType.NORMAL, "a b");
+  }
+
+  private void expectError(RuleClassType type, String name) {
+    try {
+      type.checkName(name);
+      fail();
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+}
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
new file mode 100644
index 0000000..af3d458
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/TestSizeTest.java
@@ -0,0 +1,70 @@
+// Copyright 2012-2015 Google Inc. 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.assertNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests the various methods of {@link TestSize}
+ */
+@RunWith(JUnit4.class)
+public class TestSizeTest {
+
+  @Test
+  public void testBasicConversion() {
+    assertEquals(TestSize.valueOf("SMALL"), TestSize.SMALL);
+    assertEquals(TestSize.valueOf("MEDIUM"), TestSize.MEDIUM);
+    assertEquals(TestSize.valueOf("LARGE"), TestSize.LARGE);
+    assertEquals(TestSize.valueOf("ENORMOUS"), TestSize.ENORMOUS);
+  }
+
+  @Test
+  public void testGetDefaultTimeout() {
+    assertEquals(TestTimeout.SHORT, TestSize.SMALL.getDefaultTimeout());
+    assertEquals(TestTimeout.MODERATE, TestSize.MEDIUM.getDefaultTimeout());
+    assertEquals(TestTimeout.LONG, TestSize.LARGE.getDefaultTimeout());
+    assertEquals(TestTimeout.ETERNAL, TestSize.ENORMOUS.getDefaultTimeout());
+  }
+
+  @Test
+  public void testGetDefaultShards() {
+    assertEquals(2, TestSize.SMALL.getDefaultShards());
+    assertEquals(10, TestSize.MEDIUM.getDefaultShards());
+    assertEquals(20, TestSize.LARGE.getDefaultShards());
+    assertEquals(30, TestSize.ENORMOUS.getDefaultShards());
+  }
+
+  @Test
+  public void testGetTestSizeFromString() {
+    assertNull(TestSize.getTestSize("Small"));
+    assertNull(TestSize.getTestSize("Koala"));
+    assertEquals(TestSize.SMALL, TestSize.getTestSize("small"));
+    assertEquals(TestSize.MEDIUM, TestSize.getTestSize("medium"));
+    assertEquals(TestSize.LARGE, TestSize.getTestSize("large"));
+    assertEquals(TestSize.ENORMOUS, TestSize.getTestSize("enormous"));
+  }
+
+  @Test
+  public void testGetTestSizeFromDefaultTimeout() {
+    assertEquals(TestSize.SMALL, TestSize.getTestSize(TestTimeout.SHORT));
+    assertEquals(TestSize.MEDIUM, TestSize.getTestSize(TestTimeout.MODERATE));
+    assertEquals(TestSize.LARGE, TestSize.getTestSize(TestTimeout.LONG));
+    assertEquals(TestSize.ENORMOUS, TestSize.getTestSize(TestTimeout.ETERNAL));
+  }
+}
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
new file mode 100644
index 0000000..d83c1e5
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/TestTimeoutTest.java
@@ -0,0 +1,93 @@
+// Copyright 2009-2015 Google Inc. 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.packages.TestTimeout.ETERNAL;
+import static com.google.devtools.build.lib.packages.TestTimeout.LONG;
+import static com.google.devtools.build.lib.packages.TestTimeout.MODERATE;
+import static com.google.devtools.build.lib.packages.TestTimeout.SHORT;
+import static com.google.devtools.build.lib.packages.TestTimeout.getSuggestedTestTimeout;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests the various methods of {@link TestTimeout}
+ */
+@RunWith(JUnit4.class)
+public class TestTimeoutTest {
+
+  @Test
+  public void testBasicConversion() throws Exception {
+    assertSame(SHORT, TestTimeout.valueOf("SHORT"));
+    assertSame(MODERATE, TestTimeout.valueOf("MODERATE"));
+    assertSame(LONG, TestTimeout.valueOf("LONG"));
+    assertSame(ETERNAL, TestTimeout.valueOf("ETERNAL"));
+  }
+
+  @Test
+  public void testSuggestedTestSize() throws Exception {
+    assertEquals(SHORT, getSuggestedTestTimeout(0));
+    assertEquals(SHORT, getSuggestedTestTimeout(2));
+    assertEquals(SHORT, getSuggestedTestTimeout(6));
+    assertEquals(SHORT, getSuggestedTestTimeout(59));
+    assertEquals(MODERATE, getSuggestedTestTimeout(60));
+    assertEquals(MODERATE, getSuggestedTestTimeout(299));
+    assertEquals(LONG, getSuggestedTestTimeout(300));
+    assertEquals(LONG, getSuggestedTestTimeout(899));
+    assertEquals(ETERNAL, getSuggestedTestTimeout(900));
+    assertEquals(ETERNAL, getSuggestedTestTimeout(1234567890));
+  }
+
+  @Test
+  public void testIsInRangeExact() throws Exception {
+    assertTrue(SHORT.isInRangeExact(0));
+    assertTrue(SHORT.isInRangeExact(1));
+    assertFalse(SHORT.isInRangeExact(60));
+    assertTrue(MODERATE.isInRangeExact(60));
+    assertTrue(MODERATE.isInRangeExact(299));
+    assertFalse(MODERATE.isInRangeExact(300));
+    assertTrue(LONG.isInRangeExact(300));
+    assertTrue(LONG.isInRangeExact(899));
+    assertFalse(LONG.isInRangeExact(900));
+    assertTrue(ETERNAL.isInRangeExact(900));
+    assertFalse(ETERNAL.isInRangeExact(1234567890));
+  }
+
+  @Test
+  public void testIsInRangeFuzzy() throws Exception {
+    assertFuzzyRange(SHORT, 0, 105);
+    assertFuzzyRange(MODERATE, 8, 525);
+    assertFuzzyRange(LONG, 75, 1575);
+    assertFuzzyRange(ETERNAL, 225, Integer.MAX_VALUE);
+  }
+
+  private void assertFuzzyRange(TestTimeout timeout, int min, int max) {
+    if (min > 0) {
+      assertFalse(timeout.isInRangeFuzzy(min - 1));
+    }
+    assertTrue(timeout.isInRangeFuzzy(min));
+    assertTrue(timeout.isInRangeFuzzy(min + 1));
+    assertTrue(timeout.isInRangeFuzzy(max - 1));
+    assertTrue(timeout.isInRangeFuzzy(max));
+    if (max < Integer.MAX_VALUE) {
+      assertFalse(timeout.isInRangeFuzzy(max + 1));
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java b/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java
new file mode 100644
index 0000000..39c5dbe
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java
@@ -0,0 +1,744 @@
+// Copyright 2006-2015 Google Inc. 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.testutil.MoreAsserts.assertSameContents;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SelectorList;
+import com.google.devtools.build.lib.syntax.SelectorValue;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test of type-conversions using Type.
+ */
+@RunWith(JUnit4.class)
+public class TypeTest {
+
+  private Label currentRule;
+
+  @Before
+  public void setUp() throws Exception {
+    this.currentRule = Label.parseAbsolute("//quux:baz");
+  }
+
+  @Test
+  public void testInteger() throws Exception {
+    Object x = 3;
+    assertEquals(x, Type.INTEGER.convert(x, null));
+    assertThat(Type.INTEGER.getLabels(x)).isEmpty();
+  }
+
+  @Test
+  public void testNonInteger() throws Exception {
+    try {
+      Type.INTEGER.convert("foo", null);
+      fail();
+    } catch (Type.ConversionException e) {
+      // This does not use assertMessageContainsWordsWithQuotes because at least
+      // one test should test exact wording (but they all shouldn't to make
+      // changing/improving the messages easy).
+      assertThat(e).hasMessage("expected value of type 'int', but got 'foo' (string)");
+    }
+  }
+
+  // Ensure that types are reported correctly.
+  @Test
+  public void testTypeErrorMessage() throws Exception {
+    try {
+      Type.STRING_LIST.convert("[(1,2), 3, 4]", "myexpr", null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'list(string)' for myexpr, "
+          + "but got '[(1,2), 3, 4]' (string)");
+    }
+  }
+
+  @Test
+  public void testString() throws Exception {
+    Object s = "foo";
+    assertEquals(s, Type.STRING.convert(s, null));
+    assertThat(Type.STRING.getLabels(s)).isEmpty();
+  }
+
+  @Test
+  public void testNonString() throws Exception {
+    try {
+      Type.STRING.convert(3, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "3", "string");
+    }
+  }
+
+  @Test
+  public void testBoolean() throws Exception {
+    Object myTrue = true;
+    Object myFalse = false;
+    assertEquals(Boolean.TRUE, Type.BOOLEAN.convert(1, null));
+    assertEquals(Boolean.FALSE, Type.BOOLEAN.convert(0, null));
+    assertTrue(Type.BOOLEAN.convert(true, null));
+    assertTrue(Type.BOOLEAN.convert(myTrue, null));
+    assertFalse(Type.BOOLEAN.convert(false, null));
+    assertFalse(Type.BOOLEAN.convert(myFalse, null));
+    assertThat(Type.BOOLEAN.getLabels(myTrue)).isEmpty();
+  }
+
+  @Test
+  public void testNonBoolean() throws Exception {
+    try {
+      Type.BOOLEAN.convert("unexpected", null);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "unexpected");
+    }
+    // Integers other than [0, 1] should fail.
+    try {
+      Type.BOOLEAN.convert(2, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertEquals(e.getMessage(), "boolean is not one of [0, 1]");
+    }
+    try {
+      Type.BOOLEAN.convert(-1, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertEquals(e.getMessage(), "boolean is not one of [0, 1]");
+    }
+  }
+
+  @Test
+  public void testTriState() throws Exception {
+    assertEquals(TriState.YES, Type.TRISTATE.convert(1, null));
+    assertEquals(TriState.NO, Type.TRISTATE.convert(0, null));
+    assertEquals(TriState.AUTO, Type.TRISTATE.convert(-1, null));
+    assertEquals(TriState.YES, Type.TRISTATE.convert(true, null));
+    assertEquals(TriState.NO, Type.TRISTATE.convert(false, null));
+    assertEquals(TriState.YES, Type.TRISTATE.convert(TriState.YES, null));
+    assertEquals(TriState.NO, Type.TRISTATE.convert(TriState.NO, null));
+    assertEquals(TriState.AUTO, Type.TRISTATE.convert(TriState.AUTO, null));
+    assertThat(Type.TRISTATE.getLabels(TriState.YES)).isEmpty();
+  }
+
+  @Test
+  public void testTriStateDoesNotAcceptArbitraryIntegers() throws Exception {
+    List<Integer> listOfCases = Lists.newArrayList(2, 3, -5, -2, 20);
+    for (Object entry : listOfCases) {
+      try {
+        Type.TRISTATE.convert(entry, null);
+        fail();
+      } catch (Type.ConversionException e) {
+        // Expected.
+      }
+    }
+  }
+
+  @Test
+  public void testTriStateDoesNotAcceptStrings() throws Exception {
+    List<String> listOfCases = Lists.newArrayList("bad", "true", "auto", "false");
+    for (Object entry : listOfCases) {
+      try {
+        Type.TRISTATE.convert(entry, null);
+        fail();
+      } catch (Type.ConversionException e) {
+        // Expected.
+      }
+    }
+  }
+
+  @Test
+  public void testTagConversion() throws Exception {
+    assertSameContents(Sets.newHashSet("attribute"),
+        Type.BOOLEAN.toTagSet(true, "attribute"));
+    assertSameContents(Sets.newHashSet("noattribute"),
+        Type.BOOLEAN.toTagSet(false, "attribute"));
+
+    assertSameContents(Sets.newHashSet("whiskey"),
+        Type.STRING.toTagSet("whiskey", "preferred_cocktail"));
+
+    assertSameContents(Sets.newHashSet("cheddar", "ementaler", "gruyere"),
+        Type.STRING_LIST.toTagSet(
+            Lists.newArrayList("cheddar", "ementaler", "gruyere"), "cheeses"));
+  }
+
+  @Test
+  public void testIllegalTagConversionByType() throws Exception {
+    try {
+      Type.TRISTATE.toTagSet(TriState.AUTO, "some_tristate");
+      fail("Expect UnsuportedOperationException");
+    } catch (UnsupportedOperationException e) {
+      // Success.
+    }
+    try {
+      Type.LICENSE.toTagSet(License.NO_LICENSE, "output_license");
+      fail("Expect UnsuportedOperationException");
+    } catch (UnsupportedOperationException e) {
+      // Success.
+    }
+  }
+
+  @Test
+  public void testIllegalTagConversIonFromNullOnSupportedType() throws Exception {
+    try {
+      Type.BOOLEAN.toTagSet(null, "a_boolean");
+      fail("Expect UnsuportedOperationException");
+    } catch (IllegalStateException e) {
+      // Success.
+    }
+  }
+
+  @Test
+  public void testLabel() throws Exception {
+    Label label = Label.parseAbsolute("//foo:bar");
+    assertEquals(label, Type.LABEL.convert("//foo:bar", null, currentRule));
+    assertThat(Type.LABEL.getLabels(label)).containsExactly(label);
+  }
+
+  @Test
+  public void testNodepLabel() throws Exception {
+    Label label = Label.parseAbsolute("//foo:bar");
+    assertEquals(label, Type.NODEP_LABEL.convert("//foo:bar", null, currentRule));
+    assertThat(Type.NODEP_LABEL.getLabels(label)).containsExactly(label);
+  }
+
+  @Test
+  public void testRelativeLabel() throws Exception {
+    assertEquals(Label.parseAbsolute("//quux:wiz"),
+        Type.LABEL.convert(":wiz", null, currentRule));
+    assertEquals(Label.parseAbsolute("//quux:wiz"),
+        Type.LABEL.convert("wiz", null, currentRule));
+    try {
+      Type.LABEL.convert("wiz", null);
+      fail();
+    } catch (NullPointerException e) {
+      /* ok */
+    }
+  }
+
+  @Test
+  public void testInvalidLabel() throws Exception {
+    try {
+      Type.LABEL.convert("not a label", null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "not a label");
+    }
+  }
+
+  @Test
+  public void testNonLabel() throws Exception {
+    try {
+      Type.LABEL.convert(3, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "3", "string");
+    }
+  }
+
+  @Test
+  public void testStringList() throws Exception {
+    Object input = Arrays.asList("foo", "bar", "wiz");
+    List<String> converted =
+        Type.STRING_LIST.convert(input, null);
+    assertEquals(input, converted);
+    assertNotSame(input, converted);
+    assertThat(Type.STRING_LIST.getLabels(input)).isEmpty();
+  }
+
+  @Test
+  public void testStringDict() throws Exception {
+    Object input = ImmutableMap.of("foo", "bar",
+                                   "wiz", "bang");
+    Map<String, String> converted = Type.STRING_DICT.convert(input, null);
+    assertEquals(input, converted);
+    assertNotSame(input, converted);
+    assertThat(Type.STRING_DICT.getLabels(converted)).isEmpty();
+  }
+
+  @Test
+  public void testStringDictBadElements() throws Exception {
+    Object input = ImmutableMap.of("foo", Arrays.asList("bar", "baz"),
+        "wiz", "bang");
+    try {
+      Type.STRING_DICT.convert(input, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'string' for dict value element, but got "
+          + "'[\"bar\", \"baz\"]' (List)");
+    }
+  }
+
+  @Test
+  public void testNonStringList() throws Exception {
+    try {
+      Type.STRING_LIST.convert(3, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "3", "list(string)");
+    }
+  }
+
+  @Test
+  public void testStringListBadElements() throws Exception {
+    Object input = Arrays.<Object>asList("foo", "bar", 1);
+    try {
+      Type.STRING_LIST.convert(input, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "1", "string");
+    }
+  }
+
+  @Test
+  public void testLabelList() throws Exception {
+    Object input = Arrays.asList("//foo:bar", ":wiz");
+    List<Label> converted =
+      Type.LABEL_LIST.convert(input , null, currentRule);
+    List<Label> expected =
+      Arrays.asList(Label.parseAbsolute("//foo:bar"),
+                    Label.parseAbsolute("//quux:wiz"));
+    assertEquals(expected, converted);
+    assertNotSame(expected, converted);
+    assertThat(Type.LABEL_LIST.getLabels(converted)).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testNonLabelList() throws Exception {
+    try {
+      Type.LABEL_LIST.convert(3, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "3", "list(label)");
+    }
+  }
+
+  @Test
+  public void testLabelListBadElements() throws Exception {
+    Object list = Arrays.<Object>asList("//foo:bar", 2, "foo");
+    try {
+      Type.LABEL_LIST.convert(list, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "2", "string");
+    }
+  }
+
+  @Test
+  public void testLabelListSyntaxError() throws Exception {
+    Object list = Arrays.<Object>asList("//foo:bar/..", "foo");
+    try {
+      Type.LABEL_LIST.convert(list, "myexpr", currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("invalid label '//foo:bar/..' in element 0 of myexpr: "
+          + "invalid target name 'bar/..': "
+          + "target names may not contain up-level references '..'");
+    }
+  }
+
+  @Test
+  public void testLabelListDict() throws Exception {
+    Object input = ImmutableMap.of("foo", Arrays.asList("//foo:bar"),
+        "wiz", Arrays.asList(":bang"));
+    Map<String, List<Label>> converted = Type.LABEL_LIST_DICT.convert(input, null, currentRule);
+    Label fooLabel = Label.parseAbsolute("//foo:bar");
+    Label bangLabel = Label.parseAbsolute("//quux:bang");
+    Map<?, ?> expected = ImmutableMap.<String, List<Label>>of(
+            "foo", Arrays.<Label>asList(fooLabel),
+            "wiz", Arrays.<Label>asList(bangLabel));
+    assertEquals(expected, converted);
+    assertNotSame(expected, converted);
+    assertThat(Type.LABEL_LIST_DICT.getLabels(converted)).containsExactly(fooLabel, bangLabel);
+  }
+
+  @Test
+  public void testLabelListDictBadFirstElement() throws Exception {
+    Object input = ImmutableMap.of(2, Arrays.asList("//foo:bar"),
+        "wiz", Arrays.asList(":bang"));
+    try {
+      Type.LABEL_LIST_DICT.convert(input, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'string' for dict key element,"
+          + " but got '2' (int)");
+    }
+  }
+
+  @Test
+  public void testLabelListDictBadSecondElement() throws Exception {
+    Object input = ImmutableMap.of("foo", "//foo:bar",
+                                   "wiz", Arrays.asList(":bang"));
+    try {
+      Type.LABEL_LIST_DICT.convert(input, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "//foo:bar", "list(label)");
+    }
+  }
+
+  @Test
+  public void testLabelListDictBadElements1() throws Exception {
+    Object input = ImmutableMap.of("foo", "bar",
+                                   "bar", Arrays.asList("//foo:bar"),
+                                   "wiz", Arrays.asList(":bang"));
+    try {
+      Type.LABEL_LIST_DICT.convert(input, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'list(label)' for dict value element, "
+          + "but got 'bar' (string)");
+    }
+  }
+
+  @Test
+  public void testLabelListDictSyntaxError() throws Exception {
+    Object input = ImmutableMap.of("foo", Arrays.asList("//foo:.."),
+                                   "wiz", Arrays.asList(":bang"));
+    try {
+      Type.LABEL_LIST_DICT.convert(input, "baz", currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("invalid label '//foo:..' in element 0 of dict value element: "
+          + "invalid target name '..': "
+          + "target names may not contain up-level references '..'");
+    }
+  }
+
+  @Test
+  public void testStringListDict() throws Exception {
+    Object input = ImmutableMap.of("foo", Arrays.asList("foo", "bar"),
+                                   "wiz", Arrays.asList("bang"));
+    Map<String, List<String>> converted =
+        Type.STRING_LIST_DICT.convert(input, null, currentRule);
+    Map<?, ?> expected = ImmutableMap.<String, List<String>>of(
+            "foo", Arrays.asList("foo", "bar"),
+            "wiz", Arrays.asList("bang"));
+    assertEquals(expected, converted);
+    assertNotSame(expected, converted);
+    assertThat(Type.STRING_LIST_DICT.getLabels(converted)).isEmpty();
+  }
+
+  @Test
+  public void testStringListDictBadFirstElement() throws Exception {
+    Object input = ImmutableMap.of(2, Arrays.asList("foo", "bar"),
+                                   "wiz", Arrays.asList("bang"));
+    try {
+      Type.STRING_LIST_DICT.convert(input, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage(
+          "expected value of type 'string' for dict key element, but got '2' (int)");
+    }
+  }
+
+  @Test
+  public void testStringListDictBadSecondElement() throws Exception {
+    Object input = ImmutableMap.of("foo", "bar",
+                                   "wiz", Arrays.asList("bang"));
+    try {
+      Type.STRING_LIST_DICT.convert(input, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      MoreAsserts.assertContainsWordsWithQuotes(e.getMessage(), "bar", "list(string)");
+    }
+  }
+
+  @Test
+  public void testStringListDictBadElements1() throws Exception {
+    Object input = ImmutableMap.of(Arrays.asList("foo"), Arrays.asList("bang"),
+                                   "wiz", Arrays.asList("bang"));
+    try {
+      Type.STRING_LIST_DICT.convert(input, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'string' for dict key element, but got "
+          + "'[\"foo\"]' (List)");
+    }
+  }
+
+  @Test
+  public void testStringDictUnary() throws Exception {
+    Object input = ImmutableMap.of("foo", "bar",
+                                   "wiz", "bang");
+    Map<?, ?> converted =
+        Type.STRING_DICT_UNARY.convert(input, null, currentRule);
+    Map<?, ?> expected = ImmutableMap.<String, String>of(
+            "foo", "bar",
+            "wiz", "bang");
+    assertEquals(expected, converted);
+    assertNotSame(expected, converted);
+    assertThat(Type.STRING_DICT_UNARY.getLabels(converted)).isEmpty();
+  }
+
+  @Test
+  public void testStringDictUnaryBadFirstElement() throws Exception {
+    Object input = ImmutableMap.of(2, Arrays.asList("foo", "bar"),
+                                   "wiz", Arrays.asList("bang"));
+    try {
+      Type.STRING_DICT_UNARY.convert(input, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'string' for dict key element, but got "
+          + "'2' (int)");
+    }
+  }
+
+  @Test
+  public void testStringDictUnaryBadSecondElement() throws Exception {
+    Object input = ImmutableMap.of("foo", "bar",
+                                   "wiz", Arrays.asList("bang"));
+    try {
+      Type.STRING_DICT_UNARY.convert(input, null, currentRule);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'string' for dict value element, but got "
+          + "'[\"bang\"]' (List)");
+    }
+  }
+
+  @Test
+  public void testStringDictUnaryBadElements1() throws Exception {
+    Object input = ImmutableMap.of("foo", "bar",
+                                   Arrays.asList("foo", "bar"),
+                                   Arrays.<Object>asList("wiz", "bang"));
+    try {
+      Type.STRING_DICT_UNARY.convert(input, null);
+      fail();
+    } catch (Type.ConversionException e) {
+      assertThat(e).hasMessage("expected value of type 'string' for dict key element, but got "
+          + "'[\"foo\", \"bar\"]' (List)");
+    }
+  }
+
+  @Test
+  public void testStringDictThrowsConversionException() throws Exception {
+    try {
+      Type.STRING_DICT.convert("some string", null);
+      fail();
+    } catch (ConversionException e) {
+      assertThat(e).hasMessage("Expected a map for dictionary but got a java.lang.String");
+    }
+  }
+
+  @Test
+  public void testFilesetEntry() throws Exception {
+    Label srcDir = Label.create("foo", "src");
+    Label entryLabel = Label.create("foo", "entry");
+    FilesetEntry input =
+        new FilesetEntry(srcDir, ImmutableList.of(entryLabel), null, null, null, null);
+    assertEquals(input, Type.FILESET_ENTRY.convert(input, null, currentRule));
+    assertThat(Type.FILESET_ENTRY.getLabels(input)).containsExactly(entryLabel);
+  }
+
+  @Test
+  public void testFilesetEntryList() throws Exception {
+    Label srcDir = Label.create("foo", "src");
+    Label entry1Label = Label.create("foo", "entry1");
+    Label entry2Label = Label.create("foo", "entry");
+    List<FilesetEntry> input = ImmutableList.of(
+        new FilesetEntry(srcDir, ImmutableList.of(entry1Label), null, null, null, null),
+        new FilesetEntry(srcDir, ImmutableList.of(entry2Label), null, null, null, null));
+    assertEquals(input, Type.FILESET_ENTRY_LIST.convert(input, null, currentRule));
+    assertThat(Type.FILESET_ENTRY_LIST.getLabels(input)).containsExactly(entry1Label, entry2Label);
+  }
+
+  /**
+   * Tests basic {@link Type.Selector} functionality.
+   */
+  @Test
+  public void testSelector() throws Exception {
+    Object input = ImmutableMap.of(
+        "//conditions:a", "//a:a",
+        "//conditions:b", "//b:b",
+        Type.Selector.DEFAULT_CONDITION_KEY, "//d:d");
+    Type.Selector<Label> selector = new Type.Selector<>(input, null, currentRule, Type.LABEL);
+    assertEquals(Type.LABEL, selector.getOriginalType());
+
+    Map<Label, Label> expectedMap = ImmutableMap.of(
+        Label.parseAbsolute("//conditions:a"), Label.create("a", "a"),
+        Label.parseAbsolute("//conditions:b"), Label.create("b", "b"),
+        Label.parseAbsolute(Type.Selector.DEFAULT_CONDITION_KEY), Label.create("d", "d"));
+    assertSameContents(expectedMap.entrySet(), selector.getEntries().entrySet());
+  }
+
+  /**
+   * Tests that creating a {@link Type.Selector} over a mismatching native type triggers an
+   * exception.
+   */
+  @Test
+  public void testSelectorWrongType() throws Exception {
+    Object input = ImmutableMap.of(
+        "//conditions:a", "not a label",
+        "//conditions:b", "also not a label",
+        Type.Selector.DEFAULT_CONDITION_KEY, "whatever");
+    try {
+      new Type.Selector<Label>(input, null, currentRule, Type.LABEL);
+      fail("Expected Selector instantiation to fail since the input isn't a selection of labels");
+    } catch (ConversionException e) {
+      assertThat(e.getMessage()).contains("invalid label 'not a label'");
+    }
+  }
+
+  /**
+   * Tests that non-label selector keys trigger an exception.
+   */
+  @Test
+  public void testSelectorKeyIsNotALabel() throws Exception {
+    Object input = ImmutableMap.of(
+        "not a label", "//a:a",
+        Type.Selector.DEFAULT_CONDITION_KEY, "whatever");
+    try {
+      new Type.Selector<Label>(input, null, currentRule, Type.LABEL);
+      fail("Expected Selector instantiation to fail since the key isn't a label");
+    } catch (ConversionException e) {
+      assertThat(e.getMessage()).contains("invalid label 'not a label'");
+    }
+  }
+
+  /**
+   * Tests that {@link Type.Selector} correctly references its default value.
+   */
+  @Test
+  public void testSelectorDefault() throws Exception {
+    Object input = ImmutableMap.of(
+        "//conditions:a", "//a:a",
+        "//conditions:b", "//b:b",
+        Type.Selector.DEFAULT_CONDITION_KEY, "//d:d");
+    assertEquals(
+        Label.create("d", "d"),
+        new Type.Selector<Label>(input, null, currentRule, Type.LABEL).getDefault());
+  }
+
+  @Test
+  public void testSelectorList() throws Exception {
+    Object selector1 = new SelectorValue(ImmutableMap.of("//conditions:a",
+        ImmutableList.of("//a:a"), "//conditions:b", ImmutableList.of("//b:b")));
+    Object selector2 = new SelectorValue(ImmutableMap.of("//conditions:c",
+        ImmutableList.of("//c:c"), "//conditions:d", ImmutableList.of("//d:d")));
+    Type.SelectorList<List<Label>> selectorList = new Type.SelectorList<>(
+        ImmutableList.of(selector1, selector2), null, currentRule, Type.LABEL_LIST);
+
+    assertEquals(Type.LABEL_LIST, selectorList.getOriginalType());
+    assertSameContents(
+        ImmutableSet.of(
+            Label.parseAbsolute("//conditions:a"), Label.parseAbsolute("//conditions:b"),
+            Label.parseAbsolute("//conditions:c"), Label.parseAbsolute("//conditions:d")),
+        selectorList.getKeyLabels());
+
+    List<Type.Selector<List<Label>>> selectors = selectorList.getSelectors();
+    assertSameContents(
+        ImmutableMap.of(
+                Label.parseAbsolute("//conditions:a"), ImmutableList.of(Label.create("a", "a")),
+                Label.parseAbsolute("//conditions:b"), ImmutableList.of(Label.create("b", "b")))
+            .entrySet(),
+        selectors.get(0).getEntries().entrySet());
+    assertSameContents(
+        ImmutableMap.of(
+                Label.parseAbsolute("//conditions:c"), ImmutableList.of(Label.create("c", "c")),
+                Label.parseAbsolute("//conditions:d"), ImmutableList.of(Label.create("d", "d")))
+            .entrySet(),
+        selectors.get(1).getEntries().entrySet());
+  }
+
+  @Test
+  public void testSelectorListMixedTypes() throws Exception {
+    Object selector1 =
+        new SelectorValue(ImmutableMap.of("//conditions:a", ImmutableList.of("//a:a")));
+    Object selector2 =
+        new SelectorValue(ImmutableMap.of("//conditions:b", "//b:b"));
+    try {
+      new Type.SelectorList<>(ImmutableList.of(selector1, selector2), null, currentRule,
+          Type.LABEL_LIST);
+      fail("Expected SelectorList initialization to fail on mixed element types");
+    } catch (ConversionException e) {
+      assertThat(e.getMessage()).contains("expected value of type 'list(label)'");
+    }
+  }
+
+  /**
+   * Tests that {@link Type#selectableConvert} returns either the native type or a selector
+   * on that type, in accordance with the provided input.
+   */
+  @SuppressWarnings("unchecked")
+  @Test
+  public void testSelectableConvert() throws Exception {
+    Object nativeInput = Arrays.asList("//a:a1", "//a:a2");
+    Object selectableInput =
+        SelectorList.of(new SelectorValue(ImmutableMap.of(
+            "//conditions:a", nativeInput,
+            Type.Selector.DEFAULT_CONDITION_KEY, nativeInput)));
+    List<Label> expectedLabels = ImmutableList.of(Label.create("a", "a1"), Label.create("a", "a2"));
+
+    // Conversion to direct type:
+    Object converted = Type.LABEL_LIST.selectableConvert(nativeInput, null, currentRule);
+    assertTrue(converted instanceof List<?>);
+    assertSameContents(expectedLabels, (List<Label>) converted);
+
+    // Conversion to selectable type:
+    converted = Type.LABEL_LIST.selectableConvert(selectableInput, null, currentRule);
+    Type.SelectorList<?> selectorList = (Type.SelectorList<?>) converted;
+    assertSameContents(
+        ImmutableMap.of(
+            Label.parseAbsolute("//conditions:a"), expectedLabels,
+            Label.parseAbsolute(Type.Selector.DEFAULT_CONDITION_KEY), expectedLabels).entrySet(),
+        ((Type.Selector<Label>) selectorList.getSelectors().get(0)).getEntries().entrySet());
+  }
+
+  /**
+   * Tests that {@link Type#convert} fails on selector inputs.
+   */
+  @Test
+  public void testConvertDoesNotAcceptSelectables() throws Exception {
+    Object selectableInput = SelectorList.of(
+        new SelectorValue(ImmutableMap.of("//conditions:a", Arrays.asList("//a:a1", "//a:a2"))));
+    try {
+      Type.LABEL_LIST.convert(selectableInput, null, currentRule);
+      fail("Expected conversion to fail on a selectable input");
+    } catch (ConversionException e) {
+      assertThat(e.getMessage()).contains("expected value of type 'list(label)'");
+    }
+  }
+
+  /**
+   * Tests for "reserved" key labels (i.e. not intended to map to actual targets).
+   */
+  @Test
+  public void testReservedKeyLabels() throws Exception {
+    assertFalse(Type.Selector.isReservedLabel(Label.parseAbsolute("//condition:a")));
+    assertTrue(Type.Selector.isReservedLabel(
+        Label.parseAbsolute(Type.Selector.DEFAULT_CONDITION_KEY)));
+  }
+}
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
new file mode 100644
index 0000000..8d7ee5b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/PackageLoadingTestCase.java
@@ -0,0 +1,244 @@
+// Copyright 2006-2015 Google Inc. 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.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageFactory.EnvironmentExtension;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.skyframe.DiffAwareness;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.common.options.OptionsParser;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * This is a specialization of {@link FoundationTestCase} that's useful for
+ * implementing tests of the "packages" library.
+ */
+public abstract class PackageLoadingTestCase extends FoundationTestCase {
+
+  protected ConfiguredRuleClassProvider ruleClassProvider;
+  private SkyframeExecutor skyframeExecutor;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+
+    ruleClassProvider = TestRuleClassProvider.getRuleClassProvider();
+    skyframeExecutor = SequencedSkyframeExecutor.create(reporter,
+        new PackageFactory(ruleClassProvider, getEnvironmentExtensions()),
+        new TimestampGranularityMonitor(BlazeClock.instance()),
+        new BlazeDirectories(outputBase, outputBase, rootDirectory),
+        null, /* workspaceStatusActionFactory */
+        ruleClassProvider.getBuildInfoFactories(),
+        ImmutableSet.<Path>of(),
+        ImmutableList.<DiffAwareness.Factory>of(),
+        Predicates.<PathFragment>alwaysFalse(),
+        Preprocessor.Factory.Supplier.NullSupplier.INSTANCE,
+        ImmutableMap.<SkyFunctionName, SkyFunction>of(),
+        ImmutableList.<PrecomputedValue.Injected>of()
+    );
+    skyframeExecutor.preparePackageLoading(
+        new PathPackageLocator(rootDirectory), ConstantRuleVisibility.PUBLIC, true, "",
+        UUID.randomUUID());
+    setUpSkyframe(parsePackageCacheOptions());
+  }
+
+  protected Iterable<EnvironmentExtension> getEnvironmentExtensions() {
+    return ImmutableList.<EnvironmentExtension>of();
+  }
+
+  private void setUpSkyframe(PackageCacheOptions packageCacheOptions) {
+    PathPackageLocator pkgLocator = PathPackageLocator.create(
+        packageCacheOptions.packagePath, reporter, rootDirectory, rootDirectory);
+    skyframeExecutor.preparePackageLoading(pkgLocator,
+        packageCacheOptions.defaultVisibility, true,
+        ruleClassProvider.getDefaultsPackageContent(),
+        UUID.randomUUID());
+    skyframeExecutor.setDeletedPackages(ImmutableSet.copyOf(packageCacheOptions.deletedPackages));
+  }
+
+  private PackageCacheOptions parsePackageCacheOptions(String... options) throws Exception {
+    OptionsParser parser = OptionsParser.newOptionsParser(PackageCacheOptions.class);
+    parser.parse(new String[] { "--default_visibility=public" });
+    parser.parse(options);
+    return parser.getOptions(PackageCacheOptions.class);
+  }
+
+  protected void setPackageCacheOptions(String... options) throws Exception {
+    setUpSkyframe(parsePackageCacheOptions(options));
+  }
+
+  protected Target getTarget(String label)
+      throws NoSuchPackageException, NoSuchTargetException,
+             Label.SyntaxException, InterruptedException {
+    return getTarget(Label.parseAbsolute(label));
+  }
+
+  protected Target getTarget(Label label)
+      throws NoSuchPackageException, NoSuchTargetException, InterruptedException {
+    return getPackageManager().getTarget(reporter, label);
+  }
+
+  /**
+   * Create and return a scratch rule.
+   *
+   * @param packageName the package name of the rule.
+   * @param ruleName the name of the rule.
+   * @param lines the text of the rule.
+   * @return the rule instance for the created rule.
+   * @throws IOException
+   * @throws Exception
+   */
+  protected Rule scratchRule(String packageName, String ruleName, String... lines)
+      throws Exception {
+    scratch.file(packageName + "/BUILD", lines);
+    return (Rule) getTarget("//" + packageName + ":" + ruleName);
+  }
+
+  /**
+   * A Utility method that generates build file rules for tests.
+   * @param rule the name of the rule class.
+   * @param name the name of the rule instance.
+   * @param body an array of strings containing the contents of the rule.
+   * @return a string containing the build file rule.
+   */
+  protected String genRule(String rule, String name, String... body) {
+    StringBuilder buf = new StringBuilder();
+    buf.append(rule);
+    buf.append("(name='");
+    buf.append(name);
+    buf.append("',\n");
+    for (String line : body) {
+      buf.append(line);
+    }
+    buf.append(")\n");
+    return buf.toString();
+  }
+
+  /**
+   * A utility function which generates the "deps" clause for a build file
+   * rule from a list of targets.
+   * @param depTargets the list of targets.
+   * @return a string containing the deps clause
+   */
+  protected static String deps(String... depTargets) {
+    StringBuilder buf = new StringBuilder();
+    buf.append("    deps=[");
+    String sep = "'";
+    for (String dep : depTargets) {
+      buf.append(sep);
+      buf.append(dep);
+      buf.append("'");
+      sep = ", '";
+    }
+    buf.append("]");
+    return buf.toString();
+  }
+
+  /**
+   * Utility method for tests. Converts an array of strings into a set of labels.
+   *
+   * @param strings the set of strings to be converted to labels.
+   * @throws SyntaxException if there are any syntax errors in the strings.
+   */
+  public static Set<Label> asLabelSet(String... strings) throws SyntaxException {
+    return asLabelSet(ImmutableList.copyOf(strings));
+  }
+
+  /**
+   * Utility method for tests. Converts an array of strings into a set of labels.
+   *
+   * @param strings the set of strings to be converted to labels.
+   * @throws SyntaxException if there are any syntax errors in the strings.
+   */
+  public static Set<Label> asLabelSet(Iterable<String> strings) throws SyntaxException {
+    Set<Label> result = Sets.newTreeSet();
+    for (String s : strings) {
+      result.add(Label.parseAbsolute(s));
+    }
+    return result;
+  }
+
+  protected final Set<Target> asTargetSet(String... strLabels)
+      throws SyntaxException, NoSuchThingException, InterruptedException {
+    return asTargetSet(Arrays.asList(strLabels));
+  }
+
+  protected Set<Target> asTargetSet(Iterable<String> strLabels)
+      throws SyntaxException, NoSuchThingException, InterruptedException {
+    Set<Target> targets = new HashSet<>();
+    for (String strLabel : strLabels) {
+      targets.add(getTarget(strLabel));
+    }
+    return targets;
+  }
+
+  protected PackageManager getPackageManager() {
+    return skyframeExecutor.getPackageManager();
+  }
+
+  protected SkyframeExecutor getSkyframeExecutor() {
+    return skyframeExecutor;
+  }
+
+  /**
+   * Invalidates all existing packages below the usual rootDirectory. Must be called _after_ the
+   * files are modified.
+   *
+   * @throws InterruptedException
+   */
+  protected void invalidatePackages() throws InterruptedException {
+    skyframeExecutor.invalidateFilesUnderPathForTesting(ModifiedFileSet.EVERYTHING_MODIFIED,
+        rootDirectory);
+  }
+
+  protected String getErrorMsgNonEmptyList(String attrName, String ruleType, String ruleName) {
+    return "non empty attribute '" + attrName + "' in '" + ruleType
+        + "' rule '" + ruleName + "' has to have at least one value";
+  }
+}