Update from Google.

--
MOE_MIGRATED_REVID=85702957
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/BlazeTestSuiteBuilder.java b/src/test/java/com/google/devtools/build/lib/testutil/BlazeTestSuiteBuilder.java
new file mode 100644
index 0000000..d2f36a6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/BlazeTestSuiteBuilder.java
@@ -0,0 +1,123 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.base.Predicate;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A base class for constructing test suites by searching the classpath for
+ * tests, possibly restricted to a predicate.
+ */
+public class BlazeTestSuiteBuilder {
+
+  /**
+   * @return a TestSuiteBuilder configured for Blaze.
+   */
+  protected TestSuiteBuilder getBuilder() {
+    return new TestSuiteBuilder()
+        .addPackageRecursive("com.google.devtools.build.lib");
+  }
+
+  /** A predicate that succeeds only for LARGE tests. */
+  public static final Predicate<Class<?>> TEST_IS_LARGE =
+      hasSize(Suite.LARGE_TESTS);
+
+  /** A predicate that succeeds only for MEDIUM tests. */
+  public static final Predicate<Class<?>> TEST_IS_MEDIUM =
+      hasSize(Suite.MEDIUM_TESTS);
+
+  /** A predicate that succeeds only for SMALL tests. */
+  public static final Predicate<Class<?>> TEST_IS_SMALL =
+      hasSize(Suite.SMALL_TESTS);
+
+  /** A predicate that succeeds only for non-flaky tests. */
+  public static final Predicate<Class<?>> TEST_IS_FLAKY = new Predicate<Class<?>>() {
+    @Override
+    public boolean apply(Class<?> testClass) {
+      return Suite.isFlaky(testClass);
+    }
+  };
+
+  private static Predicate<Class<?>> hasSize(final Suite size) {
+    return new Predicate<Class<?>>() {
+      @Override
+      public boolean apply(Class<?> testClass) {
+        return Suite.getSize(testClass) == size;
+      }
+    };
+  }
+
+  protected static Predicate<Class<?>> inSuite(final String suiteName) {
+    return new Predicate<Class<?>>() {
+      @Override
+      public boolean apply(Class<?> testClass) {
+        return Suite.getSuiteName(testClass).equalsIgnoreCase(suiteName);
+      }
+    };
+  }
+
+  /**
+   * Given a TestCase subclass, returns its designated suite annotation, if
+   * any, or the empty string otherwise.
+   */
+  public static String getSuite(Class<?> clazz) {
+    TestSpec spec = clazz.getAnnotation(TestSpec.class);
+    return spec == null ? "" : spec.suite();
+  }
+
+  /**
+   * Returns a predicate over TestCases that is true iff the TestCase has a
+   * TestSpec annotation whose suite="..." value (a comma-separated list of
+   * tags) matches all of the query operators specified in the system property
+   * {@code blaze.suite}.  The latter is also a comma-separated list, but of
+   * query operators, each of which is either the name of a tag which must be
+   * present (e.g. "foo"), or the !-prefixed name of a tag that must be absent
+   * (e.g. "!foo").
+   */
+  public static Predicate<Class<?>> matchesSuiteQuery() {
+    final String suiteProperty = System.getProperty("blaze.suite");
+    if (suiteProperty == null) {
+      throw new IllegalArgumentException("blaze.suite property not found");
+    }
+    final Set<String> queryTokens = splitCommas(suiteProperty);
+    return new Predicate<Class<?>>() {
+      @Override
+      public boolean apply(Class<?> testClass) {
+        // Return true iff every queryToken is satisfied by suiteTags.
+        Set<String> suiteTags = splitCommas(getSuite(testClass));
+        for (String queryToken : queryTokens) {
+          if (queryToken.startsWith("!")) { // forbidden tag
+            if (suiteTags.contains(queryToken.substring(1))) {
+              return false;
+            }
+          } else { // mandatory tag
+            if (!suiteTags.contains(queryToken)) {
+              return false;
+            }
+          }
+        }
+        return true;
+      }
+    };
+  }
+
+  private static Set<String> splitCommas(String s) {
+    return new HashSet<>(Arrays.asList(s.split(",")));
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/BlazeTestUtils.java b/src/test/java/com/google/devtools/build/lib/testutil/BlazeTestUtils.java
new file mode 100644
index 0000000..8588a08
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/BlazeTestUtils.java
@@ -0,0 +1,118 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Some static utility functions for testing Blaze code. In contrast to {@link TestUtils}, these
+ * functions are Blaze-specific.
+ */
+public class BlazeTestUtils {
+  private BlazeTestUtils() {}
+
+  /**
+   * Populates the _embedded_binaries/ directory, containing all binaries/libraries, by symlinking
+   * directories#getEmbeddedBinariesRoot() to the test's runfiles tree.
+   */
+  public static BinTools getIntegrationBinTools(BlazeDirectories directories) throws IOException {
+    Path embeddedDir = directories.getEmbeddedBinariesRoot();
+    FileSystemUtils.createDirectoryAndParents(embeddedDir);
+
+    Path runfiles = directories.getFileSystem().getPath(BlazeTestUtils.runfilesDir());
+    // Copy over everything in embedded_scripts.
+    Path embeddedScripts = runfiles.getRelative(TestConstants.EMBEDDED_SCRIPTS_PATH);
+    Collection<Path> files = new ArrayList<Path>();
+    if (embeddedScripts.exists()) {
+      files.addAll(embeddedScripts.getDirectoryEntries());
+    } else {
+      System.err.println("test does not have " + embeddedScripts);
+    }
+
+    for (Path fromFile : files) {
+      try {
+        embeddedDir.getChild(fromFile.getBaseName()).createSymbolicLink(fromFile);
+      } catch (IOException e) {
+        System.err.println("Could not symlink: " + e.getMessage());
+      }
+    }
+
+    return BinTools.forIntegrationTesting(
+        directories, embeddedDir.toString(), TestConstants.EMBEDDED_TOOLS);
+  }
+
+  /**
+   * Writes a FilesetRule to a String array.
+   *
+   * @param name the name of the rule.
+   * @param out the output directory.
+   * @param entries The FilesetEntry entries.
+   * @return the String array of the rule.  One String for each line.
+   */
+  public static String[] createFilesetRule(String name, String out, String... entries) {
+    return new String[] {
+        String.format("Fileset(name = '%s', out = '%s',", name, out),
+                      "        entries = [" +  Joiner.on(", ").join(entries) + "])"
+    };
+  }
+
+  public static File undeclaredOutputDir() {
+    String dir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+    if (dir != null) {
+      return new File(dir);
+    }
+
+    return TestUtils.tmpDirFile();
+  }
+
+  public static String runfilesDir() {
+    String runfilesDirStr = TestUtils.getUserValue("TEST_SRCDIR");
+    Preconditions.checkState(runfilesDirStr != null && runfilesDirStr.length() > 0,
+        "TEST_SRCDIR unset or empty");
+    return new File(runfilesDirStr).getAbsolutePath();
+  }
+
+  /** Creates an empty file, along with all its parent directories. */
+  public static void makeEmptyFile(Path path) throws IOException {
+    FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+    FileSystemUtils.createEmptyFile(path);
+  }
+
+  /**
+   * Changes the mtime of the file "path", which must exist.  No guarantee is
+   * made about the new mtime except that it is different from the previous one.
+   *
+   * @throws IOException if the mtime could not be read or set.
+   */
+  public static void changeModtime(Path path)
+    throws IOException {
+    long prevMtime = path.getLastModifiedTime();
+    long newMtime = prevMtime;
+    do {
+      newMtime += 1000;
+      path.setLastModifiedTime(newMtime);
+    } while (path.getLastModifiedTime() == prevMtime);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/BuildRuleBuilder.java b/src/test/java/com/google/devtools/build/lib/testutil/BuildRuleBuilder.java
new file mode 100644
index 0000000..9f770c5
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/BuildRuleBuilder.java
@@ -0,0 +1,202 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.packages.RuleClass;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility for quickly creating BUILD file rules for use in tests.
+ *
+ * <p>The use case for this class is writing BUILD files where simple
+ * readability for the sake of rules' relationship to the test framework
+ * is more important than detailed semantics and layout.
+ *
+ * <p>The behavior provided by this class is not meant to be exhaustive,
+ * but should handle a majority of simple cases.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ *   String text = new BuildRuleBuilder("java_library", "MyRule")
+        .setSources("First.java", "Second.java", "Third.java")
+        .setDeps(":library", "//java/com/google/common/collect")
+        .setResources("schema/myschema.xsd")
+        .build();
+ * </pre>
+ *
+ */
+public class BuildRuleBuilder {
+  protected final RuleClass ruleClass;
+  protected final String ruleName;
+  private Map<String, List<String>> multiValueAttributes;
+  private Map<String, Object> singleValueAttributes;
+  protected Map<String, RuleClass> ruleClassMap;
+  
+  /**
+   * Create a new instance.
+   *
+   * @param ruleClass the rule class of the new rule
+   * @param ruleName the name of the new rule.
+   */
+  public BuildRuleBuilder(String ruleClass, String ruleName) {
+    this(ruleClass, ruleName, getDefaultRuleClassMap());
+  }
+
+  protected static Map<String, RuleClass> getDefaultRuleClassMap() {
+    return TestRuleClassProvider.getRuleClassProvider().getRuleClassMap();
+  }
+
+  public BuildRuleBuilder(String ruleClass, String ruleName, Map<String, RuleClass> ruleClassMap) {
+    this.ruleClass = ruleClassMap.get(ruleClass);
+    this.ruleName = ruleName;
+    this.multiValueAttributes = new HashMap<>();
+    this.singleValueAttributes = new HashMap<>();
+    this.ruleClassMap = ruleClassMap;
+  }
+
+  /**
+   * Sets the value of a single valued attribute
+   */
+  public BuildRuleBuilder setSingleValueAttribute(String attrName, Object value) {
+    Preconditions.checkState(!singleValueAttributes.containsKey(attrName),
+        "attribute '" + attrName + "' already set");
+    singleValueAttributes.put(attrName, value);
+    return this;
+  }
+
+  /**
+   * Sets the value of a list type attribute
+   */
+  public BuildRuleBuilder setMultiValueAttribute(String attrName, String... value) {
+    Preconditions.checkState(!multiValueAttributes.containsKey(attrName),
+        "attribute '" + attrName + "' already set");
+    multiValueAttributes.put(attrName, Lists.newArrayList(value));
+    return this;
+  }
+
+  /**
+   * Set the srcs attribute.
+   */
+  public BuildRuleBuilder setSources(String... sources) {
+    return setMultiValueAttribute("srcs", sources);
+  }
+
+  /**
+   * Set the deps attribute.
+   */
+  public BuildRuleBuilder setDeps(String... deps) {
+    return setMultiValueAttribute("deps", deps);
+  }
+
+  /**
+   * Set the resources attribute.
+   */
+  public BuildRuleBuilder setResources(String... resources) {
+    return setMultiValueAttribute("resources", resources);
+  }
+
+  /**
+   * Set the data attribute.
+   */
+  public BuildRuleBuilder setData(String... data) {
+    return setMultiValueAttribute("data", data);
+  }
+
+  /**
+   * Generate the rule
+   *
+   * @return a string representation of the rule.
+   */
+  public String build() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(ruleClass.getName()).append("(");
+    printNormal(sb, "name", ruleName);
+    for (Map.Entry<String, List<String>> entry : multiValueAttributes.entrySet()) {
+      printArray(sb, entry.getKey(), entry.getValue());
+    }
+    for (Map.Entry<String, Object> entry : singleValueAttributes.entrySet()) {
+      printNormal(sb, entry.getKey(), entry.getValue());
+    }
+    sb.append(")\n");
+    return sb.toString();
+  }
+
+  private void printArray(StringBuilder sb, String attr, List<String> values) {
+    if (values == null || values.isEmpty()) {
+      return;
+    }
+    sb.append("      ").append(attr).append(" = ");
+    printList(sb, values);
+    sb.append(",");
+    sb.append("\n");
+  }
+
+  private void printNormal(StringBuilder sb, String attr, Object value) {
+    if (value == null) {
+      return;
+    }
+    sb.append("      ").append(attr).append(" = ");
+    if (value instanceof Integer) {
+      sb.append(value);
+    } else {
+      sb.append("'").append(value).append("'");
+    }
+    sb.append(",");
+    sb.append("\n");
+  }
+
+  /**
+   * Turns iterable of {a b c} into string "['a', 'b', 'c']", appends to
+   * supplied StringBuilder.
+   */
+  private void printList(StringBuilder sb, List<String> elements) {
+    sb.append("[");
+    Joiner.on(",").appendTo(sb,
+        Iterables.transform(elements, new Function<String, String>() {
+          @Override
+          public String apply(String from) {
+            return "'" + from + "'";
+          }
+        }));
+    sb.append("]");
+  }
+
+  /**
+   * Returns the transitive closure of file names need to be generated in order
+   * for this rule to build.
+   */
+  public Collection<String> getFilesToGenerate() {
+    return ImmutableList.of();
+  }
+
+  /**
+   * Returns the transitive closure of BuildRuleBuilders need to be generated in order
+   * for this rule to build.
+   */
+  public Collection<BuildRuleBuilder> getRulesToGenerate() {
+    return ImmutableList.of();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/BuildRuleWithDefaultsBuilder.java b/src/test/java/com/google/devtools/build/lib/testutil/BuildRuleWithDefaultsBuilder.java
new file mode 100644
index 0000000..4af7ad1
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/BuildRuleWithDefaultsBuilder.java
@@ -0,0 +1,231 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.AllowedValueSet;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A helper class to generate valid rules with filled attributes if necessary.
+ */
+public class BuildRuleWithDefaultsBuilder extends BuildRuleBuilder {
+
+  private Set<String> generateFiles;
+  private Map<String, BuildRuleBuilder> generateRules;
+
+  public BuildRuleWithDefaultsBuilder(String ruleClass, String ruleName) {
+    super(ruleClass, ruleName);
+    this.generateFiles = new HashSet<>();
+    this.generateRules = new HashMap<>();
+  }
+
+  private BuildRuleWithDefaultsBuilder(String ruleClass, String ruleName,
+      Map<String, RuleClass> ruleClassMap, Set<String> generateFiles,
+      Map<String, BuildRuleBuilder> generateRules) {
+    super(ruleClass, ruleName, ruleClassMap);
+    this.generateFiles = generateFiles;
+    this.generateRules = generateRules;
+  }
+
+  /**
+   * Creates a dummy file with the given extension in the given package and returns a valid Blaze
+   * label referring to the file. Note, the created label depends on the package of the rule.
+   */
+  private String getDummyFileLabel(String rulePkg, String filePkg, String extension,
+      Type<?> attrType) {
+    boolean isInput = (attrType == Type.LABEL || attrType == Type.LABEL_LIST);
+    String fileName = (isInput ? "dummy_input" : "dummy_output") + extension;
+    generateFiles.add(filePkg + "/" + fileName);
+    if (rulePkg.equals(filePkg)) {
+      return ":" + fileName;
+    } else {
+      return filePkg + ":" + fileName;
+    }
+  }
+
+  private String getDummyRuleLabel(String rulePkg, RuleClass referencedRuleClass) {
+    String referencedRuleName = ruleName + "_ref_" + referencedRuleClass.getName()
+        .replace("$", "").replace(":", "");
+    // The new generated rule should have the same generatedFiles and generatedRules
+    // in order to avoid duplications
+    BuildRuleWithDefaultsBuilder builder = new BuildRuleWithDefaultsBuilder(
+        referencedRuleClass.getName(), referencedRuleName, ruleClassMap, generateFiles,
+        generateRules);
+    builder.popuplateAttributes(rulePkg, true);
+    generateRules.put(referencedRuleClass.getName(), builder);
+    return referencedRuleName;
+  }
+
+  public BuildRuleWithDefaultsBuilder popuplateLabelAttribute(String pkg, Attribute attribute) {
+    return popuplateLabelAttribute(pkg, pkg, attribute);
+  }
+
+  /**
+   * Populates the label type attribute with generated values. Populates with a file if possible, or
+   * generates an appropriate rule. Note, that the rules are always generated in the same package.
+   */
+  public BuildRuleWithDefaultsBuilder popuplateLabelAttribute(String rulePkg, String filePkg,
+      Attribute attribute) {
+    Type<?> attrType = attribute.getType();
+    String label = null;
+    if (attribute.getAllowedFileTypesPredicate() != FileTypeSet.NO_FILE) {
+      // Try to populate with files first
+      String extension = null;
+      if (attribute.getAllowedFileTypesPredicate() == FileTypeSet.ANY_FILE) {
+        extension = ".txt";
+      } else {
+        FileTypeSet fileTypes = attribute.getAllowedFileTypesPredicate();
+        // This argument should always hold, if not that means a Blaze design/implementation error
+        Preconditions.checkArgument(fileTypes.getExtensions().size() > 0);
+        extension = fileTypes.getExtensions().get(0);
+      }
+      label = getDummyFileLabel(rulePkg, filePkg, extension, attrType);
+    } else {
+      Predicate<RuleClass> allowedRuleClasses = attribute.getAllowedRuleClassesPredicate();
+      if (allowedRuleClasses != Predicates.<RuleClass>alwaysFalse()) {
+        // See if there is an applicable rule among the already enqueued rules
+        BuildRuleBuilder referencedRuleBuilder = getFirstApplicableRule(allowedRuleClasses);
+        if (referencedRuleBuilder != null) {
+          label = ":" + referencedRuleBuilder.ruleName;
+        } else {
+          RuleClass referencedRuleClass = getFirstApplicableRuleClass(allowedRuleClasses);
+          if (referencedRuleClass != null) {
+            // Generate a rule with the appropriate ruleClass and a label for it in
+            // the original rule
+            label = ":" + getDummyRuleLabel(rulePkg, referencedRuleClass);
+          }
+        }
+      }
+    }
+    if (label != null) {
+      if (attrType == Type.LABEL_LIST || attrType == Type.OUTPUT_LIST) {
+        setMultiValueAttribute(attribute.getName(), label);
+      } else {
+        setSingleValueAttribute(attribute.getName(), label);
+      }
+    }
+    return this;
+  }
+
+  private BuildRuleBuilder getFirstApplicableRule(Predicate<RuleClass> allowedRuleClasses) {
+    // There is no direct way to get the set of allowedRuleClasses from the Attribute
+    // The Attribute API probably should not be modified for sole testing purposes
+    for (Map.Entry<String, BuildRuleBuilder> entry : generateRules.entrySet()) {
+      if (allowedRuleClasses.apply(ruleClassMap.get(entry.getKey()))) {
+        return entry.getValue();
+      }
+    }
+    return null;
+  }
+
+  private RuleClass getFirstApplicableRuleClass(Predicate<RuleClass> allowedRuleClasses) {
+    // See comments in getFirstApplicableRule(Predicate<RuleClass> allowedRuleClasses)
+    for (RuleClass ruleClass : ruleClassMap.values()) {
+      if (allowedRuleClasses.apply(ruleClass)) {
+        return ruleClass;
+      }
+    }
+    return null;
+  }
+
+  public BuildRuleWithDefaultsBuilder popuplateStringListAttribute(Attribute attribute) {
+    setMultiValueAttribute(attribute.getName(), "x");
+    return this;
+  }
+
+  public BuildRuleWithDefaultsBuilder popuplateStringAttribute(Attribute attribute) {
+    setSingleValueAttribute(attribute.getName(), "x");
+    return this;
+  }
+
+  public BuildRuleWithDefaultsBuilder popuplateBooleanAttribute(Attribute attribute) {
+    setSingleValueAttribute(attribute.getName(), "false");
+    return this;
+  }
+
+  public BuildRuleWithDefaultsBuilder popuplateIntegerAttribute(Attribute attribute) {
+    setSingleValueAttribute(attribute.getName(), 1);
+    return this;
+  }
+
+  public BuildRuleWithDefaultsBuilder popuplateAttributes(String rulePkg, boolean heuristics) {
+    for (Attribute attribute : ruleClass.getAttributes()) {
+      if (attribute.isMandatory()) {
+        if (attribute.getType() == Type.LABEL_LIST || attribute.getType() == Type.OUTPUT_LIST) {
+          if (attribute.isNonEmpty()) {
+            popuplateLabelAttribute(rulePkg, attribute);
+          } else {
+            // TODO(bazel-team): actually here an empty list would be fine, but BuildRuleBuilder
+            // doesn't support that, and it makes little sense anyway
+            popuplateLabelAttribute(rulePkg, attribute);
+          }
+        } else if (attribute.getType() == Type.LABEL || attribute.getType() == Type.OUTPUT) {
+          popuplateLabelAttribute(rulePkg, attribute);
+        } else {
+          // Non label type attributes
+          if (attribute.getAllowedValues() instanceof AllowedValueSet) {
+            Collection<Object> allowedValues =
+                ((AllowedValueSet) attribute.getAllowedValues()).getAllowedValues();
+            setSingleValueAttribute(attribute.getName(), allowedValues.iterator().next());
+          } else if (attribute.getType() == Type.STRING) {
+            popuplateStringAttribute(attribute);
+          } else if (attribute.getType() == Type.BOOLEAN) {
+            popuplateBooleanAttribute(attribute);
+          } else if (attribute.getType() == Type.INTEGER) {
+            popuplateIntegerAttribute(attribute);
+          } else if (attribute.getType() == Type.STRING_LIST) {
+            popuplateStringListAttribute(attribute);
+          }
+        }
+        // TODO(bazel-team): populate for other data types
+      } else if (heuristics) {
+        populateAttributesHeuristics(rulePkg, attribute);
+      }
+    }
+    return this;
+  }
+
+  // Heuristics which might help to generate valid rules.
+  // This is a bit hackish, but it helps some generated ruleclasses to pass analysis phase.
+  private void populateAttributesHeuristics(String rulePkg, Attribute attribute) {
+    if (attribute.getName().equals("srcs") && attribute.getType() == Type.LABEL_LIST) {
+      // If there is a srcs attribute it might be better to populate it even if it's not mandatory
+      popuplateLabelAttribute(rulePkg, attribute);
+    } else if (attribute.getName().equals("main_class") && attribute.getType() == Type.STRING) {
+      popuplateStringAttribute(attribute);
+    }
+  }
+
+  @Override
+  public Collection<String> getFilesToGenerate() {
+    return generateFiles;
+  }
+
+  @Override
+  public Collection<BuildRuleBuilder> getRulesToGenerate() {
+    return generateRules.values();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/ChattyAssertsTestCase.java b/src/test/java/com/google/devtools/build/lib/testutil/ChattyAssertsTestCase.java
new file mode 100644
index 0000000..f17d81e
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/ChattyAssertsTestCase.java
@@ -0,0 +1,237 @@
+// Copyright 2014 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.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.ExitCode;
+
+import junit.framework.TestCase;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Most of this stuff is copied from junit's {@link junit.framework.Assert}
+ * class, and then customized to make the error messages a bit more informative.
+ */
+public abstract class ChattyAssertsTestCase extends TestCase {
+  private long currentTestStartTime = -1;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    currentTestStartTime = BlazeClock.instance().currentTimeMillis();
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    JunitTestUtils.nullifyInstanceFields(this);
+    assertFalse("tearDown without setUp!", currentTestStartTime == -1);
+
+    super.tearDown();
+  }
+
+  /**
+   * Asserts that two objects are equal. If they are not
+   * an AssertionFailedError is thrown with the given message.
+   */
+  public static void assertEquals(String message, Object expected,
+      Object actual) {
+    if (Objects.equals(expected, actual)) {
+      return;
+    }
+    chattyFailNotEquals(message, expected, actual);
+  }
+
+  /**
+   * Asserts that two objects are equal. If they are not
+   * an AssertionFailedError is thrown.
+   */
+  public static void assertEquals(Object expected, Object actual) {
+    assertEquals(null, expected, actual);
+  }
+
+  /**
+   * Asserts that two Strings are equal.
+   */
+  public static void assertEquals(String message, String expected, String actual) {
+    assertWithMessage(message).that(actual).isEqualTo(expected);
+  }
+
+  /**
+   * Asserts that two Strings are equal.
+   */
+  public static void assertEquals(String expected, String actual) {
+    assertEquals(null, expected, actual);
+  }
+
+  /**
+   * Asserts that two Strings are equal considering the line separator to be \n
+   * independently of the operating system.
+   */
+  public static void assertEqualsUnifyingLineEnds(String expected, String actual) {
+    MoreAsserts.assertEqualsUnifyingLineEnds(expected, actual);
+  }
+
+  private static void chattyFailNotEquals(String message, Object expected,
+      Object actual) {
+    fail(MoreAsserts.chattyFormat(message, expected, actual));
+  }
+
+  /**
+   * Asserts that {@code e}'s exception message contains each of {@code strings}
+   * <b>surrounded by single quotation marks</b>.
+   */
+  public static void assertMessageContainsWordsWithQuotes(Exception e,
+                                                          String... strings) {
+    assertContainsWordsWithQuotes(e.getMessage(), strings);
+  }
+
+  /**
+   * Asserts that {@code message} contains each of {@code strings}
+   * <b>surrounded by single quotation marks</b>.
+   */
+  public static void assertContainsWordsWithQuotes(String message,
+                                                   String... strings) {
+    MoreAsserts.assertContainsWordsWithQuotes(message, strings);
+  }
+
+  public static void assertNonZeroExitCode(int exitCode, String stdout, String stderr) {
+    MoreAsserts.assertNonZeroExitCode(exitCode, stdout, stderr);
+  }
+
+  public static void assertZeroExitCode(int exitCode, String stdout, String stderr) {
+    assertExitCode(0, exitCode, stdout, stderr);
+  }
+
+  public static void assertExitCode(ExitCode expectedExitCode,
+      int exitCode, String stdout, String stderr) {
+    int expectedExitCodeValue = expectedExitCode.getNumericExitCode();
+    if (exitCode != expectedExitCodeValue) {
+      fail(String.format("expected exit code '%s' <%d> but exit code was <%d> and stdout was <%s> "
+              + "and stderr was <%s>",
+              expectedExitCode.name(), expectedExitCodeValue, exitCode, stdout, stderr));
+    }
+  }
+
+  public static void assertExitCode(int expectedExitCode,
+      int exitCode, String stdout, String stderr) {
+    MoreAsserts.assertExitCode(expectedExitCode, exitCode,  stdout, stderr);
+  }
+
+  public static void assertStdoutContainsString(String expected, String stdout, String stderr) {
+    MoreAsserts.assertStdoutContainsString(expected, stdout, stderr);
+  }
+
+  public static void assertStderrContainsString(String expected, String stdout, String stderr) {
+    MoreAsserts.assertStderrContainsString(expected, stdout, stderr);
+  }
+
+  public static void assertStdoutContainsRegex(String expectedRegex,
+      String stdout, String stderr) {
+    MoreAsserts.assertStdoutContainsRegex(expectedRegex, stdout, stderr);
+  }
+
+  public static void assertStderrContainsRegex(String expectedRegex,
+      String stdout, String stderr) {
+    MoreAsserts.assertStderrContainsRegex(expectedRegex, stdout, stderr);
+  }
+
+
+
+  /********************************************************************
+   *                                                                  *
+   *       Other testing utilities (unrelated to "chattiness")        *
+   *                                                                  *
+   ********************************************************************/
+
+  /**
+   * Returns the elements from the given collection in a set.
+   */
+  protected static <T> Set<T> asSet(Iterable<T> collection) {
+    return Sets.newHashSet(collection);
+  }
+
+  /**
+   * Returns the arguments given as varargs as a set.
+   */
+  @SuppressWarnings({"unchecked", "varargs"})
+  protected static <T> Set<T> asSet(T... elements) {
+    return Sets.newHashSet(elements);
+  }
+
+  /**
+   * Returns the arguments given as varargs as a set of sorted Strings.
+   */
+  protected static Set<String> asStringSet(Iterable<?> collection) {
+    return MoreAsserts.asStringSet(collection);
+  }
+
+  /**
+   * An equivalence relation for Collection, based on mapping to Set.
+   *
+   * Oft-forgotten fact: for all x in Set, y in List, !x.equals(y) even if
+   * their elements are the same.
+   */
+  protected static <T> void
+      assertSameContents(Iterable<? extends T> expected, Iterable<? extends T> actual) {
+    MoreAsserts.assertSameContents(expected, actual);
+  }
+
+  /**
+   * Asserts the presence or absence of values in the collection.
+   */
+  protected <T> void assertPresence(Iterable<T> actual, Iterable<Presence<T>> expectedPresences) {
+    for (Presence<T> expected : expectedPresences) {
+      if (expected.presence) {
+        assertThat(actual).contains(expected.value);
+      } else {
+        assertThat(actual).doesNotContain(expected.value);
+      }
+    }
+  }
+
+  /** Creates a presence information with expected value. */
+  protected static <T> Presence<T> present(T expected) {
+    return new Presence<>(expected, true);
+  }
+
+  /** Creates an absence information with expected value. */
+  protected static <T> Presence<T> absent(T expected) {
+    return new Presence<>(expected, false);
+  }
+
+  /**
+   * Combines value with the boolean presence flag.
+   *
+   * @param <T> value type
+   */
+  protected final static class Presence <T> {
+    /** wrapped value */
+    public final T value;
+    /** boolean presence flag */
+    public final boolean presence;
+
+    /** Creates a tuple of value and a boolean presence flag. */
+    Presence(T value, boolean presence) {
+      this.value = value;
+      this.presence = presence;
+    }
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/Classpath.java b/src/test/java/com/google/devtools/build/lib/testutil/Classpath.java
new file mode 100644
index 0000000..94711ac
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/Classpath.java
@@ -0,0 +1,132 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.base.Preconditions;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * A helper class to find all classes on the current classpath. This is used to automatically create
+ * JUnit 3 and 4 test suites.
+ */
+final class Classpath {
+  private static final String CLASS_EXTENSION = ".class";
+
+  /**
+   * Finds all classes that live in or below the given package.
+   */
+  static Set<Class<?>> findClasses(String packageName) {
+    Set<Class<?>> result = new LinkedHashSet<>();
+    String pathPrefix = (packageName + '.').replace('.', '/');
+    for (String entryName : getClassPath()) {
+      File classPathEntry = new File(entryName);
+      if (classPathEntry.exists()) {
+        try {
+          Set<String> classNames;
+          if (classPathEntry.isDirectory()) {
+            classNames = findClassesInDirectory(classPathEntry, pathPrefix);
+          } else {
+            classNames = findClassesInJar(classPathEntry, pathPrefix);
+          }
+          for (String className : classNames) {
+            Class<?> clazz = Class.forName(className);
+            result.add(clazz);
+          }
+        } catch (IOException e) {
+          throw new AssertionError("Can't read classpath entry "
+              + entryName + ": " + e.getMessage());
+        } catch (ClassNotFoundException e) {
+          throw new AssertionError("Class not found even though it is on the classpath "
+              + entryName + ": " + e.getMessage());
+        }
+      }
+    }
+    return result;
+  }
+
+  private static Set<String> findClassesInDirectory(File classPathEntry, String pathPrefix) {
+    Set<String> result = new TreeSet<>();
+    File directory = new File(classPathEntry, pathPrefix);
+    innerFindClassesInDirectory(result, directory, pathPrefix);
+    return result;
+  }
+
+  /**
+   * Finds all classes and sub packages in the given directory that are below the given package and
+   * add them to the respective sets.
+   *
+   * @param directory Directory to inspect
+   * @param pathPrefix Prefix for the path to the classes that are requested
+   *                   (ex: {@code com/google/foo/bar})
+   */
+  private static void innerFindClassesInDirectory(Set<String> classNames, File directory,
+      String pathPrefix) {
+    Preconditions.checkArgument(pathPrefix.endsWith("/"));
+    if (directory.exists()) {
+      for (File f : directory.listFiles()) {
+        String name = f.getName();
+        if (name.endsWith(CLASS_EXTENSION)) {
+          String clzName = getClassName(pathPrefix + name);
+          classNames.add(clzName);
+        } else if (f.isDirectory()) {
+          findClassesInDirectory(f, pathPrefix + name + "/");
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns a set of all classes in the jar that start with the given prefix.
+   */
+  private static Set<String> findClassesInJar(File jarFile, String pathPrefix) throws IOException {
+    Set<String> classNames = new TreeSet<>();
+    try (ZipFile zipFile = new ZipFile(jarFile)) {
+      Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        String entryName = entries.nextElement().getName();
+        if (entryName.startsWith(pathPrefix) && entryName.endsWith(CLASS_EXTENSION)) {
+          classNames.add(getClassName(entryName));
+        }
+      }
+    }
+    return classNames;
+  }
+
+  /**
+   * Given the absolute path of a class file, return the class name.
+   */
+  private static String getClassName(String className) {
+    int classNameEnd = className.length() - CLASS_EXTENSION.length();
+    return className.substring(0, classNameEnd).replace('/', '.');
+  }
+
+  /**
+   * Gets the class path from the System Property "java.class.path" and splits
+   * it up into the individual elements.
+   */
+  private static String[] getClassPath() {
+    String classPath = System.getProperty("java.class.path");
+    String separator = System.getProperty("path.separator", ":");
+    return classPath.split(Pattern.quote(separator));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/ClasspathSuite.java b/src/test/java/com/google/devtools/build/lib/testutil/ClasspathSuite.java
new file mode 100644
index 0000000..ee880fc
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/ClasspathSuite.java
@@ -0,0 +1,43 @@
+// Copyright 2014 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.testutil;
+
+import org.junit.runners.Suite;
+import org.junit.runners.model.RunnerBuilder;
+
+import java.util.Set;
+
+/**
+ * A suite implementation that finds all JUnit 3 and 4 classes on the current classpath in or below
+ * the package of the annotated class, except classes that are annotated with {@code ClasspathSuite}
+ * or {@link CustomSuite}.
+ *
+ * <p>If you need to specify a custom test class filter or a different package prefix, then use
+ * {@link CustomSuite} instead.
+ */
+public final class ClasspathSuite extends Suite {
+
+  /**
+   * Only called reflectively. Do not use programmatically.
+   */
+  public ClasspathSuite(Class<?> klass, RunnerBuilder builder) throws Throwable {
+    super(builder, klass, getClasses(klass));
+  }
+
+  private static Class<?>[] getClasses(Class<?> klass) {
+    Set<Class<?>> result = new TestSuiteBuilder().addPackageRecursive(klass.getPackage().getName())
+        .create();
+    return result.toArray(new Class<?>[result.size()]);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/CustomSuite.java b/src/test/java/com/google/devtools/build/lib/testutil/CustomSuite.java
new file mode 100644
index 0000000..6e3b6c5
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/CustomSuite.java
@@ -0,0 +1,53 @@
+// Copyright 2014 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.testutil;
+
+import org.junit.runners.Suite;
+import org.junit.runners.model.RunnerBuilder;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Set;
+
+/**
+ * A JUnit4 suite implementation that delegates the class finding to a {@code suite()} method on the
+ * annotated class. To be used in combination with {@link TestSuiteBuilder}.
+ */
+public final class CustomSuite extends Suite {
+
+  /**
+   * Only called reflectively. Do not use programmatically.
+   */
+  public CustomSuite(Class<?> klass, RunnerBuilder builder) throws Throwable {
+    super(builder, klass, getClasses(klass));
+  }
+
+  private static Class<?>[] getClasses(Class<?> klass) {
+    Set<Class<?>> result = evalSuite(klass);
+    return result.toArray(new Class<?>[result.size()]);
+  }
+
+  @SuppressWarnings("unchecked") // unchecked cast to a generic type
+  private static Set<Class<?>> evalSuite(Class<?> klass) {
+    try {
+      Method m = klass.getMethod("suite");
+      if (!Modifier.isStatic(m.getModifiers())) {
+        throw new IllegalStateException("suite() must be static");
+      }
+      return (Set<Class<?>>) m.invoke(null);
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/DebuggingEventHandler.java b/src/test/java/com/google/devtools/build/lib/testutil/DebuggingEventHandler.java
new file mode 100644
index 0000000..59fe5fb
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/DebuggingEventHandler.java
@@ -0,0 +1,41 @@
+// Copyright 2014 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.testutil;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+
+import java.io.PrintStream;
+
+/**
+ * Prints all errors and warnings to {@link System#out}.
+ */
+public class DebuggingEventHandler implements EventHandler {
+
+  private PrintStream out;
+
+  public DebuggingEventHandler() {
+    this.out = System.out;
+  }
+
+  @Override
+  public void handle(Event e) {
+    if (e.getLocation() != null) {
+      out.println(e.getKind() + " " + e.getLocation() + ": " + e.getMessage());
+    } else {
+      out.println(e.getKind() + " " + e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java b/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java
new file mode 100644
index 0000000..5c04612
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/FoundationTestCase.java
@@ -0,0 +1,264 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventCollector;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This is a specialization of {@link ChattyAssertsTestCase} that's useful for
+ * implementing tests of the "foundation" library.
+ */
+public abstract class FoundationTestCase extends ChattyAssertsTestCase {
+
+  protected Path rootDirectory;
+
+  protected Path outputBase;
+
+  protected Path actionOutputBase;
+
+  // May be overridden by subclasses:
+  protected Reporter reporter;
+  protected EventCollector eventCollector;
+
+  private Scratch scratch;
+
+
+  // Individual tests can opt-out of this handler if they expect an error, by
+  // calling reporter.removeHandler(failFastHandler).
+  protected static final EventHandler failFastHandler = new EventHandler() {
+      @Override
+      public void handle(Event event) {
+        if (EventKind.ERRORS.contains(event.getKind())) {
+          fail(event.toString());
+        }
+      }
+    };
+
+  protected static final EventHandler printHandler = new EventHandler() {
+      @Override
+      public void handle(Event event) {
+        System.out.println(event);
+      }
+    };
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    scratch = new Scratch(createFileSystem());
+    outputBase = scratchDir("/usr/local/google/_blaze_jrluser/FAKEMD5/");
+    rootDirectory = scratchDir("/" + TestConstants.TEST_WORKSPACE_DIRECTORY);
+    copySkylarkFilesIfExist();
+    actionOutputBase = scratchDir("/usr/local/google/_blaze_jrluser/FAKEMD5/action_out/");
+    eventCollector = new EventCollector(EventKind.ERRORS_AND_WARNINGS);
+    reporter = new Reporter(eventCollector);
+    reporter.addHandler(failFastHandler);
+  }
+
+  /*
+   * Creates the file system; override to inject FS behavior.
+   */
+  protected FileSystem createFileSystem() {
+     return new InMemoryFileSystem(BlazeClock.instance());
+  }
+
+
+  private void copySkylarkFilesIfExist() throws IOException {
+    scratchFile(rootDirectory.getRelative("devtools/blaze/rules/BUILD").getPathString());
+    scratchFile(rootDirectory.getRelative("rules/BUILD").getPathString());
+    copySkylarkFilesIfExist("devtools/blaze/rules/staging", "devtools/blaze/rules");
+    copySkylarkFilesIfExist("devtools/blaze/bazel/base_workspace/tools/build_rules", "rules");
+  }
+
+  private void copySkylarkFilesIfExist(String from, String to) throws IOException {
+    File rulesDir = new File(from);
+    if (rulesDir.exists() && rulesDir.isDirectory()) {
+      for (String fileName : rulesDir.list()) {
+        File file = new File(from + "/" + fileName);
+        if (file.isFile() && fileName.endsWith(".bzl")) {
+          String context = loadFile(file);
+          Path path = rootDirectory.getRelative(to + "/" + fileName);
+          if (path.exists()) {
+            overwriteScratchFile(path.getPathString(), context);
+          } else {
+            scratchFile(path.getPathString(), context);
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    Thread.interrupted(); // Clear any interrupt pending against this thread,
+                          // so that we don't cause later tests to fail.
+
+    super.tearDown();
+  }
+
+  /**
+   * A scratch filesystem that is completely in-memory. Since this file system
+   * is "cached" in a private (but *not* static) field in the test class,
+   * each testFoo method in junit sees a fresh filesystem.
+   */
+  protected FileSystem scratchFS() {
+    return scratch.getFileSystem();
+  }
+
+  /**
+   * Create a scratch file in the scratch filesystem, with the given pathName,
+   * consisting of a set of lines. The method returns a Path instance for the
+   * scratch file.
+   */
+  protected Path scratchFile(String pathName, String... lines)
+      throws IOException {
+    return scratch.file(pathName, lines);
+  }
+
+  /**
+   * Like {@code scratchFile}, but the file is first deleted if it already
+   * exists.
+   */
+  protected Path overwriteScratchFile(String pathName, String... lines) throws IOException {
+    return scratch.overwriteFile(pathName, lines);
+  }
+
+  /**
+   * Deletes the specified scratch file, using the same specification as {@link Path#delete}.
+   */
+  protected boolean deleteScratchFile(String pathName) throws IOException {
+    return scratch.deleteFile(pathName);
+  }
+
+  /**
+   * Create a scratch file in the given filesystem, with the given pathName,
+   * consisting of a set of lines. The method returns a Path instance for the
+   * scratch file.
+   */
+  protected Path scratchFile(FileSystem fs, String pathName, String... lines)
+      throws IOException {
+    return scratch.file(fs, pathName, lines);
+  }
+
+  /**
+   * Create a scratch file in the given filesystem, with the given pathName,
+   * consisting of a set of lines. The method returns a Path instance for the
+   * scratch file.
+   */
+  protected Path scratchFile(FileSystem fs, String pathName, byte[] content)
+      throws IOException {
+    return scratch.file(fs, pathName, content);
+  }
+
+  /**
+   * Create a directory in the scratch filesystem, with the given path name.
+   */
+  public Path scratchDir(String pathName) throws IOException {
+    return scratch.dir(pathName);
+  }
+
+  /**
+   * If "expectedSuffix" is not a suffix of "actual", fails with an informative
+   * assertion.
+   */
+  protected void assertEndsWith(String expectedSuffix, String actual) {
+    if (!actual.endsWith(expectedSuffix)) {
+      fail("\"" + actual + "\" does not end with "
+           + "\"" + expectedSuffix + "\"");
+    }
+  }
+
+  /**
+   * If "expectedPrefix" is not a prefix of "actual", fails with an informative
+   * assertion.
+   */
+  protected void assertStartsWith(String expectedPrefix, String actual) {
+    if (!actual.startsWith(expectedPrefix)) {
+      fail("\"" + actual + "\" does not start with "
+           + "\"" + expectedPrefix + "\"");
+    }
+  }
+
+  // Mix-in assertions:
+
+  protected void assertNoEvents() {
+    JunitTestUtils.assertNoEvents(eventCollector);
+  }
+
+  protected Event assertContainsEvent(String expectedMessage) {
+    return JunitTestUtils.assertContainsEvent(eventCollector,
+                                              expectedMessage);
+  }
+
+  protected Event assertContainsEvent(String expectedMessage, Set<EventKind> kinds) {
+    return JunitTestUtils.assertContainsEvent(eventCollector,
+                                              expectedMessage,
+                                              kinds);
+  }
+
+  protected void assertContainsEventWithFrequency(String expectedMessage,
+      int expectedFrequency) {
+    JunitTestUtils.assertContainsEventWithFrequency(eventCollector, expectedMessage,
+        expectedFrequency);
+  }
+
+  protected void assertDoesNotContainEvent(String expectedMessage) {
+    JunitTestUtils.assertDoesNotContainEvent(eventCollector,
+                                             expectedMessage);
+  }
+
+  protected Event assertContainsEventWithWordsInQuotes(String... words) {
+    return JunitTestUtils.assertContainsEventWithWordsInQuotes(
+        eventCollector, words);
+  }
+
+  protected void assertContainsEventsInOrder(String... expectedMessages) {
+    JunitTestUtils.assertContainsEventsInOrder(eventCollector, expectedMessages);
+  }
+
+  @SuppressWarnings({"unchecked", "varargs"})
+  protected static <T> void assertContainsSublist(List<T> arguments,
+                                                  T... expectedSublist) {
+    JunitTestUtils.assertContainsSublist(arguments, expectedSublist);
+  }
+
+  @SuppressWarnings({"unchecked", "varargs"})
+  protected static <T> void assertDoesNotContainSublist(List<T> arguments,
+                                                        T... expectedSublist) {
+    JunitTestUtils.assertDoesNotContainSublist(arguments, expectedSublist);
+  }
+
+  protected static <T> void assertContainsSubset(Iterable<T> arguments,
+                                                 Iterable<T> expectedSubset) {
+    JunitTestUtils.assertContainsSubset(arguments, expectedSubset);
+  }
+
+  protected String loadFile(File file) throws IOException {
+    return Files.toString(file, Charset.defaultCharset());
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/JunitTestUtils.java b/src/test/java/com/google/devtools/build/lib/testutil/JunitTestUtils.java
new file mode 100644
index 0000000..efe1599
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/JunitTestUtils.java
@@ -0,0 +1,310 @@
+// Copyright 2014 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.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+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.util.Pair;
+
+import junit.framework.TestCase;
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class contains a utility method {@link #nullifyInstanceFields(Object)}
+ * for setting all fields in an instance to {@code null}. This is needed for
+ * junit {@code TestCase} instances that keep expensive objects in fields.
+ * Basically junit holds onto the instances
+ * even after the test methods have run, and it creates one such instance
+ * per {@code testFoo} method.
+ */
+public class JunitTestUtils {
+
+  public static void nullifyInstanceFields(Object instance)
+      throws IllegalAccessException {
+    /**
+     * We're cleaning up this test case instance by assigning null pointers
+     * to all fields to reduce the memory overhead of test case instances
+     * staying around after the test methods have been executed. This is a
+     * bug in junit.
+     */
+    List<Field> instanceFields = new ArrayList<>();
+    for (Class<?> clazz = instance.getClass();
+         !clazz.equals(TestCase.class) && !clazz.equals(Object.class);
+         clazz = clazz.getSuperclass()) {
+      for (Field field : clazz.getDeclaredFields()) {
+        if (Modifier.isStatic(field.getModifiers())) {
+          continue;
+        }
+        if (field.getType().isPrimitive()) {
+          continue;
+        }
+        if (Modifier.isFinal(field.getModifiers())) {
+          String msg = "Please make field \"" + field + "\" non-final, or, if " +
+                       "it's very simple and truly immutable and not too " +
+                       "big, make it static.";
+          throw new AssertionError(msg);
+        }
+        instanceFields.add(field);
+      }
+    }
+    // Run setAccessible for efficiency
+    AccessibleObject.setAccessible(instanceFields.toArray(new Field[0]), true);
+    for (Field field : instanceFields) {
+      field.set(instance, null);
+    }
+  }
+
+  /********************************************************************
+   *                                                                  *
+   *                         "Mix-in methods"                         *
+   *                                                                  *
+   ********************************************************************/
+
+  // Java doesn't support mix-ins, but we need them in our tests so that we can
+  // inherit a bunch of useful methods, e.g. assertions over an EventCollector.
+  // We do this by hand, by delegating from instance methods in each TestCase
+  // to the static methods below.
+
+  /**
+   * If the specified EventCollector contains any events, an informative
+   * assertion fails in the context of the specified TestCase.
+   */
+  public static void assertNoEvents(Iterable<Event> eventCollector) {
+    String eventsString = eventsToString(eventCollector);
+    assertThat(eventsString).isEmpty();
+  }
+
+  /**
+   * If the specified EventCollector contains an unexpected number of events, an informative
+   * assertion fails in the context of the specified TestCase.
+   */
+  public static void assertEventCount(int expectedCount, EventCollector eventCollector) {
+    assertWithMessage(eventsToString(eventCollector))
+        .that(eventCollector.count()).isEqualTo(expectedCount);
+  }
+
+  /**
+   * If the specified EventCollector does not contain an event which has
+   * 'expectedEvent' as a substring, an informative assertion fails. Otherwise
+   * the matching event is returned.
+   */
+  public static Event assertContainsEvent(Iterable<Event> eventCollector,
+      String expectedEvent) {
+    return assertContainsEvent(eventCollector, expectedEvent, EventKind.ALL_EVENTS);
+  }
+
+  /**
+   * If the specified EventCollector does not contain an event of a kind of 'kinds' which has
+   * 'expectedEvent' as a substring, an informative assertion fails. Otherwise
+   * the matching event is returned.
+   */
+  public static Event assertContainsEvent(Iterable<Event> eventCollector,
+                                          String expectedEvent,
+                                          Set<EventKind> kinds) {
+    for (Event event : eventCollector) {
+      if (event.getMessage().contains(expectedEvent) && kinds.contains(event.getKind())) {
+        return event;
+      }
+    }
+    String eventsString = eventsToString(eventCollector);
+    assertWithMessage("Event '" + expectedEvent + "' not found"
+        + (eventsString.length() == 0 ? "" : ("; found these though:" + eventsString)))
+        .that(false).isTrue();
+    return null; // unreachable
+  }
+
+  /**
+   * If the specified EventCollector contains an event which has
+   * 'expectedEvent' as a substring, an informative assertion fails.
+   */
+  public static void assertDoesNotContainEvent(Iterable<Event> eventCollector,
+                                          String expectedEvent) {
+    for (Event event : eventCollector) {
+      assertWithMessage("Unexpected string '" + expectedEvent + "' matched following event:\n"
+          + event.getMessage()).that(event.getMessage()).doesNotContain(expectedEvent);
+    }
+  }
+
+  /**
+   * If the specified EventCollector does not contain an event which has
+   * each of {@code words} surrounded by single quotes as a substring, an
+   * informative assertion fails.  Otherwise the matching event is returned.
+   */
+  public static Event assertContainsEventWithWordsInQuotes(
+      Iterable<Event> eventCollector,
+      String... words) {
+    for (Event event : eventCollector) {
+      boolean found = true;
+      for (String word : words) {
+        if (!event.getMessage().contains("'" + word + "'")) {
+          found = false;
+          break;
+        }
+      }
+      if (found) {
+        return event;
+      }
+    }
+    String eventsString = eventsToString(eventCollector);
+    assertWithMessage("Event containing words " + Arrays.toString(words) + " in "
+        + "single quotes not found"
+        + (eventsString.length() == 0 ? "" : ("; found these though:" + eventsString)))
+        .that(false).isTrue();
+    return null; // unreachable
+  }
+
+  /**
+   * Returns a string consisting of each event in the specified collector,
+   * preceded by a newline.
+   */
+  private static String eventsToString(Iterable<Event> eventCollector) {
+    StringBuilder buf = new StringBuilder();
+    eventLoop: for (Event event : eventCollector) {
+      for (String ignoredPrefix : TestConstants.IGNORED_MESSAGE_PREFIXES) {
+        if (event.getMessage().startsWith(ignoredPrefix)) {
+          continue eventLoop;
+        }
+      }
+      buf.append('\n').append(event);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * If "expectedSublist" is not a sublist of "arguments", an informative
+   * assertion is failed in the context of the specified TestCase.
+   *
+   * Argument order mnemonic: assert(X)ContainsSublist(Y).
+   */
+  @SuppressWarnings({"unchecked", "varargs"})
+  public static <T> void assertContainsSublist(List<T> arguments, T... expectedSublist) {
+    List<T> sublist = Arrays.asList(expectedSublist);
+    try {
+      assertThat(Collections.indexOfSubList(arguments, sublist)).isNotEqualTo(-1);
+    } catch (AssertionError e) {
+      throw new AssertionError("Did not find " + sublist + " as a sublist of " + arguments, e);
+    }
+  }
+
+  /**
+   * If "expectedSublist" is a sublist of "arguments", an informative
+   * assertion is failed in the context of the specified TestCase.
+   *
+   * Argument order mnemonic: assert(X)DoesNotContainSublist(Y).
+   */
+  @SuppressWarnings({"unchecked", "varargs"})
+  public static <T> void assertDoesNotContainSublist(List<T> arguments, T... expectedSublist) {
+    List<T> sublist = Arrays.asList(expectedSublist);
+    try {
+      assertThat(Collections.indexOfSubList(arguments, sublist)).isEqualTo(-1);
+    } catch (AssertionError e) {
+      throw new AssertionError("Found " + sublist + " as a sublist of " + arguments, e);
+    }
+  }
+
+  /**
+   * If "arguments" does not contain "expectedSubset" as a subset, an
+   * informative assertion is failed in the context of the specified TestCase.
+   *
+   * Argument order mnemonic: assert(X)ContainsSubset(Y).
+   */
+  public static <T> void assertContainsSubset(Iterable<T> arguments,
+                                              Iterable<T> expectedSubset) {
+    Set<T> argumentsSet = arguments instanceof Set<?>
+        ? (Set<T>) arguments
+        : Sets.newHashSet(arguments);
+
+    for (T x : expectedSubset) {
+      assertWithMessage("assertContainsSubset failed: did not find element " + x
+          + "\nExpected subset = " + expectedSubset + "\nArguments = " + arguments)
+          .that(argumentsSet).contains(x);
+    }
+  }
+
+  /**
+   * Check to see if each element of expectedMessages is the beginning of a message
+   * in eventCollector, in order, as in {@link #containsSublistWithGapsAndEqualityChecker}.
+   * If not, an informative assertion is failed
+   */
+  protected static void assertContainsEventsInOrder(Iterable<Event> eventCollector,
+      String... expectedMessages) {
+    String failure = containsSublistWithGapsAndEqualityChecker(
+        ImmutableList.copyOf(eventCollector),
+        new Function<Pair<Event, String>, Boolean> () {
+      @Override
+      public Boolean apply(Pair<Event, String> pair) {
+        return pair.first.getMessage().contains(pair.second);
+      }
+    }, expectedMessages);
+
+    String eventsString = eventsToString(eventCollector);
+    assertWithMessage("Event '" + failure + "' not found in proper order"
+        + (eventsString.length() == 0 ? "" : ("; found these though:" + eventsString)))
+        .that(failure).isNull();
+  }
+
+  /**
+   * Check to see if each element of expectedSublist is in arguments, according to
+   * the equalityChecker, in the same order as in expectedSublist (although with
+   * other interspersed elements in arguments allowed).
+   * @param equalityChecker function that takes a Pair<S, T> element and returns true
+   * if the elements of the pair are equal by its lights.
+   * @return first element not in arguments in order, or null if success.
+   */
+  @SuppressWarnings({"unchecked"})
+  protected static <S, T> T containsSublistWithGapsAndEqualityChecker(List<S> arguments,
+      Function<Pair<S, T>, Boolean> equalityChecker, T... expectedSublist) {
+    Iterator<S> iter = arguments.iterator();
+    outerLoop:
+    for (T expected : expectedSublist) {
+      while (iter.hasNext()) {
+        S actual = iter.next();
+        if (equalityChecker.apply(Pair.of(actual, expected))) {
+          continue outerLoop;
+        }
+      }
+      return expected;
+    }
+    return null;
+  }
+
+  public static List<Event> assertContainsEventWithFrequency(Iterable<Event> events,
+      String expectedMessage, int expectedFrequency) {
+    ImmutableList.Builder<Event> builder = ImmutableList.builder();
+    for (Event event : events) {
+      if (event.getMessage().contains(expectedMessage)) {
+        builder.add(event);
+      }
+    }
+    List<Event> foundEvents = builder.build();
+    assertWithMessage(foundEvents.toString()).that(foundEvents).hasSize(expectedFrequency);
+    return foundEvents;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/ManualClock.java b/src/test/java/com/google/devtools/build/lib/testutil/ManualClock.java
new file mode 100644
index 0000000..d4f6058
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/ManualClock.java
@@ -0,0 +1,40 @@
+// Copyright 2014 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.testutil;
+
+import com.google.devtools.build.lib.util.Clock;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A fake clock for testing.
+ */
+public final class ManualClock implements Clock {
+  private long currentTimeMillis = 0L;
+
+  @Override
+  public long currentTimeMillis() {
+    return currentTimeMillis;
+  }
+
+  @Override
+  public long nanoTime() {
+    return TimeUnit.MILLISECONDS.toNanos(currentTimeMillis);
+  }
+
+  public void advanceMillis(long time) {
+    currentTimeMillis += time;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java b/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
new file mode 100644
index 0000000..9224b8a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
@@ -0,0 +1,319 @@
+// Copyright 2014 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.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.lang.ref.Reference;
+import java.lang.reflect.Field;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * A helper class for tests providing a simple interface for asserts.
+ */
+public class MoreAsserts {
+
+  public static void assertContainsRegex(String regex, String actual) {
+    assertThat(actual).containsMatch(regex);
+  }
+
+  public static void assertContainsRegex(String msg, String regex, String actual) {
+    assertWithMessage(msg).that(actual).containsMatch(regex);
+  }
+
+  public static void assertNotContainsRegex(String regex, String actual) {
+    assertThat(actual).doesNotContainMatch(regex);
+  }
+
+  public static void assertNotContainsRegex(String msg, String regex, String actual) {
+    assertWithMessage(msg).that(actual).doesNotContainMatch(regex);
+  }
+
+  public static void assertMatchesRegex(String regex, String actual) {
+    assertThat(actual).matches(regex);
+  }
+
+  public static void assertMatchesRegex(String msg, String regex, String actual) {
+    assertWithMessage(msg).that(actual).matches(regex);
+  }
+
+  public static void assertNotMatchesRegex(String regex, String actual) {
+    assertThat(actual).doesNotMatch(regex);
+  }
+
+  public static <T> void assertEquals(T expected, T actual, Comparator<T> comp) {
+    assertThat(comp.compare(expected, actual)).isEqualTo(0);
+  }
+
+  public static <T> void assertContentsAnyOrder(
+      Iterable<? extends T> expected, Iterable<? extends T> actual,
+      Comparator<? super T> comp) {
+    assertThat(actual).hasSize(Iterables.size(expected));
+    int i = 0;
+    for (T e : expected) {
+      for (T a : actual) {
+        if (comp.compare(e, a) == 0) {
+          i++;
+        }
+      }
+    }
+    assertThat(actual).hasSize(i);
+  }
+
+  public static void assertGreaterThanOrEqual(long target, long actual) {
+    assertThat(actual).isAtLeast(target);
+  }
+
+  public static void assertGreaterThanOrEqual(String msg, long target, long actual) {
+    assertWithMessage(msg).that(actual).isAtLeast(target);
+  }
+
+  public static void assertGreaterThan(long target, long actual) {
+    assertThat(actual).isGreaterThan(target);
+  }
+
+  public static void assertGreaterThan(String msg, long target, long actual) {
+    assertWithMessage(msg).that(actual).isGreaterThan(target);
+  }
+
+  public static void assertLessThanOrEqual(long target, long actual) {
+    assertThat(actual).isAtMost(target);
+  }
+
+  public static void assertLessThanOrEqual(String msg, long target, long actual) {
+    assertWithMessage(msg).that(actual).isAtMost(target);
+  }
+
+  public static void assertLessThan(long target, long actual) {
+    assertThat(actual).isLessThan(target);
+  }
+
+  public static void assertLessThan(String msg, long target, long actual) {
+    assertWithMessage(msg).that(actual).isLessThan(target);
+  }
+
+  public static void assertEndsWith(String ending, String actual) {
+    assertThat(actual).endsWith(ending);
+  }
+
+  public static void assertStartsWith(String prefix, String actual) {
+    assertThat(actual).startsWith(prefix);
+  }
+
+  /**
+   * Scans if an instance of given class is strongly reachable from a given
+   * object.
+   * <p>Runs breadth-first search in object reachability graph to check if
+   * an instance of <code>clz</code> can be reached.
+   * <strong>Note:</strong> This method can take a long time if analyzed
+   * data structure spans across large part of heap and may need a lot of
+   * memory.
+   *
+   * @param start object to start the search from
+   * @param clazz class to look for
+   */
+  public static void assertInstanceOfNotReachable(
+      Object start, final Class<?> clazz) {
+    Predicate<Object> p = new Predicate<Object>() {
+      @Override
+      public boolean apply(Object obj) {
+        return clazz.isAssignableFrom(obj.getClass());
+      }
+    };
+    if (isRetained(p, start)) {
+      assert_().fail("Found an instance of " + clazz.getCanonicalName() +
+          " reachable from " + start.toString());
+    }
+  }
+
+  private static final Field NON_STRONG_REF;
+
+  static {
+    try {
+      NON_STRONG_REF = Reference.class.getDeclaredField("referent");
+    } catch (SecurityException e) {
+      throw new RuntimeException(e);
+    } catch (NoSuchFieldException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  static final Predicate<Field> ALL_STRONG_REFS = new Predicate<Field>() {
+    @Override
+    public boolean apply(Field field) {
+      return NON_STRONG_REF.equals(field);
+    }
+  };
+
+  private static boolean isRetained(Predicate<Object> predicate, Object start) {
+    Map<Object, Object> visited = Maps.newIdentityHashMap();
+    visited.put(start, start);
+    Queue<Object> toScan = Lists.newLinkedList();
+    toScan.add(start);
+
+    while (!toScan.isEmpty()) {
+      Object current = toScan.poll();
+      if (current.getClass().isArray()) {
+        if (current.getClass().getComponentType().isPrimitive()) {
+          continue;
+        }
+
+        for (Object ref : (Object[]) current) {
+          if (ref != null) {
+            if (predicate.apply(ref)) {
+              return true;
+            }
+            if (visited.put(ref, ref) == null) {
+              toScan.add(ref);
+            }
+          }
+        }
+      } else {
+        // iterate *all* fields (getFields() returns only accessible ones)
+        for (Class<?> clazz = current.getClass(); clazz != null;
+            clazz = clazz.getSuperclass()) {
+          for (Field f : clazz.getDeclaredFields()) {
+            if (f.getType().isPrimitive() || ALL_STRONG_REFS.apply(f)) {
+              continue;
+            }
+
+            f.setAccessible(true);
+            try {
+              Object ref = f.get(current);
+              if (ref != null) {
+                if (predicate.apply(ref)) {
+                  return true;
+                }
+                if (visited.put(ref, ref) == null) {
+                  toScan.add(ref);
+                }
+              }
+            } catch (IllegalArgumentException e) {
+              throw new IllegalStateException("Error when scanning the heap", e);
+            } catch (IllegalAccessException e) {
+              throw new IllegalStateException("Error when scanning the heap", e);
+            }
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  private static String getClassDescription(Object object) {
+    return object == null
+        ? "null"
+        : ("instance of " + object.getClass().getName());
+  }
+
+  public static String chattyFormat(String message, Object expected, Object actual) {
+    String expectedClass = getClassDescription(expected);
+    String actualClass = getClassDescription(actual);
+
+    return Joiner.on('\n').join((message != null) ? ("\n" + message) : "",
+        "  expected " + expectedClass + ": <" + expected + ">",
+        "  but was " + actualClass + ": <" + actual + ">");
+  }
+
+  public static void assertEqualsUnifyingLineEnds(String expected, String actual) {
+    if (actual != null) {
+      actual = actual.replaceAll(System.getProperty("line.separator"), "\n");
+    }
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  public static void assertContainsWordsWithQuotes(String message,
+      String... strings) {
+    for (String string : strings) {
+      assertTrue(message + " should contain '" + string + "' (with quotes)",
+          message.contains("'" + string + "'"));
+    }
+  }
+
+  public static void assertNonZeroExitCode(int exitCode, String stdout, String stderr) {
+    if (exitCode == 0) {
+      fail("expected non-zero exit code but exit code was 0 and stdout was <"
+          + stdout + "> and stderr was <" + stderr + ">");
+    }
+  }
+
+  public static void assertExitCode(int expectedExitCode,
+      int exitCode, String stdout, String stderr) {
+    if (exitCode != expectedExitCode) {
+      fail(String.format("expected exit code <%d> but exit code was <%d> and stdout was <%s> "
+          + "and stderr was <%s>", expectedExitCode, exitCode, stdout, stderr));
+    }
+  }
+
+  public static void assertStdoutContainsString(String expected, String stdout, String stderr) {
+    if (!stdout.contains(expected)) {
+      fail("expected stdout to contain string <" + expected + "> but stdout was <"
+          + stdout + "> and stderr was <" + stderr + ">");
+    }
+  }
+
+  public static void assertStderrContainsString(String expected, String stdout, String stderr) {
+    if (!stderr.contains(expected)) {
+      fail("expected stderr to contain string <" + expected + "> but stdout was <"
+          + stdout + "> and stderr was <" + stderr + ">");
+    }
+  }
+
+  public static void assertStdoutContainsRegex(String expectedRegex,
+      String stdout, String stderr) {
+    if (!Pattern.compile(expectedRegex).matcher(stdout).find()) {
+      fail("expected stdout to contain regex <" + expectedRegex + "> but stdout was <"
+          + stdout + "> and stderr was <" + stderr + ">");
+    }
+  }
+
+  public static void assertStderrContainsRegex(String expectedRegex,
+      String stdout, String stderr) {
+    if (!Pattern.compile(expectedRegex).matcher(stderr).find()) {
+      fail("expected stderr to contain regex <" + expectedRegex + "> but stdout was <"
+          + stdout + "> and stderr was <" + stderr + ">");
+    }
+  }
+
+  public static Set<String> asStringSet(Iterable<?> collection) {
+    Set<String> set = Sets.newTreeSet();
+    for (Object o : collection) {
+      set.add("\"" + String.valueOf(o) + "\"");
+    }
+    return set;
+  }
+
+  public static <T> void
+  assertSameContents(Iterable<? extends T> expected, Iterable<? extends T> actual) {
+    if (!Sets.newHashSet(expected).equals(Sets.newHashSet(actual))) {
+      fail("got string set: " + asStringSet(actual).toString()
+          + "\nwant: " + asStringSet(expected).toString());
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/Scratch.java b/src/test/java/com/google/devtools/build/lib/testutil/Scratch.java
new file mode 100644
index 0000000..229d2a7
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/Scratch.java
@@ -0,0 +1,150 @@
+// Copyright 2014 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.testutil;
+
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+import java.io.IOException;
+
+/**
+ * Allow tests to easily manage scratch files in a FileSystem.
+ */
+public final class Scratch {
+
+  private final FileSystem fileSystem;
+
+  /**
+   * Create a new ScratchFileSystem using the {@link InMemoryFileSystem}
+   */
+  public Scratch() {
+    this(new InMemoryFileSystem(BlazeClock.instance()));
+  }
+
+  /**
+   * Create a new ScratchFileSystem using the supplied FileSystem.
+   */
+  public Scratch(FileSystem fileSystem) {
+    this.fileSystem = fileSystem;
+  }
+
+  /**
+   * Returns the FileSystem in use.
+   */
+  public FileSystem getFileSystem() {
+    return fileSystem;
+  }
+
+  /**
+   * Create a directory in the scratch filesystem, with the given path name.
+   */
+  public Path dir(String pathName) throws IOException {
+    Path dir = getFileSystem().getPath(pathName);
+    if (!dir.exists()) {
+      FileSystemUtils.createDirectoryAndParents(dir);
+    }
+    if (!dir.isDirectory()) {
+      throw new IOException("Exists, but is not a directory: " + pathName);
+    }
+    return dir;
+  }
+
+  /**
+   * Create a scratch file in the scratch filesystem, with the given pathName,
+   * consisting of a set of lines. The method returns a Path instance for the
+   * scratch file.
+   */
+  public Path file(String pathName, String... lines)
+      throws IOException {
+    Path newFile = file(getFileSystem(), pathName, lines);
+    newFile.setLastModifiedTime(-1L);
+    return newFile;
+  }
+
+  /**
+   * Like {@code scratchFile}, but the file is first deleted if it already
+   * exists.
+   */
+  public Path overwriteFile(String pathName, String... lines) throws IOException {
+    Path oldFile = getFileSystem().getPath(pathName);
+    long newMTime = oldFile.exists() ? oldFile.getLastModifiedTime() + 1 : -1;
+    oldFile.delete();
+    Path newFile = file(getFileSystem(), pathName, lines);
+    newFile.setLastModifiedTime(newMTime);
+    return newFile;
+  }
+
+  /**
+   * Deletes the specified scratch file, using the same specification as {@link Path#delete}.
+   */
+  public boolean deleteFile(String pathName) throws IOException {
+    Path file = getFileSystem().getPath(pathName);
+    return file.delete();
+  }
+
+  /**
+   * Create a scratch file in the given filesystem, with the given pathName,
+   * consisting of a set of lines. The method returns a Path instance for the
+   * scratch file.
+   */
+  public Path file(FileSystem fs, String pathName, String... lines)
+      throws IOException {
+    Path file = newScratchFile(fs, pathName);
+    FileSystemUtils.writeContentAsLatin1(file, linesAsString(lines));
+    return file;
+  }
+
+  /**
+   * Create a scratch file in the given filesystem, with the given pathName,
+   * consisting of a set of lines. The method returns a Path instance for the
+   * scratch file.
+   */
+  public Path file(FileSystem fs, String pathName, byte[] content)
+      throws IOException {
+    Path file = newScratchFile(fs, pathName);
+    FileSystemUtils.writeContent(file, content);
+    return file;
+  }
+
+  /** Creates a new scratch file, ensuring parents exist. */
+  private Path newScratchFile(FileSystem fs, String pathName) throws IOException {
+    Path file = fs.getPath(pathName);
+    Path parentDir = file.getParentDirectory();
+    if (!parentDir.exists()) {
+      FileSystemUtils.createDirectoryAndParents(parentDir);
+    }
+    if (file.exists()) {
+      throw new IOException("Could not create scratch file (file exists) "
+          + pathName);
+    }
+    return file;
+  }
+
+  /**
+   * Converts the lines into a String with linebreaks. Useful for creating
+   * in-memory input for a file, for example.
+   */
+  private static String linesAsString(String... lines) {
+    StringBuilder builder = new StringBuilder();
+    for (String line : lines) {
+      builder.append(line);
+      builder.append('\n');
+    }
+    return builder.toString();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/Suite.java b/src/test/java/com/google/devtools/build/lib/testutil/Suite.java
new file mode 100644
index 0000000..43590d4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/Suite.java
@@ -0,0 +1,86 @@
+// Copyright 2014 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.testutil;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Test annotations used to select which tests to run in a given situation.
+ */
+public enum Suite {
+
+  /**
+   * It's so blazingly fast and lightweight we run it whenever we make any
+   * build.lib change. This size is the default.
+   */
+  SMALL_TESTS,
+
+  /**
+   * It's a bit too slow to run all the time, but it still tests some
+   * unit of functionality. May run external commands such as gcc, for example.
+   */
+  MEDIUM_TESTS,
+
+  /**
+   * I don't even want to think about running this one after every edit,
+   * but I don't mind if the continuous build runs it, and I'm happy to have
+   * it before making a release.
+   */
+  LARGE_TESTS,
+
+  /**
+   * These tests take a long time. They should only ever be run manually and probably from their
+   * own Blaze test target.
+   */
+  ENORMOUS_TESTS;
+
+  /**
+   * Given a class, determine the test size.
+   */
+  public static Suite getSize(Class<?> clazz) {
+    return getAnnotationElementOrDefault(clazz, "size");
+  }
+
+  /**
+   * Given a class, determine the suite it belongs to.
+   */
+  public static String getSuiteName(Class<?> clazz) {
+    return getAnnotationElementOrDefault(clazz, "suite");
+  }
+
+  /**
+   * Given a class, determine if it is flaky.
+   */
+  public static boolean isFlaky(Class<?> clazz) {
+    return getAnnotationElementOrDefault(clazz, "flaky");
+  }
+
+  /**
+   * Returns the value of the given element in the {@link TestSpec} annotation of the given class,
+   * or the default value of that element if the class doesn't have a {@link TestSpec} annotation.
+   */
+  @SuppressWarnings("unchecked")
+  private static <T> T getAnnotationElementOrDefault(Class<?> clazz, String elementName) {
+    TestSpec spec = clazz.getAnnotation(TestSpec.class);
+    try {
+      Method method = TestSpec.class.getMethod(elementName);
+      return spec != null ? (T) method.invoke(spec) : (T) method.getDefaultValue();
+    } catch (NoSuchMethodException e) {
+      throw new IllegalStateException("no such element " + elementName, e);
+    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+      throw new IllegalStateException("can't invoke accessor for element " + elementName, e);
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java
new file mode 100644
index 0000000..d9552ad
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestConstants.java
@@ -0,0 +1,53 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Various constants required by the tests.
+ */
+public class TestConstants {
+  private TestConstants() {
+  }
+
+  /**
+   * A list of all embedded binaries that go into the regular Bazel binary.
+   */
+  public static final ImmutableList<String> EMBEDDED_TOOLS = ImmutableList.of(
+      "build-runfiles",
+      "process-wrapper",
+      "build_interface_so");
+
+
+  /**
+   * Location in the bazel repo where embedded binaries come from.
+   */
+  public static final String EMBEDDED_SCRIPTS_PATH = "DOES-NOT-WORK-YET";
+
+  /**
+   * Directory where we can find bazel's Java tests, relative to a test's runfiles directory.
+   */
+  public static final String JAVATESTS_ROOT = "src/test/java/";
+
+  /**
+   * The directory in InMemoryFileSystem where workspaces created during unit tests reside.
+   */
+  public static final String TEST_WORKSPACE_DIRECTORY = "bazel";
+
+  public static final String TEST_RULE_CLASS_PROVIDER =
+      "com.google.devtools.build.lib.bazel.rules.BazelRuleClassProvider";
+  public static final ImmutableList<String> IGNORED_MESSAGE_PREFIXES = ImmutableList.<String>of();
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestFileOutErr.java b/src/test/java/com/google/devtools/build/lib/testutil/TestFileOutErr.java
new file mode 100644
index 0000000..6f0494f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestFileOutErr.java
@@ -0,0 +1,127 @@
+// Copyright 2014 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.testutil;
+
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.util.io.RecordingOutErr;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An implementation of the FileOutErr that doesn't use a file.
+ * This is useful for tests, as they often test the action directly
+ * and would otherwise have to create files on the vfs.
+ */
+public class TestFileOutErr extends FileOutErr {
+
+  RecordingOutErr recorder;
+
+  public TestFileOutErr(TestFileOutErr arg) {
+    this(arg.getOutputStream(), arg.getErrorStream());
+  }
+
+  public TestFileOutErr() {
+    this(new ByteArrayOutputStream(), new ByteArrayOutputStream());
+  }
+
+  public TestFileOutErr(ByteArrayOutputStream stream) {
+    super(null, null); // This is a pretty brutal overloading - We're just inheriting for the type.
+    recorder = new RecordingOutErr(stream, stream);
+  }
+
+  public TestFileOutErr(ByteArrayOutputStream stream1, ByteArrayOutputStream stream2) {
+    super(null, null); // This is a pretty brutal overloading - We're just inheriting for the type.
+    recorder = new RecordingOutErr(stream1, stream2);
+  }
+
+
+  @Override
+  public Path getOutputFile() {
+    return null;
+  }
+
+  @Override
+  public Path getErrorFile() {
+    return null;
+  }
+
+  @Override
+  public ByteArrayOutputStream getOutputStream() {
+    return recorder.getOutputStream();
+  }
+
+  @Override
+  public ByteArrayOutputStream getErrorStream() {
+    return recorder.getErrorStream();
+  }
+
+  @Override
+  public void printOut(String s) {
+    recorder.printOut(s);
+  }
+
+  @Override
+  public void printErr(String s) {
+    recorder.printErr(s);
+  }
+
+  @Override
+  public String toString() {
+    return recorder.toString();
+  }
+
+  @Override
+  public boolean hasRecordedOutput() {
+    return recorder.hasRecordedOutput();
+  }
+
+  @Override
+  public String outAsLatin1() {
+    return recorder.outAsLatin1();
+  }
+
+  @Override
+  public String errAsLatin1() {
+    return recorder.errAsLatin1();
+  }
+
+  @Override
+  public void dumpOutAsLatin1(OutputStream out) {
+    try {
+      out.write(recorder.getOutputStream().toByteArray());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public void dumpErrAsLatin1(OutputStream out) {
+    try {
+      out.write(recorder.getErrorStream().toByteArray());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public String getRecordedOutput() {
+    return recorder.outAsLatin1() + recorder.errAsLatin1();
+  }
+
+  public void reset() {
+    recorder.reset();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestRuleClassProvider.java b/src/test/java/com/google/devtools/build/lib/testutil/TestRuleClassProvider.java
new file mode 100644
index 0000000..752605c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestRuleClassProvider.java
@@ -0,0 +1,84 @@
+// Copyright 2014 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.testutil;
+
+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_LIST;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.lang.reflect.Method;
+
+/**
+ * Helper class to provide a RuleClassProvider for tests.
+ */
+public class TestRuleClassProvider {
+  private static ConfiguredRuleClassProvider ruleProvider = null;
+
+  /**
+   * Adds all the rule classes supported internally within the build tool to the given builder.
+   */
+  public static void addStandardRules(ConfiguredRuleClassProvider.Builder builder) {
+    try {
+      Class<?> providerClass = Class.forName(TestConstants.TEST_RULE_CLASS_PROVIDER);
+      Method setupMethod = providerClass.getMethod("setup",
+          ConfiguredRuleClassProvider.Builder.class);
+      setupMethod.invoke(null, builder);
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  /**
+   * Return a rule class provider.
+   */
+  public static ConfiguredRuleClassProvider getRuleClassProvider() {
+    if (ruleProvider == null) {
+      ConfiguredRuleClassProvider.Builder builder =
+          new ConfiguredRuleClassProvider.Builder();
+      addStandardRules(builder);
+      builder.addRuleDefinition(TestingDummyRule.class);
+      ruleProvider = builder.build();
+    }
+    return ruleProvider;
+  }
+
+  @BlazeRule(name = "testing_dummy_rule",
+               ancestors = { BaseRuleClasses.RuleBase.class },
+               // Instantiated only in tests
+               factoryClass = UnknownRuleConfiguredTarget.class)
+  public static final class TestingDummyRule implements RuleDefinition {
+    @Override
+    public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+      return builder
+          .setUndocumented()
+          .add(attr("srcs", LABEL_LIST).allowedFileTypes(FileTypeSet.ANY_FILE))
+          .add(attr("outs", OUTPUT_LIST))
+          .add(attr("dummystrings", STRING_LIST))
+          .add(attr("dummyinteger", INTEGER))
+          .build();
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestSpec.java b/src/test/java/com/google/devtools/build/lib/testutil/TestSpec.java
new file mode 100644
index 0000000..316169c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestSpec.java
@@ -0,0 +1,48 @@
+// Copyright 2014 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.testutil;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation class which we use to attach a little meta data to test
+ * classes. For now, we use this to attach a {@link Suite}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TestSpec {
+
+  /**
+   * The size of the specified test, in terms of its resource consumption and
+   * execution time.
+   */
+  Suite size() default Suite.SMALL_TESTS;
+
+  /**
+   * The name of the suite to which this test belongs.  Useful for creating
+   * test suites organised by function.
+   */
+  String suite() default "";
+
+  /**
+   * If the test will pass consistently without outside changes.
+   * This should be fixed as soon as possible.
+   */
+  boolean flaky() default false;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestSuiteBuilder.java b/src/test/java/com/google/devtools/build/lib/testutil/TestSuiteBuilder.java
new file mode 100644
index 0000000..af90c52
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestSuiteBuilder.java
@@ -0,0 +1,139 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+import junit.framework.TestCase;
+
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Modifier;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * A collector for test classes, for both JUnit 3 and 4. To be used in combination with {@link
+ * CustomSuite}.
+ */
+public final class TestSuiteBuilder {
+
+  private Set<Class<?>> testClasses = Sets.newTreeSet(new TestClassNameComparator());
+  private Predicate<Class<?>> matchClassPredicate = Predicates.alwaysTrue();
+
+  /**
+   * Adds the tests found (directly) in class {@code c} to the set of tests
+   * this builder will search.
+   */
+  public TestSuiteBuilder addTestClass(Class<?> c) {
+    testClasses.add(c);
+    return this;
+  }
+
+  /**
+   * Adds all the test classes (top-level or nested) found in package
+   * {@code pkgName} or its subpackages to the set of tests this builder will
+   * search.
+   */
+  public TestSuiteBuilder addPackageRecursive(String pkgName) {
+    for (Class<?> c : getClassesRecursive(pkgName)) {
+      addTestClass(c);
+    }
+    return this;
+  }
+
+  private Set<Class<?>> getClassesRecursive(String pkgName) {
+    Set<Class<?>> result = new LinkedHashSet<>();
+    for (Class<?> clazz : Classpath.findClasses(pkgName)) {
+      if (isTestClass(clazz)) {
+        result.add(clazz);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Specifies a predicate returns false for classes we want to exclude.
+   */
+  public TestSuiteBuilder matchClasses(Predicate<Class<?>> predicate) {
+    matchClassPredicate = predicate;
+    return this;
+  }
+
+  /**
+   * Creates and returns a TestSuite containing the tests from the given
+   * classes and/or packages which matched the given tags.
+   */
+  public Set<Class<?>> create() {
+    Set<Class<?>> result = new LinkedHashSet<>();
+    // We have some cases where the resulting test suite is empty, which some of our test
+    // infrastructure treats as an error.
+    result.add(TautologyTest.class);
+    for (Class<?> testClass : Iterables.filter(testClasses, matchClassPredicate)) {
+      result.add(testClass);
+    }
+    return result;
+  }
+
+  /**
+   * Determines if a given class is a test class.
+   *
+   * @param container class to test
+   * @return <code>true</code> if the test is a test class.
+   */
+  private static boolean isTestClass(Class<?> container) {
+    return (isJunit4Test(container) || isJunit3Test(container))
+        && !isSuite(container)
+        && Modifier.isPublic(container.getModifiers())
+        && !Modifier.isAbstract(container.getModifiers());
+  }
+
+  private static boolean isJunit4Test(Class<?> container) {
+    return container.getAnnotation(RunWith.class) != null;
+  }
+
+  private static boolean isJunit3Test(Class<?> container) {
+    return TestCase.class.isAssignableFrom(container);
+  }
+
+  /**
+   * Classes that have a {@code RunWith} annotation for {@link ClasspathSuite} or {@link
+   * CustomSuite} are automatically excluded to avoid picking up the suite class itself.
+   */
+  private static boolean isSuite(Class<?> container) {
+    RunWith runWith = container.getAnnotation(RunWith.class);
+    return (runWith != null)
+        && ((runWith.value() == ClasspathSuite.class) || (runWith.value() == CustomSuite.class));
+  }
+
+  private static class TestClassNameComparator implements Comparator<Class<?>> {
+    @Override
+    public int compare(Class<?> o1, Class<?> o2) {
+      return o1.getName().compareTo(o2.getName());
+    }
+  }
+
+  /**
+   * A test that does nothing and always passes. We have some cases where an empty test suite is
+   * treated as an error, so we use this test to make sure that the test suite is always non-empty.
+   */
+  public static class TautologyTest extends TestCase {
+    public void testThatNothingHappens() {
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestThread.java b/src/test/java/com/google/devtools/build/lib/testutil/TestThread.java
new file mode 100644
index 0000000..e043025
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestThread.java
@@ -0,0 +1,66 @@
+// Copyright 2014 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.testutil;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+/**
+ * Test thread implementation that allows the use of assertions within
+ * spawned threads.
+ *
+ * Main test method must call {@link TestThread#joinAndAssertState(long)}
+ * for each spawned test thread.
+ */
+public abstract class TestThread extends Thread {
+  Throwable testException = null;
+  boolean isSucceeded = false;
+
+  /**
+   * Specific test thread implementation overrides this method.
+   */
+  abstract public void runTest() throws Exception;
+
+  @Override public final void run() {
+    try {
+      runTest();
+      isSucceeded = true;
+    } catch (Exception e) {
+      testException = e;
+    } catch (AssertionError e) {
+      testException = e;
+    }
+  }
+
+  /**
+   * Joins test thread (waiting specified number of ms) and validates that
+   * it has been completed successfully.
+   */
+  public void joinAndAssertState(long timeout) throws InterruptedException {
+    join(timeout);
+    Throwable exception = this.testException;
+    if (isAlive()) {
+      exception = new AssertionError (
+          "Test thread " + getName() + " is still alive");
+      exception.setStackTrace(getStackTrace());
+    }
+    if(exception != null) {
+      AssertionError error = new AssertionError("Test thread " + getName() + " failed to execute");
+      error.initCause(exception);
+      throw error;
+    }
+    assertWithMessage("Test thread " + getName() + " has not run successfully").that(isSucceeded)
+        .isTrue();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/TestUtils.java b/src/test/java/com/google/devtools/build/lib/testutil/TestUtils.java
new file mode 100644
index 0000000..2ee5972
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/TestUtils.java
@@ -0,0 +1,152 @@
+// Copyright 2014 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.testutil;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * Some static utility functions for testing.
+ */
+public class TestUtils {
+  public static final ThreadPoolExecutor POOL =
+    (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
+
+  public static final UUID ZERO_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
+
+  /**
+   * Wait until the {@link System#currentTimeMillis} / 1000 advances.
+   *
+   * This method takes 0-1000ms to run, 500ms on average.
+   */
+  public static void advanceCurrentTimeSeconds() throws InterruptedException {
+    long currentTimeSeconds = System.currentTimeMillis() / 1000;
+    do {
+      Thread.sleep(50);
+    } while (currentTimeSeconds == System.currentTimeMillis() / 1000);
+  }
+
+  public static ThreadPoolExecutor getPool() {
+    return POOL;
+  }
+
+  public static String tmpDir() {
+    return tmpDirFile().getAbsolutePath();
+  }
+
+  static String getUserValue(String key) {
+    String value = System.getProperty(key);
+    if (value == null) {
+      value = System.getenv(key);
+    }
+    return value;
+  }
+
+  public static File tmpDirFile() {
+    File tmpDir;
+
+    // Flag value specified in environment?
+    String tmpDirStr = getUserValue("TEST_TMPDIR");
+
+    if (tmpDirStr != null && tmpDirStr.length() > 0) {
+      tmpDir = new File(tmpDirStr);
+    } else {
+      // Fallback default $TEMP/$USER/tmp/$TESTNAME
+      String baseTmpDir = System.getProperty("java.io.tmpdir");
+      tmpDir = new File(baseTmpDir).getAbsoluteFile();
+
+      // .. Add username
+      String username = System.getProperty("user.name");
+      username = username.replace('/', '_');
+      username = username.replace('\\', '_');
+      username = username.replace('\000', '_');
+      tmpDir = new File(tmpDir, username);
+      tmpDir = new File(tmpDir, "tmp");
+    }
+
+    // Ensure tmpDir exists
+    if (!tmpDir.isDirectory()) {
+      tmpDir.mkdirs();
+    }
+    return tmpDir;
+  }
+
+  public static File makeTempDir() throws IOException {
+    File dir = File.createTempFile(TestUtils.class.getName(), ".temp", tmpDirFile());
+    if (!dir.delete()) {
+      throw new IOException("Cannot remove a temporary file " + dir);
+    }
+    if (!dir.mkdir()) {
+      throw new IOException("Cannot create a temporary directory " + dir);
+    }
+    return dir;
+  }
+
+  public static int getRandomSeed() {
+    // Default value if not running under framework
+    int randomSeed = 301;
+
+    // Value specified in environment by framework?
+    String value = getUserValue("TEST_RANDOM_SEED");
+    if ((value != null) && (value.length() > 0)) {
+      try {
+        randomSeed = Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        // throw new AssertionError("TEST_RANDOM_SEED must be an integer");
+        throw new RuntimeException("TEST_RANDOM_SEED must be an integer");
+      }
+    }
+
+    return randomSeed;
+  }
+
+  public static byte[] serializeObject(Object obj) throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    try (ObjectOutputStream objectStream = new ObjectOutputStream(outputStream)) {
+      objectStream.writeObject(obj);
+    }
+    return outputStream.toByteArray();
+  }
+
+  public static Object deserializeObject(byte[] buf) throws IOException, ClassNotFoundException {
+    try (ObjectInputStream inStream = new ObjectInputStream(new ByteArrayInputStream(buf))) {
+      return inStream.readObject();
+    }
+  }
+
+  /**
+   * Timeouts for asserting that an arbitrary event occurs eventually.
+   *
+   * <p>In general, it's not appropriate to use a small constant timeout for an arbitrary
+   * computation since there is no guarantee that a snippet of code will execute within a given
+   * amount of time - you are at the mercy of the jvm, your machine, and your OS. In theory we
+   * could try to take all of these factors into account but instead we took the simpler and
+   * obviously correct approach of not having timeouts.
+   *
+   * <p>If a test that uses these timeout values is failing due to a "timeout" at the
+   * 'blaze test' level, it could be because of a legitimate deadlock that would have been caught
+   * if the timeout values below were small. So you can rule out such a deadlock by changing these
+   * values to small numbers (also note that the --test_timeout blaze flag may be useful).
+   */
+  public static final long WAIT_TIMEOUT_MILLISECONDS = Long.MAX_VALUE;
+  public static final long WAIT_TIMEOUT_SECONDS = WAIT_TIMEOUT_MILLISECONDS / 1000;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/UnknownRuleConfiguredTarget.java b/src/test/java/com/google/devtools/build/lib/testutil/UnknownRuleConfiguredTarget.java
new file mode 100644
index 0000000..d3e5457
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/testutil/UnknownRuleConfiguredTarget.java
@@ -0,0 +1,59 @@
+// Copyright 2014 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.testutil;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.FailAction;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * A null implementation of ConfiguredTarget for rules we don't know how to build.
+ */
+public class UnknownRuleConfiguredTarget implements RuleConfiguredTargetFactory {
+
+  @Override
+  public ConfiguredTarget create(RuleContext context)  {
+    // TODO(bazel-team): (2009) why isn't this an error?  It would stop the build more promptly...
+    context.ruleWarning("cannot build " + context.getRule().getRuleClass() + " rules");
+
+    ImmutableList<Artifact> outputArtifacts = context.getOutputArtifacts();
+    NestedSet<Artifact> filesToBuild;
+    if (outputArtifacts.isEmpty()) {
+      // Gotta build *something*...
+      filesToBuild = NestedSetBuilder.create(Order.STABLE_ORDER,
+          context.createOutputArtifact());
+    } else {
+      filesToBuild = NestedSetBuilder.wrap(Order.STABLE_ORDER, outputArtifacts);
+    }
+
+    Rule rule = context.getRule();
+    context.registerAction(new FailAction(context.getActionOwner(),
+        filesToBuild, "cannot build " + rule.getRuleClass() + " rules such as " + rule.getLabel()));
+    return new RuleConfiguredTargetBuilder(context)
+        .setFilesToBuild(filesToBuild)
+        .add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY))
+        .build();
+  }
+}