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();
+ }
+}