Open source more analysis tests.

--
MOS_MIGRATED_REVID=91909389
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java b/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java
new file mode 100644
index 0000000..1f24cf4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java
@@ -0,0 +1,175 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.analysis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
+import com.google.devtools.build.lib.analysis.util.TestAspects;
+import com.google.devtools.build.lib.analysis.util.TestAspects.AspectRequiringRule;
+import com.google.devtools.build.lib.packages.AspectDefinition;
+import com.google.devtools.build.lib.packages.AspectFactory;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import javax.annotation.Nullable;
+
+/**
+ * Tests for {@link DependencyResolver}.
+ *
+ * <p>These use custom rules so that all usual and unusual cases related to aspect processing can
+ * be tested.
+ *
+ * <p>It would be nicer is we didn't have a Skyframe executor, if we didn't have that, we'd need a
+ * way to create a configuration, a package manager and a whole lot of other things, so it's just
+ * easier this way.
+ */
+@RunWith(JUnit4.class)
+public class DependencyResolverTest extends AnalysisTestCase {
+  private DependencyResolver dependencyResolver;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    dependencyResolver = new DependencyResolver() {
+      @Override
+      protected void invalidVisibilityReferenceHook(TargetAndConfiguration node, Label label) {
+        throw new IllegalStateException();
+      }
+
+      @Override
+      protected void invalidPackageGroupReferenceHook(TargetAndConfiguration node, Label label) {
+        throw new IllegalStateException();
+      }
+
+      @Nullable
+      @Override
+      protected Target getTarget(Label label) throws NoSuchThingException {
+        try {
+          return packageManager.getTarget(reporter, label);
+        } catch (InterruptedException e) {
+          throw new IllegalStateException(e);
+        }
+      }
+    };
+  }
+
+  @Override
+  @After
+  public void tearDown() throws Exception {
+    super.tearDown();
+  }
+
+  private void pkg(String name, String... contents) throws Exception {
+    scratchFile("" + name + "/BUILD", contents);
+  }
+
+  @SafeVarargs
+  private final void setRules(RuleDefinition... rules) throws Exception {
+    ConfiguredRuleClassProvider.Builder builder =
+        new ConfiguredRuleClassProvider.Builder();
+    TestRuleClassProvider.addStandardRules(builder);
+    for (RuleDefinition rule : rules) {
+      builder.addRuleDefinition(rule);
+    }
+
+    useRuleClassProvider(builder.build());
+    update();
+  }
+
+  private ListMultimap<Attribute, Dependency> dependentNodeMap(
+      String targetName, Class<? extends ConfiguredAspectFactory> aspect) throws Exception {
+    AspectDefinition aspectDefinition = aspect == null
+        ? null
+        : AspectFactory.Util.create(aspect).getDefinition();
+    Target target = packageManager.getTarget(reporter, Label.parseAbsolute(targetName));
+    return dependencyResolver.dependentNodeMap(
+        new TargetAndConfiguration(target, getTargetConfiguration()),
+        aspectDefinition,
+        ImmutableSet.<ConfigMatchingProvider>of());
+  }
+
+  @SafeVarargs
+  private final void assertDep(
+      ListMultimap<Attribute, Dependency> dependentNodeMap,
+      String attrName,
+      String dep,
+      Class<? extends AspectFactory<?, ?, ?>>... aspects) {
+    Attribute attr = null;
+    for (Attribute candidate : dependentNodeMap.keySet()) {
+      if (candidate.getName().equals(attrName)) {
+        attr = candidate;
+        break;
+      }
+    }
+
+    assertNotNull("Attribute '" + attrName + "' not found", attr);
+    Dependency dependency = null;
+    for (Dependency candidate : dependentNodeMap.get(attr)) {
+      if (candidate.getLabel().toString().equals(dep)) {
+        dependency = candidate;
+        break;
+      }
+    }
+
+    assertNotNull("Dependency '" + dep + "' on attribute '" + attrName + "' not found", dependency);
+    assertThat(dependency.getAspects()).containsExactly((Object[]) aspects);
+  }
+
+  @Test
+  public void hasAspectsRequiredByRule() throws Exception {
+    setRules(new AspectRequiringRule(), new TestAspects.BaseRule());
+    pkg("a",
+        "aspect(name='a', foo=[':b'])",
+        "aspect(name='b', foo=[])");
+    ListMultimap<Attribute, Dependency> map = dependentNodeMap("//a:a", null);
+    assertDep(map, "foo", "//a:b", TestAspects.SimpleAspect.class);
+  }
+
+  @Test
+  public void hasAspectsRequiredByAspect() throws Exception {
+    setRules(new TestAspects.BaseRule(), new TestAspects.SimpleRule());
+    pkg("a",
+        "simple(name='a', foo=[':b'])",
+        "simple(name='b', foo=[])");
+    ListMultimap<Attribute, Dependency> map =
+        dependentNodeMap("//a:a", TestAspects.AttributeAspect.class);
+    assertDep(map, "foo", "//a:b", TestAspects.AttributeAspect.class);
+  }
+
+  @Test
+  public void hasAspectDependencies() throws Exception {
+    setRules(new TestAspects.BaseRule());
+    pkg("a", "base(name='a')");
+    pkg("extra", "base(name='extra')");
+    ListMultimap<Attribute, Dependency> map =
+        dependentNodeMap("//a:a", TestAspects.ExtraAttributeAspect.class);
+    assertDep(map, "$dep", "//extra:extra");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/LabelExpanderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/LabelExpanderTest.java
new file mode 100644
index 0000000..708408f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/LabelExpanderTest.java
@@ -0,0 +1,257 @@
+// Copyright 2010-2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Map;
+
+/**
+ * Tests for {@link LabelExpander}.
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+public class LabelExpanderTest extends BuildViewTestCase {
+  /**
+   * A dummy target that resolves labels and receives errors.
+   */
+  private ConfiguredTarget dummyTarget;
+
+  /**
+   * Artifacts generated by {@code dummyTarget} identified by their
+   * root-relative paths; to be used for mock label-to-artifact mappings.
+   */
+  private Map<String, Artifact> artifactsByName;
+
+  /**
+   * All characters that the heuristic considers to be part of a target.
+   * This is a subset of the allowed label characters. The ones left out may
+   * have a special meaning during expression expansion:
+   *
+   * <ul>
+   *   <li>comma (",") - may separate labels
+   *   <li>equals sign ("=") - may separate labels
+   *   <li>colon (":") - can only appear in labels, not in target names
+   * </ul>
+   */
+  private static final String allowedChars = "_/.-+" + PathFragment.SEPARATOR_CHAR
+      + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+  // Helper methods -----------------------------------------------------------
+
+  private void setupDummy() throws Exception {
+    dummyTarget = scratchConfiguredTarget(
+        "foo", "foo",
+        "filegroup(name = 'foo',",
+        "          srcs = ['x1','x2','bar/x3', '" + allowedChars + "', 'xx11', 'x11', 'xx1'])");
+    collectArtifacts();
+  }
+
+  /**
+   * Collects all generated mock artifacts for {@code dummyTarget} and assigns
+   * the result to {@code artifactsByName}.
+   */
+  private void collectArtifacts() {
+    ImmutableMap.Builder<String, Artifact> builder = ImmutableMap.builder();
+    for (Artifact artifact : getFilesToBuild(dummyTarget)) {
+      builder.put(artifact.getRootRelativePath().toString(), artifact);
+    }
+    artifactsByName = builder.build();
+  }
+
+  /**
+   * Gets a generated artifact object for a target in package "foo" from {@code
+   * artifactsByName}.
+   */
+  private Artifact artifactFor(String targetName) {
+    return artifactsByName.get("foo/" + targetName);
+  }
+
+  /**
+   * Creates fake label in package "foo".
+   */
+  private static Label labelFor(String targetName) throws SyntaxException {
+    return Label.create("foo", targetName);
+  }
+
+  /**
+   * Asserts that an expansion with a given mapping produces the expected
+   * results.
+   */
+  private void assertExpansion(String expectedResult, String expressionToExpand,
+      Map<Label, Iterable<Artifact>> mapping) throws Exception {
+    assertEquals(expectedResult,
+        LabelExpander.expand(expressionToExpand, mapping, dummyTarget.getLabel()));
+  }
+
+  /**
+   * Asserts that an expansion with an empty mapping produces the expected
+   * results.
+   */
+  private void assertExpansion(String expected, String original) throws Exception {
+    assertExpansion(expected, original, ImmutableMap.<Label, Iterable<Artifact>>of());
+  }
+
+  // Actual tests -------------------------------------------------------------
+
+  /**
+   * Tests that if no mapping is specified, then strings expand to themselves.
+   */
+  public void testStringExpandsToItselfWhenNoMappingSpecified() throws Exception {
+    setupDummy();
+    assertExpansion("", null);
+    assertExpansion("cmd", "cmd");
+    assertExpansion("//x:y,:z,w", "//x:y,:z,w");
+    assertExpansion(allowedChars, allowedChars);
+  }
+
+  /**
+   * Tests that in case of a one-to-one label-to-artifact mapping the expansion
+   * produces the expected results.
+   */
+  public void testExpansion() throws Exception {
+    setupDummy();
+    assertExpansion("foo/x1", "x1", ImmutableMap.<Label, Iterable<Artifact>>of(
+        labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+
+    assertExpansion("foo/x1", ":x1", ImmutableMap.<Label, Iterable<Artifact>>of(
+        labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+
+    assertExpansion("foo/x1", "//foo:x1", ImmutableMap.<Label, Iterable<Artifact>>of(
+        labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+  }
+
+  /**
+   * Tests that label extraction works as expected - disallowed label characters
+   * are resolved to themselves.
+   */
+  public void testLabelExtraction() throws Exception {
+    setupDummy();
+    assertExpansion("(foo/" + allowedChars + ")", "(//foo:" + allowedChars + ")",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor(allowedChars), ImmutableList.of(artifactFor(allowedChars))));
+
+    assertExpansion("foo/x1,foo/x2=foo/bar/x3", "x1,x2=bar/x3",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x2"), ImmutableList.of(artifactFor("x2")),
+            labelFor("bar/x3"), ImmutableList.of(artifactFor("bar/x3"))));
+  }
+
+  /**
+   * Tests that an exception is thrown when the mapping is not one-to-one.
+   */
+  public void testThrowsWhenMappingIsNotOneToOne() throws Exception {
+    setupDummy();
+    try {
+      LabelExpander.expand("x1", ImmutableMap.<Label, Iterable<Artifact>>of(
+          labelFor("x1"), ImmutableList.<Artifact>of()), dummyTarget.getLabel());
+
+      fail("Expected an exception.");
+    } catch (LabelExpander.NotUniqueExpansionException nuee) {
+      // was expected
+    }
+
+    try {
+      LabelExpander.expand("x1", ImmutableMap.<Label, Iterable<Artifact>>of(
+          labelFor("x1"), ImmutableList.of(artifactFor("x1"), artifactFor("x2"))),
+          dummyTarget.getLabel());
+
+      fail("Expected an exception.");
+    } catch (LabelExpander.NotUniqueExpansionException nuee) {
+      // was expected
+    }
+  }
+
+  /**
+   * Tests expanding labels that result in a SyntaxException.
+   */
+  public void testIllFormedLabels() throws Exception {
+    setupDummy();
+    assertExpansion("x1:x2:x3", "x1:x2:x3",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x2"), ImmutableList.of(artifactFor("x2")),
+            labelFor("bar/x3"), ImmutableList.of(artifactFor("bar/x3"))));
+
+    assertExpansion("foo://x1 x1/../x2", "foo://x1 x1/../x2",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x2"), ImmutableList.of(artifactFor("x2")),
+            labelFor("bar/x3"), ImmutableList.of(artifactFor("bar/x3"))));
+
+    assertExpansion("//foo:/x1", "//foo:/x1",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+
+    assertExpansion("//foo:../x1", "//foo:../x1",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+
+    assertExpansion("//foo:x1/../x2", "//foo:x1/../x2",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x2"), ImmutableList.of(artifactFor("x2"))));
+
+    assertExpansion("//foo:x1/./x2", "//foo:x1/./x2",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x2"), ImmutableList.of(artifactFor("x2"))));
+
+    assertExpansion("//foo:x1//x2", "//foo:x1//x2",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x2"), ImmutableList.of(artifactFor("x2"))));
+
+    assertExpansion("//foo:x1/..", "//foo:x1/..",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+
+    assertExpansion("//foo:x1/", "//foo:x1/",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+
+    assertExpansion(":", ":");
+  }
+
+  /**
+   * Tests that label parsing is greedy (always extracting the longest
+   * possible label). This means that if a label is a substring of another
+   * label, it should not be expanded but be treated as part of the longer one.
+   */
+  public void testLabelIsSubstringOfValidLabel() throws Exception {
+    setupDummy();
+    assertExpansion("x3=foo/bar/x3", "x3=bar/x3",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("bar/x3"), ImmutableList.of(artifactFor("bar/x3"))));
+
+    assertExpansion("foo/x1,foo/x11,foo/xx1,foo/xx11", "x1,x11,xx1,xx11",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1")),
+            labelFor("x11"), ImmutableList.of(artifactFor("x11")),
+            labelFor("xx1"), ImmutableList.of(artifactFor("xx1")),
+            labelFor("xx11"), ImmutableList.of(artifactFor("xx11"))));
+
+    assertExpansion("//x1", "//x1",
+        ImmutableMap.<Label, Iterable<Artifact>>of(
+            labelFor("x1"), ImmutableList.of(artifactFor("x1"))));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/MakeVariableExpanderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/MakeVariableExpanderTest.java
new file mode 100644
index 0000000..dbdc1c1
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/MakeVariableExpanderTest.java
@@ -0,0 +1,149 @@
+// Copyright 2006-2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.analysis;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for the {@link MakeVariableExpander}, which expands variable references of the form
+ * <code>"$x"</code> and <code>"$(foo)"</code> into their corresponding values.
+ */
+@RunWith(JUnit4.class)
+public class MakeVariableExpanderTest {
+
+  private MakeVariableExpander.Context context;
+
+  private Map<String, String> vars = new HashMap<>();
+
+  @Before
+  public void setUp() throws Exception {
+    context = new MakeVariableExpander.Context() {
+        @Override
+        public String lookupMakeVariable(String name)
+            throws MakeVariableExpander.ExpansionException {
+          // Not a Make variable. Let the shell handle the expansion.
+          if (name.startsWith("$")) {
+            return name;
+          }
+          if (!vars.containsKey(name)) {
+            throw new MakeVariableExpander.ExpansionException("$(" + name + ") not defined");
+          }
+          return vars.get(name);
+        }
+      };
+
+    vars.put("SRCS", "src1 src2");
+  }
+
+  private void assertExpansionEquals(String expected, String cmd)
+      throws MakeVariableExpander.ExpansionException {
+    assertEquals(expected, MakeVariableExpander.expand(cmd, context));
+  }
+
+  private void assertExpansionFails(String expectedErrorSuffix, String cmd) {
+    try {
+      MakeVariableExpander.expand(cmd, context);
+      fail("Expansion of " + cmd + " didn't fail as expected");
+    } catch (Exception e) {
+      assertThat(e).hasMessage(expectedErrorSuffix);
+    }
+  }
+
+  @Test
+  public void testExpansion() throws Exception {
+    vars.put("<", "src1");
+    vars.put("OUTS", "out1 out2");
+    vars.put("@", "out1");
+    vars.put("^", "src1 src2 dep1 dep2");
+    vars.put("@D", "outdir");
+    vars.put("BINDIR", "bindir");
+
+    assertExpansionEquals("src1 src2", "$(SRCS)");
+    assertExpansionEquals("src1", "$<");
+    assertExpansionEquals("out1 out2", "$(OUTS)");
+    assertExpansionEquals("out1", "$(@)");
+    assertExpansionEquals("out1", "$@");
+    assertExpansionEquals("out1,", "$@,");
+
+    assertExpansionEquals("src1 src2 out1 out2", "$(SRCS) $(OUTS)");
+
+    assertExpansionEquals("cmd", "cmd");
+    assertExpansionEquals("cmd src1 src2,", "cmd $(SRCS),");
+    assertExpansionEquals("label1 src1 src2,", "label1 $(SRCS),");
+    assertExpansionEquals(":label1 src1 src2,", ":label1 $(SRCS),");
+
+    // Note: $(location x) is considered an undefined variable;
+    assertExpansionFails("$(location label1) not defined",
+                         "$(location label1), $(SRCS),");
+  }
+
+  @Test
+  public void testRecursiveExpansion() throws Exception {
+    // Expansion is recursive: $(recursive) -> $(SRCS) -> "src1 src2"
+    vars.put("recursive", "$(SRCS)");
+    assertExpansionEquals("src1 src2", "$(recursive)");
+
+    // Recursion does not span expansion boundaries:
+    // $(recur2a)$(recur2b) --> "$" + "(SRCS)"  --/--> "src1 src2"
+    vars.put("recur2a", "$$");
+    vars.put("recur2b", "(SRCS)");
+    assertExpansionEquals("$(SRCS)", "$(recur2a)$(recur2b)");
+  }
+
+  @Test
+  public void testInfiniteRecursionFailsGracefully() throws Exception {
+    vars.put("infinite", "$(infinite)");
+    assertExpansionFails("potentially unbounded recursion during expansion "
+                         + "of '$(infinite)'",
+                         "$(infinite)");
+
+    vars.put("black", "$(white)");
+    vars.put("white", "$(black)");
+    assertExpansionFails("potentially unbounded recursion during expansion "
+                         + "of '$(black)'",
+                         "$(white) is the new $(black)");
+  }
+
+  @Test
+  public void testErrors() throws Exception {
+    assertExpansionFails("unterminated variable reference", "$(SRCS");
+    assertExpansionFails("unterminated $", "$");
+
+    String suffix = "instead for \"Make\" variables, or escape the '$' as '$$' if you intended "
+        + "this for the shell";
+    assertExpansionFails("'$file' syntax is not supported; use '$(file)' " + suffix,
+                         "for file in a b c;do echo $file;done");
+    assertExpansionFails("'${file%:.*8}' syntax is not supported; use '$(file%:.*8)' " + suffix,
+                         "${file%:.*8}");
+  }
+
+  @Test
+  public void testShellVariables() throws Exception {
+    assertExpansionEquals("for file in a b c;do echo $file;done",
+        "for file in a b c;do echo $$file;done");
+    assertExpansionEquals("${file%:.*8}", "$${file%:.*8}");
+    assertExpansionFails("$(basename file) not defined", "$(basename file)");
+    assertExpansionEquals("$(basename file)", "$$(basename file)");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java b/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java
new file mode 100644
index 0000000..107b6fc
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/RunfilesTest.java
@@ -0,0 +1,112 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.analysis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Test for {@link Runfiles}.
+ */
+public class RunfilesTest extends FoundationTestCase {
+
+  private void checkWarning() {
+    assertContainsEvent("obscured by a -> /workspace/a");
+    assertEquals("Runfiles.filterListForObscuringSymlinks should have warned once",
+                 1, eventCollector.count());
+    assertEquals(EventKind.WARNING, Iterables.getOnlyElement(eventCollector).getKind());
+  }
+
+  public void testFilterListForObscuringSymlinksCatchesBadObscurer() throws Exception {
+    Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
+    PathFragment pathA = new PathFragment("a");
+    Root root = Root.asSourceRoot(scratchFS().getPath("/workspace"));
+    Artifact artifactA = new Artifact(new PathFragment("a"), root);
+    obscuringMap.put(pathA, artifactA);
+    obscuringMap.put(new PathFragment("a/b"), new Artifact(new PathFragment("c/b"),
+        root));
+    assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap).entrySet())
+        .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
+    checkWarning();
+  }
+
+  public void testFilterListForObscuringSymlinksCatchesBadGrandParentObscurer() throws Exception {
+    Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
+    PathFragment pathA = new PathFragment("a");
+    Root root = Root.asSourceRoot(scratchFS().getPath("/workspace"));
+    Artifact artifactA = new Artifact(new PathFragment("a"),
+                                          root);
+    obscuringMap.put(pathA, artifactA);
+    obscuringMap.put(new PathFragment("a/b/c"), new Artifact(new PathFragment("b/c"),
+                                                         root));
+    assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap).entrySet())
+        .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
+    checkWarning();
+  }
+
+  public void testFilterListForObscuringSymlinksCatchesBadObscurerNoListener() throws Exception {
+    Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
+    PathFragment pathA = new PathFragment("a");
+    Root root = Root.asSourceRoot(scratchFS().getPath("/workspace"));
+    Artifact artifactA = new Artifact(new PathFragment("a"),
+                                          root);
+    obscuringMap.put(pathA, artifactA);
+    obscuringMap.put(new PathFragment("a/b"), new Artifact(new PathFragment("c/b"),
+                                                         root));
+    assertThat(Runfiles.filterListForObscuringSymlinks(null, null, obscuringMap).entrySet())
+        .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
+  }
+
+  public void testFilterListForObscuringSymlinksIgnoresOkObscurer() throws Exception {
+    Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
+    PathFragment pathA = new PathFragment("a");
+    Root root = Root.asSourceRoot(scratchFS().getPath("/workspace"));
+    Artifact artifactA = new Artifact(new PathFragment("a"),
+                                          root);
+    obscuringMap.put(pathA, artifactA);
+    obscuringMap.put(new PathFragment("a/b"), new Artifact(new PathFragment("a/b"),
+                                                         root));
+
+    assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap).entrySet())
+        .containsExactly(Maps.immutableEntry(pathA, artifactA)).inOrder();
+    assertNoEvents();
+  }
+
+  public void testFilterListForObscuringSymlinksNoObscurers() throws Exception {
+    Map<PathFragment, Artifact> obscuringMap = new HashMap<>();
+    PathFragment pathA = new PathFragment("a");
+    Root root = Root.asSourceRoot(scratchFS().getPath("/workspace"));
+    Artifact artifactA = new Artifact(new PathFragment("a"),
+                                          root);
+    obscuringMap.put(pathA, artifactA);
+    PathFragment pathBC = new PathFragment("b/c");
+    Artifact artifactBC = new Artifact(new PathFragment("a/b"),
+                                       root);
+    obscuringMap.put(pathBC, artifactBC);
+    assertThat(Runfiles.filterListForObscuringSymlinks(reporter, null, obscuringMap)
+                          .entrySet()).containsExactly(Maps.immutableEntry(pathA, artifactA),
+        Maps.immutableEntry(pathBC, artifactBC));
+    assertNoEvents();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/UtilTest.java b/src/test/java/com/google/devtools/build/lib/analysis/UtilTest.java
new file mode 100644
index 0000000..ce3558a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/UtilTest.java
@@ -0,0 +1,36 @@
+// Copyright 2006-2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.analysis;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for the Util helper class.
+ */
+@RunWith(JUnit4.class)
+public class UtilTest {
+
+  @Test
+  public void testContainsHyphen() throws Exception {
+    assertTrue(Util.containsHyphen(new PathFragment("foo/bar/with-hyphen")));
+    assertFalse(Util.containsHyphen(new PathFragment("foo/bar/no/hyphen")));
+  }
+}