Make "blaze test <alias target>" run the test.

In order to stay consistent, also fix the tests() query operator. Not binaries(), though: query logic is kinda complicated so I don't want to shave that particular yak. Implementing ConfiguredTargetValueAccessor#getPrerequisites() was enough yak shaving for the day.

A drive-by cleanup that I *could* *have* done, but didn't, is to allow aliases in test_suite.tests and maybe make it configurable: now the logic is all there, so the only change that would be required is to actually allow aliases where we now allow only tests and test_suite rules and remove the non-configurable bit from the attribute. If we want that, it should be done in a separate change because it's separate functionality.

Taking all the select() branches for alias.actual is a questionable approach, but we don't have enough data where we (currently) compute the resolution of target patterns to do better and it's consistent with "blaze query", so I guess that'll do for now. I'll file that under the "blaze query should really be aware of configurations" label.

RELNOTES: None.
PiperOrigin-RevId: 281730608
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
index 4458b77..c51f3cf 100644
--- a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
@@ -91,7 +91,7 @@
   private static class FilterTests extends AbstractFilteringPolicy {
     @Override
     public boolean shouldRetain(Target target, boolean explicit) {
-      return TargetUtils.isTestOrTestSuiteRule(target)
+      return (TargetUtils.isTestOrTestSuiteRule(target) || TargetUtils.isAlias(target))
           && FILTER_MANUAL.shouldRetain(target, explicit);
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetValueAccessor.java b/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetValueAccessor.java
index c44b78a0..fdd77c0 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetValueAccessor.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/ConfiguredTargetValueAccessor.java
@@ -14,16 +14,27 @@
 package com.google.devtools.build.lib.query2;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.AttributeMap.DepEdge;
+import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper;
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.query2.aquery.ActionGraphQueryEnvironment;
 import com.google.devtools.build.lib.query2.cquery.ConfiguredTargetAccessor;
 import com.google.devtools.build.lib.query2.engine.KeyExtractor;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.TargetAccessor;
 import com.google.devtools.build.lib.query2.engine.QueryException;
 import com.google.devtools.build.lib.query2.engine.QueryExpression;
 import com.google.devtools.build.lib.query2.engine.QueryVisibility;
+import com.google.devtools.build.lib.rules.AliasConfiguredTarget;
 import com.google.devtools.build.lib.skyframe.AspectValue;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetValue;
@@ -39,19 +50,22 @@
 /**
  * A {@link TargetAccessor} for {@link ConfiguredTargetValue} objects.
  *
- * <p>Incomplete; we'll implement getPrerequisites and getVisibility when needed.
+ * <p>Incomplete; we'll implement getVisibility when needed.
  */
 public class ConfiguredTargetValueAccessor implements TargetAccessor<ConfiguredTargetValue> {
 
   private final WalkableGraph walkableGraph;
   private final KeyExtractor<ConfiguredTargetValue, ConfiguredTargetKey>
       configuredTargetKeyExtractor;
+  private final ActionGraphQueryEnvironment queryEnvironment;
 
   public ConfiguredTargetValueAccessor(
       WalkableGraph walkableGraph,
-      KeyExtractor<ConfiguredTargetValue, ConfiguredTargetKey> configuredTargetKeyExtractor) {
+      KeyExtractor<ConfiguredTargetValue, ConfiguredTargetKey> configuredTargetKeyExtractor,
+      ActionGraphQueryEnvironment queryEnvironment) {
     this.walkableGraph = walkableGraph;
     this.configuredTargetKeyExtractor = configuredTargetKeyExtractor;
+    this.queryEnvironment = queryEnvironment;
   }
 
   @Override
@@ -94,14 +108,54 @@
   }
 
   @Override
+  public boolean isAlias(ConfiguredTargetValue configuredTargetValue) {
+    Target actualTarget = getTargetFromConfiguredTargetValue(configuredTargetValue);
+    return TargetUtils.isAlias(actualTarget);
+  }
+
+  private static Label getOriginalLabel(ConfiguredTargetValue configuredTargetValue) {
+    ConfiguredTarget configuredTarget = configuredTargetValue.getConfiguredTarget();
+
+    return configuredTarget instanceof AliasConfiguredTarget
+        ? ((AliasConfiguredTarget) configuredTarget).getOriginalLabel()
+        : configuredTarget.getLabel();
+  }
+
+  @Override
   public List<ConfiguredTargetValue> getPrerequisites(
       QueryExpression caller,
       ConfiguredTargetValue configuredTargetValue,
       String attrName,
       String errorMsgPrefix)
-      throws QueryException, InterruptedException {
-    // TODO(bazel-team): implement this if needed.
-    throw new UnsupportedOperationException();
+      throws InterruptedException {
+
+    ConfiguredTarget configuredTarget = configuredTargetValue.getConfiguredTarget();
+
+    Multimap<Label, ConfiguredTargetValue> depsByLabel =
+        Multimaps.index(
+            queryEnvironment.getFwdDeps(ImmutableList.of(configuredTargetValue)),
+            ConfiguredTargetValueAccessor::getOriginalLabel);
+
+    ImmutableMap<Label, ConfigMatchingProvider> configConditions;
+    if (configuredTarget instanceof RuleConfiguredTarget) {
+      configConditions = ((RuleConfiguredTarget) configuredTarget).getConfigConditions();
+    } else if (configuredTarget instanceof AliasConfiguredTarget) {
+      configConditions = ((AliasConfiguredTarget) configuredTarget).getConfigConditions();
+    } else {
+      throw new IllegalStateException();
+    }
+
+    ConfiguredAttributeMapper mapper =
+        ConfiguredAttributeMapper.of(
+            (Rule) getTargetFromConfiguredTargetValue(configuredTargetValue), configConditions);
+
+    // Doing this with streams results in a type List<? super ConfiguredTargetValue>
+    ImmutableList.Builder<ConfiguredTargetValue> result = ImmutableList.builder();
+    for (DepEdge depEdge : mapper.visitLabels(mapper.getAttributeDefinition(attrName))) {
+      result.addAll(depsByLabel.get(depEdge.getLabel()));
+    }
+
+    return result.build();
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java
index f004f83..279b367 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java
@@ -110,7 +110,7 @@
         };
     this.accessor =
         new ConfiguredTargetValueAccessor(
-            walkableGraphSupplier.get(), this.configuredTargetKeyExtractor);
+            walkableGraphSupplier.get(), this.configuredTargetKeyExtractor, this);
   }
 
   public ActionGraphQueryEnvironment(
diff --git a/src/main/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetAccessor.java b/src/main/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetAccessor.java
index b6973fb..04a56c9 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetAccessor.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetAccessor.java
@@ -99,6 +99,12 @@
   }
 
   @Override
+  public boolean isAlias(ConfiguredTarget target) {
+    Target actualTarget = getTargetFromConfiguredTarget(target);
+    return TargetUtils.isAlias(actualTarget);
+  }
+
+  @Override
   public List<ConfiguredTarget> getPrerequisites(
       QueryExpression caller,
       ConfiguredTarget configuredTarget,
@@ -114,11 +120,18 @@
     Multimap<Label, ConfiguredTarget> depsByLabel =
         Multimaps.index(
             queryEnvironment.getFwdDeps(ImmutableList.of(configuredTarget)),
-            ConfiguredTarget::getLabel);
+            ConfiguredTargetAccessor::getOriginalLabel);
 
     Rule rule = (Rule) getTargetFromConfiguredTarget(configuredTarget);
-    ImmutableMap<Label, ConfigMatchingProvider> configConditions =
-        ((RuleConfiguredTarget) configuredTarget).getConfigConditions();
+    ImmutableMap<Label, ConfigMatchingProvider> configConditions;
+    if (configuredTarget instanceof RuleConfiguredTarget) {
+      configConditions = ((RuleConfiguredTarget) configuredTarget).getConfigConditions();
+    } else if (configuredTarget instanceof AliasConfiguredTarget) {
+      configConditions = ((AliasConfiguredTarget) configuredTarget).getConfigConditions();
+    } else {
+      throw new IllegalStateException();
+    }
+
     ConfiguredAttributeMapper attributeMapper =
         ConfiguredAttributeMapper.of(rule, configConditions);
     if (!attributeMapper.has(attrName)) {
@@ -163,14 +176,17 @@
     return getTargetFromConfiguredTarget(configuredTarget, walkableGraph);
   }
 
+  private static Label getOriginalLabel(ConfiguredTarget configuredTarget) {
+    return configuredTarget instanceof AliasConfiguredTarget
+        ? ((AliasConfiguredTarget) configuredTarget).getOriginalLabel()
+        : configuredTarget.getLabel();
+  }
+
   public static Target getTargetFromConfiguredTarget(
       ConfiguredTarget configuredTarget, WalkableGraph walkableGraph) {
     Target target = null;
     try {
-      Label label =
-          configuredTarget instanceof AliasConfiguredTarget
-              ? ((AliasConfiguredTarget) configuredTarget).getOriginalLabel()
-              : configuredTarget.getLabel();
+      Label label = getOriginalLabel(configuredTarget);
       target =
           ((PackageValue) walkableGraph.getValue(PackageValue.key(label.getPackageIdentifier())))
               .getPackage()
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
index b3e0dbe..a61ad74 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
@@ -559,6 +559,9 @@
      */
     boolean isTestSuite(T target);
 
+    /** Returns whether the given target is an alias rule. */
+    boolean isAlias(T target);
+
     /**
      * If the attribute of the given name on the given target is a label or label list, then this
      * method returns the list of corresponding target instances. Otherwise returns an empty list.
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java
index 1bed494..f1b77fe 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java
@@ -15,6 +15,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
@@ -77,6 +78,8 @@
           closure
               .getUniqueTestSuites(partitionResult.testSuiteTargets)
               .forEach(closure::visitUniqueTestsInUniqueSuite);
+          closure.getUniqueAliases(partitionResult.aliasTargets).forEach(closure::visitUniqueAlias);
+
           callback.process(closure.getUniqueTests(partitionResult.testTargets));
         };
 
@@ -96,14 +99,17 @@
   private static class PartitionResult<T> {
     final ImmutableList<T> testTargets;
     final ImmutableList<T> testSuiteTargets;
+    final ImmutableList<T> aliasTargets;
     final ImmutableList<T> otherTargets;
 
     private PartitionResult(
         ImmutableList<T> testTargets,
         ImmutableList<T> testSuiteTargets,
+        ImmutableList<T> aliasTargets,
         ImmutableList<T> otherTargets) {
       this.testTargets = testTargets;
       this.testSuiteTargets = testSuiteTargets;
+      this.aliasTargets = aliasTargets;
       this.otherTargets = otherTargets;
     }
   }
@@ -120,6 +126,7 @@
     private final boolean strict;
     private final Uniquifier<T> testUniquifier;
     private final Uniquifier<T> testSuiteUniquifier;
+    private final Uniquifier<T> aliasUniquifier;
     private final List<QueryTaskFuture<Void>> topLevelRecursiveVisitationFutures =
         Collections.synchronizedList(new ArrayList<>());
 
@@ -131,6 +138,7 @@
       this.strict = env.isSettingEnabled(Setting.TESTS_EXPRESSION_STRICT);
       this.testUniquifier = env.createUniquifier();
       this.testSuiteUniquifier = env.createUniquifier();
+      this.aliasUniquifier = env.createUniquifier();
     }
 
     private Iterable<T> getUniqueTests(Iterable<T> tests) throws QueryException {
@@ -141,6 +149,10 @@
       return testSuiteUniquifier.unique(testSuites);
     }
 
+    private Iterable<T> getUniqueAliases(Iterable<T> aliases) throws QueryException {
+      return aliasUniquifier.unique(aliases);
+    }
+
     private void visitUniqueTestsInUniqueSuite(T testSuite) {
       topLevelRecursiveVisitationFutures.add(
           env.executeAsync(() -> recursivelyVisitUniqueTestsInUniqueSuite(testSuite)));
@@ -154,18 +166,28 @@
       return ImmutableList.copyOf(topLevelRecursiveVisitationFutures);
     }
 
-    private QueryTaskFuture<Void> recursivelyVisitUniqueTestsInUniqueSuite(T testSuite) {
-      List<String> tagsAttribute = accessor.getStringListAttr(testSuite, "tags");
-      // Split the tags list into positive and negative tags
-      Set<String> requiredTags = new HashSet<>();
-      Set<String> excludedTags = new HashSet<>();
-      sortTagsBySense(tagsAttribute, requiredTags, excludedTags);
+    private QueryTaskFuture<Void> visitUniqueAlias(T alias) {
+      Iterable<T> actual;
 
+      try {
+        actual = getPrerequisites(alias, "actual");
+      } catch (InterruptedException e) {
+        return env.immediateCancelledFuture();
+      } catch (QueryException e) {
+        return env.immediateFailedFuture(e);
+      }
+
+      return resolveTests(accessor.getLabel(alias), actual, ImmutableSet.of(), ImmutableSet.of());
+    }
+
+    private QueryTaskFuture<Void> resolveTests(
+        String current, Iterable<T> deps, Set<String> requiredTags, Set<String> excludedTags) {
       List<T> testsToProcess = new ArrayList<>();
+      List<T> aliasesToProcess = new ArrayList<>();
       List<T> testSuites;
 
       try {
-        PartitionResult<T> partitionResult = partition(getPrerequisites(testSuite, "tests"));
+        PartitionResult<T> partitionResult = partition(deps);
 
         for (T testTarget : partitionResult.testTargets) {
           if (includeTest(requiredTags, excludedTags, testTarget)
@@ -174,6 +196,12 @@
           }
         }
 
+        for (T aliasTarget : partitionResult.aliasTargets) {
+          if (aliasUniquifier.unique(aliasTarget)) {
+            aliasesToProcess.add(aliasTarget);
+          }
+        }
+
         testSuites = testSuiteUniquifier.unique(partitionResult.testSuiteTargets);
 
         // If strict mode is enabled, then give an error for any non-test, non-test-suite target.
@@ -184,24 +212,11 @@
                 "The label '"
                     + accessor.getLabel(otherTarget)
                     + "' in the test_suite '"
-                    + accessor.getLabel(testSuite)
-                    + "' does not refer to a test or test_suite "
+                    + current
+                    + "' does not refer to a test or test_suite or alias "
                     + "rule!");
           }
         }
-
-        // Add implicit dependencies on tests in same package, if any.
-        for (T target : getPrerequisites(testSuite, "$implicit_tests")) {
-          // The Package construction of $implicit_tests ensures that this check never fails, but we
-          // add it here anyway for compatibility with future code.
-          if (accessor.isTestRule(target)
-              && includeTest(requiredTags, excludedTags, target)
-              && testUniquifier.unique(target)) {
-            testsToProcess.add(target);
-          }
-        }
-      } catch (InterruptedException e) {
-        return env.immediateCancelledFuture();
       } catch (QueryException e) {
         return env.immediateFailedFuture(e);
       }
@@ -214,18 +229,46 @@
                 return null;
               });
 
+      // Resolve aliases
+      QueryTaskFuture<Void> allAliasesVisitedFuture =
+          env.whenAllSucceed(Iterables.transform(aliasesToProcess, this::visitUniqueAlias));
+
       // Visit all suites recursively, asynchronously.
       QueryTaskFuture<Void> allTestSuitsVisitedFuture =
           env.whenAllSucceed(
               Iterables.transform(testSuites, this::recursivelyVisitUniqueTestsInUniqueSuite));
 
       return env.whenAllSucceed(
-          ImmutableList.of(allTestsProcessedFuture, allTestSuitsVisitedFuture));
+          ImmutableList.of(
+              allTestsProcessedFuture, allTestSuitsVisitedFuture, allAliasesVisitedFuture));
+    }
+
+    private QueryTaskFuture<Void> recursivelyVisitUniqueTestsInUniqueSuite(T testSuite) {
+      List<String> tagsAttribute = accessor.getStringListAttr(testSuite, "tags");
+      // Split the tags list into positive and negative tags
+      Set<String> requiredTags = new HashSet<>();
+      Set<String> excludedTags = new HashSet<>();
+      sortTagsBySense(tagsAttribute, requiredTags, excludedTags);
+
+      Iterable<T> deps;
+      try {
+        deps =
+            Iterables.concat(
+                getPrerequisites(testSuite, "tests"),
+                getPrerequisites(testSuite, "$implicit_tests"));
+      } catch (InterruptedException e) {
+        return env.immediateCancelledFuture();
+      } catch (QueryException e) {
+        return env.immediateFailedFuture(e);
+      }
+
+      return resolveTests(accessor.getLabel(testSuite), deps, requiredTags, excludedTags);
     }
 
     private PartitionResult<T> partition(Iterable<T> targets) {
       ImmutableList.Builder<T> testTargetsBuilder = ImmutableList.builder();
       ImmutableList.Builder<T> testSuiteTargetsBuilder = ImmutableList.builder();
+      ImmutableList.Builder<T> aliasesBuilder = ImmutableList.builder();
       ImmutableList.Builder<T> otherTargetsBuilder = ImmutableList.builder();
 
       for (T target : targets) {
@@ -233,13 +276,18 @@
           testTargetsBuilder.add(target);
         } else if (accessor.isTestSuite(target)) {
           testSuiteTargetsBuilder.add(target);
+        } else if (accessor.isAlias(target)) {
+          aliasesBuilder.add(target);
         } else {
           otherTargetsBuilder.add(target);
         }
       }
 
       return new PartitionResult<>(
-          testTargetsBuilder.build(), testSuiteTargetsBuilder.build(), otherTargetsBuilder.build());
+          testTargetsBuilder.build(),
+          testSuiteTargetsBuilder.build(),
+          aliasesBuilder.build(),
+          otherTargetsBuilder.build());
     }
 
     /**
diff --git a/src/main/java/com/google/devtools/build/lib/query2/query/BlazeTargetAccessor.java b/src/main/java/com/google/devtools/build/lib/query2/query/BlazeTargetAccessor.java
index 332076c..af02a04 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/query/BlazeTargetAccessor.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/query/BlazeTargetAccessor.java
@@ -131,6 +131,11 @@
   }
 
   @Override
+  public boolean isAlias(Target target) {
+    return TargetUtils.isAlias(target);
+  }
+
+  @Override
   public Set<QueryVisibility<Target>> getVisibility(Target target)
       throws QueryException, InterruptedException {
     ImmutableSet.Builder<QueryVisibility<Target>> result = ImmutableSet.builder();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
index 1d4e59a..2517378 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
@@ -123,7 +123,8 @@
     Map<Label, SkyKey> testExpansionKeys = new LinkedHashMap<>();
     if (targets != null) {
       for (Target target : targets.getTargets()) {
-        if (TargetUtils.isTestSuiteRule(target) && options.isExpandTestSuites()) {
+        if (options.isExpandTestSuites()
+            && (TargetUtils.isTestSuiteRule(target) || TargetUtils.isAlias(target))) {
           Label label = target.getLabel();
           SkyKey testExpansionKey = TestsForTargetPatternValue.key(ImmutableSet.of(label));
           testExpansionKeys.put(label, testExpansionKey);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionFunction.java
index ac6777b..178787f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionFunction.java
@@ -43,8 +43,8 @@
 import javax.annotation.Nullable;
 
 /**
- * TestExpansionFunction takes a single test_suite target and expands all of the tests it contains,
- * possibly recursively.
+ * TestExpansionFunction takes a single expandable test target ({@code test_suite} or {@code alias})
+ * and expands it recursively.
  */
 // TODO(ulfjack): What about test_suite rules that include each other.
 final class TestExpansionFunction implements SkyFunction {
@@ -80,15 +80,19 @@
     boolean hasError = false;
 
     List<Target> prerequisites = new ArrayList<>();
-    // Note that prerequisites can contain input file targets; the test_suite rule does not
-    // restrict the set of targets that can appear in tests or suites.
-    hasError |= getPrerequisites(env, rule, "tests", prerequisites);
+    if (TargetUtils.isTestSuiteRule(rule)) {
+      // Note that prerequisites can contain input file targets; the test_suite rule does not
+      // restrict the set of targets that can appear in tests or suites.
+      hasError |= getPrerequisites(env, rule, "tests", prerequisites);
+    } else if (TargetUtils.isAlias(rule)) {
+      hasError |= getPrerequisites(env, rule, "actual", prerequisites);
+    }
 
-    // 1. Add all tests
+    // Add all tests
     for (Target test : prerequisites) {
       if (TargetUtils.isTestRule(test)) {
         result.add(test);
-      } else if (strict && !TargetUtils.isTestSuiteRule(test)) {
+      } else if (strict && !TargetUtils.isTestSuiteRule(test) && !TargetUtils.isAlias(rule)) {
         // If strict mode is enabled, then give an error for any non-test, non-test-suite targets.
         // TODO(ulfjack): We need to throw to end the process if we happen to be in --nokeep_going,
         // but we can't know whether or not we are at this point.
@@ -105,27 +109,29 @@
       }
     }
 
-    // 2. Add implicit dependencies on tests in same package, if any.
-    List<Target> implicitTests = new ArrayList<>();
-    hasError |= getPrerequisites(env, rule, "$implicit_tests", implicitTests);
-    for (Target target : implicitTests) {
-      // The Package construction of $implicit_tests ensures that this check never fails, but we
-      // add it here anyway for compatibility with future code.
-      if (TargetUtils.isTestRule(target)) {
-        result.add(target);
+    if (TargetUtils.isTestSuiteRule(rule)) {
+      // Add implicit dependencies on tests in same package, if any.
+      List<Target> implicitTests = new ArrayList<>();
+      hasError |= getPrerequisites(env, rule, "$implicit_tests", implicitTests);
+      for (Target target : implicitTests) {
+        // The Package construction of $implicit_tests ensures that this check never fails, but we
+        // add it here anyway for compatibility with future code.
+        if (TargetUtils.isTestRule(target)) {
+          result.add(target);
+        }
       }
+
+      // Filter based on tags, size, env.
+      TestTargetUtils.filterTests(rule, result);
     }
 
-    // 3. Filter based on tags, size, env.
-    TestTargetUtils.filterTests(rule, result);
-
-    // 4. Expand all rules recursively, collecting labels.
+    // Expand all rules recursively, collecting labels.
     ResolvedTargets.Builder<Label> labelsBuilder = ResolvedTargets.builder();
-    // Don't set filtered targets; they would be removed from the containing test suite.
+    // Don't set filtered targets; they would be removed from the containing rule.
     labelsBuilder.merge(new ResolvedTargets<>(toLabels(result), ImmutableSet.of(), hasError));
 
     for (Target suite : prerequisites) {
-      if (TargetUtils.isTestSuiteRule(suite)) {
+      if (TargetUtils.isTestSuiteRule(suite) || TargetUtils.isAlias(suite)) {
         TestExpansionValue value =
             (TestExpansionValue) env.getValue(TestExpansionValue.key(suite, strict));
         if (value == null) {
@@ -140,9 +146,8 @@
 
   /**
    * Adds the set of targets found in the attribute named {@code attrName}, which must be of label
-   * or label list type, of the {@code test_suite} rule named {@code testSuite}. Returns true if the
-   * method found a problem during the lookup process; the actual error message is reported to the
-   * environment.
+   * or label list list type, Returns true if the method found a problem during the lookup process;
+   * the actual error message is reported to the environment.
    */
   private static boolean getPrerequisites(
       Environment env, Rule rule, String attrName, List<Target> targets)
@@ -154,6 +159,7 @@
             .collect(Collectors.toList());
 
     Set<PackageIdentifier> pkgIdentifiers = new LinkedHashSet<>();
+
     for (Label label : labels) {
       pkgIdentifiers.add(label.getPackageIdentifier());
     }
@@ -163,8 +169,10 @@
     if (env.valuesMissing()) {
       return false;
     }
+
     boolean hasError = false;
     Map<PackageIdentifier, Package> packageMap = new HashMap<>();
+
     for (Map.Entry<SkyKey, ValueOrException<NoSuchPackageException>> entry : packages.entrySet()) {
       try {
         packageMap.put(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionValue.java
index 1b1d88f..dbd3e46 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestExpansionValue.java
@@ -30,8 +30,9 @@
 import java.util.Objects;
 
 /**
- * A value referring to a computed set of resolved targets. This is used for the results of target
- * pattern parsing.
+ * A value containing all the tests expanded from a single rule.
+ *
+ * <p>The rule in question should be a {@code test_suite} or {@code alias}.
  */
 @Immutable
 @ThreadSafe
@@ -64,11 +65,11 @@
   /**
    * Create a target pattern value key.
    *
-   * @param target the target to be expanded
+   * @param target the test suite target to be expanded
    */
   @ThreadSafe
   public static SkyKey key(Target target, boolean strict) {
-    Preconditions.checkState(TargetUtils.isTestSuiteRule(target));
+    Preconditions.checkState(TargetUtils.isTestSuiteRule(target) || TargetUtils.isAlias(target));
     return new TestExpansionKey(target.getLabel(), strict);
   }
 
@@ -98,7 +99,7 @@
 
     @Override
     public String toString() {
-      return "TestsInSuite(" + label + ", strict=" + strict + ")";
+      return "TestExpansionKey(" + label + ", strict=" + strict + ")";
     }
 
     @Override
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestsForTargetPatternFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestsForTargetPatternFunction.java
index 45f767e..29df965 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TestsForTargetPatternFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestsForTargetPatternFunction.java
@@ -36,7 +36,7 @@
 /**
  * Returns all tests that need to be run when testing is requested for a given set of targets.
  *
- * <p>This requires resolving {@code test_suite} rules.
+ * <p>This requires resolving {@code test_suite} rules and aliases.
  */
 final class TestsForTargetPatternFunction implements SkyFunction {
   @Override
@@ -45,10 +45,12 @@
     ResolvedTargets<Target> targets = labelsToTargets(env, expansion.getTargets(), false);
     List<SkyKey> testsInSuitesKeys = new ArrayList<>();
     for (Target target : targets.getTargets()) {
-      if (TargetUtils.isTestSuiteRule(target)) {
+      if (TargetUtils.isTestSuiteRule(target)
+          || TargetUtils.isAlias(target)) { // TestExpansionFunction also handles aliases
         testsInSuitesKeys.add(TestExpansionValue.key(target, true));
       }
     }
+
     Map<SkyKey, SkyValue> testsInSuites = env.getValues(testsInSuitesKeys);
     if (env.valuesMissing()) {
       return null;
@@ -59,7 +61,7 @@
     for (Target target : targets.getTargets()) {
       if (TargetUtils.isTestRule(target)) {
         result.add(target.getLabel());
-      } else if (TargetUtils.isTestSuiteRule(target)) {
+      } else if (TargetUtils.isTestSuiteRule(target) || TargetUtils.isAlias(target)) {
         TestExpansionValue value =
             (TestExpansionValue) testsInSuites.get(TestExpansionValue.key(target, true));
         if (value != null) {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java b/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java
index a71401c..af33408 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/TestTargetUtilsTest.java
@@ -15,7 +15,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -166,19 +165,12 @@
         Sets.newHashSet(test1, test2, test1b), ImmutableSet.<Target>of(test1b, suite));
   }
 
-  private static final Function<Target, Label> TO_LABEL =
-      new Function<Target, Label>() {
-        @Override
-        public Label apply(Target input) {
-          return input.getLabel();
-        }
-      };
-
   private void assertExpandedSuitesSkyframe(Iterable<Target> expected, Collection<Target> suites)
       throws Exception {
     ImmutableSet<Label> expectedLabels =
-        ImmutableSet.copyOf(Iterables.transform(expected, TO_LABEL));
-    ImmutableSet<Label> suiteLabels = ImmutableSet.copyOf(Iterables.transform(suites, TO_LABEL));
+        ImmutableSet.copyOf(Iterables.transform(expected, Target::getLabel));
+    ImmutableSet<Label> suiteLabels =
+        ImmutableSet.copyOf(Iterables.transform(suites, Target::getLabel));
     SkyKey key = TestsForTargetPatternValue.key(suiteLabels);
     EvaluationContext evaluationContext =
         EvaluationContext.newBuilder()
diff --git a/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java b/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java
index 1ab99d4..fd11d96 100644
--- a/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java
@@ -484,6 +484,48 @@
   }
 
   @Test
+  public void testAliasedBuildTestsOnly() throws Exception {
+    tester.addFile(
+        "t/BUILD",
+        "sh_test(name='t', srcs=['t.sh'])",
+        "sh_binary(name='b', srcs=['b.sh'])",
+        "alias(name='ta', actual=':t')",
+        "alias(name='ba', actual=':b')");
+    tester.useLoadingOptions("--build_tests_only");
+    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//t:ta", "//t:ba"));
+    assertThat(loadingResult.getTargetLabels()).containsExactlyElementsIn(getLabels("//t:t"));
+  }
+
+  @Test
+  public void testAliasedTestExpansion() throws Exception {
+    tester.addFile("sh/BUILD", "sh_test(name='t', srcs=['t.sh'])", "alias(name='a', actual=':t')");
+    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//sh:a"));
+    assertThat(loadingResult.getTestsToRunLabels()).containsExactlyElementsIn(getLabels("//sh:t"));
+  }
+
+  @Test
+  public void testAliasedTestSuite() throws Exception {
+    tester.addFile(
+        "sh/BUILD",
+        "sh_test(name='t', srcs=['t.sh'])",
+        "test_suite(name='ts', tests=[':t'])",
+        "alias(name='a', actual=':ts')");
+    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//sh:a"));
+    assertThat(loadingResult.getTestsToRunLabels()).containsExactlyElementsIn(getLabels("//sh:t"));
+  }
+
+  @Test
+  public void testDoubleAliasedTestExpansion() throws Exception {
+    tester.addFile(
+        "sh/BUILD",
+        "sh_test(name='t', srcs=['t.sh'])",
+        "alias(name='a1', actual=':t')",
+        "alias(name='a2', actual=':a1')");
+    TargetPatternPhaseValue loadingResult = assertNoErrors(tester.loadTests("//sh:a2"));
+    assertThat(loadingResult.getTestsToRunLabels()).containsExactlyElementsIn(getLabels("//sh:t"));
+  }
+
+  @Test
   public void testTestSuiteExpansionFails() throws Exception {
     tester.addFile("ts/BUILD",
         "test_suite(name = 'tests', tests = ['//nonexistent:my_test'])");
diff --git a/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java b/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java
index 38f9e40..e62003f 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java
@@ -944,6 +944,36 @@
   }
 
   @Test
+  public void testTestsOperatorFollowsAliasesToTests() throws Exception {
+    writeFile(
+        "a/BUILD", "sh_test(name='test', srcs=['test.sh'])", "alias(name='alias', actual=':test')");
+
+    assertThat(eval("tests(//a:alias)")).isEqualTo(eval("//a:test"));
+  }
+
+  @Test
+  public void testTestsOperatorFollowsDoubleAliasesToTests() throws Exception {
+    writeFile(
+        "a/BUILD",
+        "sh_test(name='test', srcs=['test.sh'])",
+        "alias(name='alias1', actual=':test')",
+        "alias(name='alias2', actual=':alias1')");
+
+    assertThat(eval("tests(//a:alias2)")).isEqualTo(eval("//a:test"));
+  }
+
+  @Test
+  public void testTestsOperatorFollowsAliasesToTestSuites() throws Exception {
+    writeFile(
+        "a/BUILD",
+        "sh_test(name='test', srcs=['test.sh'])",
+        "test_suite(name='suite', tests=[':test'])",
+        "alias(name='alias', actual=':suite')");
+
+    assertThat(eval("tests(//a:alias)")).isEqualTo(eval("//a:test"));
+  }
+
+  @Test
   public void testDotDotDotWithUnrelatedCycle() throws Exception {
     writeFile("a/BUILD", "sh_library(name = 'a')");
     writeFile(
@@ -1219,7 +1249,7 @@
     assertThat(evalThrows("tests(//x:a)", false))
         .isEqualTo(
             "The label '//x:a.txt' in the test_suite '//x:a' does not refer to a test or "
-                + "test_suite rule!");
+                + "test_suite or alias rule!");
   }
 
   @Test