Repository: pantsbuild/pants

Issue:
JVM scopes not applied to strict_dependencies
`strict_dependencies` currently do not apply the `JvmTarget` `scope` argument, but should.

Since `strict_dependencies` are only used at compile time, this bug only affects compile time (ie, a `compile` scoped dep would properly be excluded at runtime).

Relates to #4874 ... we ought to do a wholesale rethinking of how both of these are computed (...or perhaps a reimplementation of `strict_dependencies` using `Target.closure` with the relevant dependency arguments?)


Discussion:
cc @baroquebobcat 
Hm. I've been feeling like strict_deps' implemenation should be moved within the existing traversal code because I'd been worried about things like this. Thanks for pinging me.
I think this explains a bug I saw yesterday where a test scoped target was available on the classpath of a strict_deps=True target, but not a strict_deps=False target.
Yes.
I've got a proof of concept that unifies the traversal code by implementing #5149

Resolving patch:
diff --git a/src/python/pants/build_graph/build_graph.py b/src/python/pants/build_graph/build_graph.py
--- a/src/python/pants/build_graph/build_graph.py
+++ b/src/python/pants/build_graph/build_graph.py
@@ -12,6 +12,7 @@
 
 from twitter.common.collections import OrderedSet
 
+from pants.base.deprecated import deprecated_conditional
 from pants.build_graph.address import Address
 from pants.build_graph.address_lookup_error import AddressLookupError
 from pants.build_graph.target import Target
@@ -49,8 +50,9 @@ def __init__(self, addr):
       super(BuildGraph.ManualSyntheticTargetError, self).__init__(
           'Found a manually-defined target at synthetic address {}'.format(addr.spec))
 
-  class DepthAgnosticWalk(object):
-    """This is a utility class to aid in graph traversals that don't care about the depth."""
+
+  class NoDepPredicateWalk(object):
+    """This is a utility class to aid in graph traversals that don't have predicates on dependency edges."""
 
     def __init__(self):
       self._worked = set()
@@ -74,12 +76,28 @@ def expand_once(self, vertex, _):
       self._expanded.add(vertex)
       return True
 
-  class DepthAwareWalk(DepthAgnosticWalk):
+    def dep_predicate(self, target, dep, level):
+      return True
+
+
+  class DepthAgnosticWalk(NoDepPredicateWalk):
+    """This is a utility class to aid in graph traversals that don't care about the depth."""
+
+    def __init__(self, dep_predicate):
+      super(BuildGraph.DepthAgnosticWalk, self).__init__()
+      self._dep_predicate = dep_predicate
+
+    def dep_predicate(self, target, dep, level):
+      return self._dep_predicate(target, dep)
+
+
+  class DepthAwareWalk(NoDepPredicateWalk):
     """This is a utility class to aid in graph traversals that care about the depth."""
 
-    def __init__(self):
+    def __init__(self, leveled_predicate):
       super(BuildGraph.DepthAwareWalk, self).__init__()
       self._expanded = defaultdict(set)
+      self._leveled_predicate = leveled_predicate
 
     def expand_once(self, vertex, level):
       """Returns True if this (vertex, level) pair has never been expanded, and False otherwise.
@@ -92,6 +110,9 @@ def expand_once(self, vertex, level):
       self._expanded[vertex].add(level)
       return True
 
+    def dep_predicate(self, target, dep, level):
+      return self._leveled_predicate(dep, level)
+
   @staticmethod
   def closure(*vargs, **kwargs):
     """See `Target.closure_for_targets` for arguments.
@@ -329,8 +350,38 @@ def sorted_targets(self):
     """
     return sort_targets(self.targets())
 
-  def walk_transitive_dependency_graph(self, addresses, work, predicate=None, postorder=False,
-                                       leveled_predicate=None):
+  def _walk_factory(self, dep_predicate, leveled_predicate):
+    """Construct the right context object for managing state during a transitive walk."""
+    deprecated_conditional(
+      lambda: leveled_predicate is not None,
+      '1.7.0.dev0',
+      'leveled_predicate',
+      '''
+      Deprecated property leveled_predicate used. Please migrate to using dep_predicate.
+      ''',
+      stacklevel=5
+    )
+    if leveled_predicate and dep_predicate:
+      raise ValueError('Cannot specify both leveled_predicate and dep_predicate')
+    # Use the DepthAgnosticWalk if we can, because DepthAwareWalk does a bit of extra work that can
+    # slow things down by few millis.
+
+    walk = None
+    if leveled_predicate:
+      walk = self.DepthAwareWalk(leveled_predicate)
+    elif dep_predicate:
+      walk = self.DepthAgnosticWalk(dep_predicate)
+    else:
+      walk = self.NoDepPredicateWalk()
+    return walk
+
+  def walk_transitive_dependency_graph(self,
+                                       addresses,
+                                       work,
+                                       predicate=None,
+                                       postorder=False,
+                                       leveled_predicate=None,
+                                       dep_predicate=None):
     """Given a work function, walks the transitive dependency closure of `addresses` using DFS.
 
     :API: public
@@ -344,14 +395,16 @@ def walk_transitive_dependency_graph(self, addresses, work, predicate=None, post
       out of the closure.  If it is given, any Target which fails the predicate will not be
       walked, nor will its dependencies.  Thus predicate effectively trims out any subgraph
       that would only be reachable through Targets that fail the predicate.
-    :param function leveled_predicate: Behaves identically to predicate, but takes the depth of the
+    :param function dep_predicate: Takes two parameters, the current target and the dependency of
+      the current target. If this parameter is not given, no dependencies will be filtered
+      when traversing the closure. If it is given, when the predicate fails, the edge to the dependency
+      will not be expanded.
+    :param function leveled_predicate: Deprecated. Behaves identically to predicate, but takes the depth of the
       target in the search tree as a second parameter, and it is checked just before a dependency is
       expanded.
     """
-    # Use the DepthAgnosticWalk if we can, because DepthAwareWalk does a bit of extra work that can
-    # slow things down by few millis.
-    walker = self.DepthAwareWalk if leveled_predicate else self.DepthAgnosticWalk
-    walk = walker()
+    walk = self._walk_factory(dep_predicate, leveled_predicate)
+
     def _walk_rec(addr, level=0):
       # If we've followed an edge to this address, stop recursing.
       if not walk.expand_once(addr, level):
@@ -368,8 +421,7 @@ def _walk_rec(addr, level=0):
       for dep_address in self._target_dependencies_by_address[addr]:
         if walk.expanded_or_worked(dep_address):
           continue
-        if not leveled_predicate \
-                or leveled_predicate(self._target_by_address[dep_address], level):
+        if walk.dep_predicate(target, self._target_by_address[dep_address], level):
           _walk_rec(dep_address, level + 1)
 
       if postorder and walk.do_work_once(addr):
@@ -438,7 +490,11 @@ def transitive_subgraph_of_addresses(self, addresses, *vargs, **kwargs):
       out of the closure.  If it is given, any Target which fails the predicate will not be
       walked, nor will its dependencies.  Thus predicate effectively trims out any subgraph
       that would only be reachable through Targets that fail the predicate.
-    :param function leveled_predicate: Behaves identically to predicate, but takes the depth of the
+    :param function dep_predicate: Takes two parameters, the current target and the dependency of
+      the current target. If this parameter is not given, no dependencies will be filtered
+      when traversing the closure. If it is given, when the predicate fails, the edge to the dependency
+      will not be expanded.
+    :param function leveled_predicate: Deprecated. Behaves identically to predicate, but takes the depth of the
       target in the search tree as a second parameter, and it is checked just before a dependency is
       expanded.
     """
@@ -448,7 +504,11 @@ def transitive_subgraph_of_addresses(self, addresses, *vargs, **kwargs):
                                           **kwargs)
     return ret
 
-  def transitive_subgraph_of_addresses_bfs(self, addresses, predicate=None, leveled_predicate=None):
+  def transitive_subgraph_of_addresses_bfs(self,
+                                           addresses,
+                                           predicate=None,
+                                           leveled_predicate=None,
+                                           dep_predicate=None):
     """Returns the transitive dependency closure of `addresses` using BFS.
 
     :API: public
@@ -458,15 +518,17 @@ def transitive_subgraph_of_addresses_bfs(self, addresses, predicate=None, levele
       out of the closure.  If it is given, any Target which fails the predicate will not be
       walked, nor will its dependencies.  Thus predicate effectively trims out any subgraph
       that would only be reachable through Targets that fail the predicate.
-    :param function leveled_predicate: Behaves identically to predicate, but takes the depth of the
+    :param function dep_predicate: Takes two parameters, the current target and the dependency of
+      the current target. If this parameter is not given, no dependencies will be filtered
+      when traversing the closure. If it is given, when the predicate fails, the edge to the dependency
+      will not be expanded.
+    :param function leveled_predicate: Deprecated. Behaves identically to predicate, but takes the depth of the
       target in the search tree as a second parameter, and it is checked just before a dependency is
       expanded.
     """
+    walk = self._walk_factory(dep_predicate, leveled_predicate)
+
     ordered_closure = OrderedSet()
-    # Use the DepthAgnosticWalk if we can, because DepthAwareWalk does a bit of extra work that can
-    # slow things down by few millis.
-    walker = self.DepthAwareWalk if leveled_predicate else self.DepthAgnosticWalk
-    walk = walker()
     to_walk = deque((0, addr) for addr in addresses)
     while len(to_walk) > 0:
       level, address = to_walk.popleft()
@@ -479,11 +541,11 @@ def transitive_subgraph_of_addresses_bfs(self, addresses, predicate=None, levele
         continue
       if walk.do_work_once(address):
         ordered_closure.add(target)
-      for addr in self._target_dependencies_by_address[address]:
-        if walk.expanded_or_worked(addr):
+      for dep_address in self._target_dependencies_by_address[address]:
+        if walk.expanded_or_worked(dep_address):
           continue
-        if not leveled_predicate or leveled_predicate(self._target_by_address[addr], level):
-          to_walk.append((level + 1, addr))
+        if walk.dep_predicate(target, self._target_by_address[dep_address], level):
+          to_walk.append((level + 1, dep_address))
     return ordered_closure
 
   @abstractmethod
diff --git a/src/python/pants/build_graph/target.py b/src/python/pants/build_graph/target.py
--- a/src/python/pants/build_graph/target.py
+++ b/src/python/pants/build_graph/target.py
@@ -33,49 +33,6 @@
 logger = logging.getLogger(__name__)
 
 
-class SyntheticTargetNotFound(Exception):
-  pass
-
-
-def _get_synthetic_target(target, codegen_dep):
-  """Find a codegen_dep's corresponding synthetic target in the dependencies of the given target.
-
-  TODO: This lookup represents a workaround to avoid including logic about exports at codegen time.
-  We should likely make SimpleCodegenTask aware of exports, so that it can clone exports to
-  generated targets while updating the exports with relevant synthetic target specs.
-    see https://github.com/pantsbuild/pants/issues/4936
-  """
-  for dep in target.dependencies:
-    if dep != codegen_dep and dep.is_synthetic and dep.derived_from == codegen_dep:
-      return dep
-  raise SyntheticTargetNotFound('No synthetic target is found for thrift target: {}'.format(codegen_dep))
-
-
-def _resolve_strict_dependencies(target, dep_context):
-  for declared in target.dependencies:
-    if type(declared) in dep_context.alias_types:
-      # Is an alias. Recurse to expand.
-      for r in declared.strict_dependencies(dep_context):
-        yield r
-    else:
-      yield declared
-
-    for export in _resolve_exports(declared, dep_context):
-      yield export
-
-
-def _resolve_exports(target, dep_context):
-  for export in target.exports(dep_context):
-    if type(export) in dep_context.alias_types:
-      # If exported target is an alias, expand its dependencies.
-      for dep in export.strict_dependencies(dep_context):
-        yield dep
-    else:
-      yield export
-      for exp in _resolve_exports(export, dep_context):
-        yield exp
-
-
 class AbstractTarget(object):
 
   @classmethod
@@ -267,13 +224,16 @@ def maybe_readable_combine_ids(cls, ids):
     return ids[0] if len(ids) == 1 else cls.combine_ids(ids)
 
   @classmethod
-  def _closure_predicate(cls, include_scopes=None, exclude_scopes=None, respect_intransitive=False):
+  def _closure_dep_predicate(cls, roots, include_scopes=None, exclude_scopes=None, respect_intransitive=False):
     if not respect_intransitive and include_scopes is None and exclude_scopes is None:
       return None
-    def predicate(target, level):
-      if not target.scope.in_scope(include_scopes=include_scopes, exclude_scopes=exclude_scopes):
+
+    root_lookup = set(roots)
+    def predicate(target, dep_target):
+      if not dep_target.scope.in_scope(include_scopes=include_scopes, exclude_scopes=exclude_scopes):
         return False
-      if respect_intransitive and not target.transitive and level > 0:
+      # dep_target.transitive == False means that dep_target is only included if target is a root target.
+      if respect_intransitive and not dep_target.transitive and target not in root_lookup:
         return False
       return True
     return predicate
@@ -302,7 +262,8 @@ def closure_for_targets(cls, target_roots, exclude_scopes=None, include_scopes=N
 
     build_graph = target_roots[0]._build_graph
     addresses = [target.address for target in target_roots]
-    leveled_predicate = cls._closure_predicate(include_scopes=include_scopes,
+    dep_predicate = cls._closure_dep_predicate(target_roots,
+                                               include_scopes=include_scopes,
                                                exclude_scopes=exclude_scopes,
                                                respect_intransitive=respect_intransitive)
     closure = OrderedSet()
@@ -312,12 +273,12 @@ def closure_for_targets(cls, target_roots, exclude_scopes=None, include_scopes=N
         addresses=addresses,
         work=closure.add,
         postorder=postorder,
-        leveled_predicate=leveled_predicate,
+        dep_predicate=dep_predicate,
       )
     else:
       closure.update(build_graph.transitive_subgraph_of_addresses_bfs(
         addresses=addresses,
-        leveled_predicate=leveled_predicate,
+        dep_predicate=dep_predicate,
       ))
 
     # Make sure all the roots made it into the closure.
@@ -374,7 +335,7 @@ def __init__(self, name, address, build_graph, type_alias=None, payload=None, ta
     self._cached_all_transitive_fingerprint_map = {}
     self._cached_direct_transitive_fingerprint_map = {}
     self._cached_strict_dependencies_map = {}
-    self._cached_exports_map = {}
+    self._cached_exports_addresses = None
     self._no_cache = no_cache
     if kwargs:
       self.Arguments.check(self, kwargs)
@@ -458,7 +419,7 @@ def mark_invalidation_hash_dirty(self):
     self._cached_all_transitive_fingerprint_map = {}
     self._cached_direct_transitive_fingerprint_map = {}
     self._cached_strict_dependencies_map = {}
-    self._cached_exports_map = {}
+    self._cached_exports_addresses = None
     self.mark_extra_invalidation_hash_dirty()
     self.payload.mark_dirty()
 
@@ -687,31 +648,27 @@ def dependencies(self):
     return [self._build_graph.get_target(dep_address)
             for dep_address in self._build_graph.dependencies_of(self.address)]
 
-  def exports(self, dep_context):
-    """
-    :param dep_context: A DependencyContext with configuration for the request.
-
-    :return: targets that this target directly exports. Note that this list is not transitive,
-      but that exports are transitively expanded during the computation of strict_dependencies.
-    :rtype: list of Target
-    """
-    exports = self._cached_exports_map.get(dep_context, None)
+  @property
+  def export_addresses(self):
+    exports = self._cached_exports_addresses
     if exports is None:
+
       exports = []
-      for export in getattr(self, 'export_specs', []):
-        if not isinstance(export, Target):
-          export_spec = export
-          export_addr = Address.parse(export_spec, relative_to=self.address.spec_path)
-          export = self._build_graph.get_target(export_addr)
-          if export not in self.dependencies:
-            # A target can only export its dependencies.
-            raise TargetDefinitionException(
-                self,
-                'Invalid export: "{}" must also be a dependency.'.format(export_spec))
-        if isinstance(export, dep_context.codegen_types):
-          export = _get_synthetic_target(self, export)
-        exports.append(export)
-      self._cached_exports_map[dep_context] = exports
+      for export_spec in getattr(self, 'export_specs', tuple()):
+        if isinstance(export_spec, Target):
+          exports.append(export_spec.address)
+        else:
+          exports.append(Address.parse(export_spec, relative_to=self.address.spec_path))
+      exports = tuple(exports)
+
+      dep_addresses = {d.address for d in self.dependencies}
+      invalid_export_specs = [a.spec for a in exports if a not in dep_addresses]
+      if len(invalid_export_specs) > 0:
+        raise TargetDefinitionException(
+            self,
+            'Invalid exports: these exports must also be dependencies\n  {}'.format('\n  '.join(invalid_export_specs)))
+
+      self._cached_exports_addresses = exports
     return exports
 
   def strict_dependencies(self, dep_context):
@@ -726,16 +683,48 @@ def strict_dependencies(self, dep_context):
     """
     strict_deps = self._cached_strict_dependencies_map.get(dep_context, None)
     if strict_deps is None:
+      default_predicate = self._closure_dep_predicate({self},
+                                                      **dep_context.target_closure_kwargs)
+
+      def dep_predicate(source, dependency):
+        if not default_predicate(source, dependency):
+          return False
+
+        # Always expand aliases.
+        if type(source) in dep_context.alias_types:
+          return True
+
+        # Traverse other dependencies if they are exported.
+        if source._dep_is_exported(dependency):
+          return True
+        return False
+
+      dep_addresses = [d.address for d in self.dependencies
+                        if default_predicate(self, d)
+                      ]
+      result = self._build_graph.transitive_subgraph_of_addresses_bfs(
+        addresses=dep_addresses,
+        dep_predicate=dep_predicate
+      )
+
       strict_deps = OrderedSet()
-      for declared in _resolve_strict_dependencies(self, dep_context):
+      for declared in result:
+        if type(declared) in dep_context.alias_types:
+          continue
         if isinstance(declared, dep_context.compiler_plugin_types):
-          strict_deps.update(declared.closure(bfs=True, **dep_context.target_closure_kwargs))
-        else:
-          strict_deps.add(declared)
+          strict_deps.update(declared.closure(
+            bfs=True,
+            **dep_context.target_closure_kwargs))
+        strict_deps.add(declared)
+
       strict_deps = list(strict_deps)
       self._cached_strict_dependencies_map[dep_context] = strict_deps
     return strict_deps
 
+  def _dep_is_exported(self, dependency):
+    return dependency.address in self.export_addresses or \
+           dependency.is_synthetic and (dependency.concrete_derived_from.address in self.export_addresses)
+
   @property
   def dependents(self):
     """
diff --git a/src/python/pants/task/simple_codegen_task.py b/src/python/pants/task/simple_codegen_task.py
--- a/src/python/pants/task/simple_codegen_task.py
+++ b/src/python/pants/task/simple_codegen_task.py
@@ -340,7 +340,7 @@ def _supports_exports(self, target_type):
     return hasattr(target_type, 'export_specs')
 
   def _original_export_specs(self, target):
-    return [t.address.spec for t in target.exports(EmptyDepContext())]
+    return [t.spec for t in target.export_addresses]
 
   def resolve_deps(self, unresolved_deps):
     """


Test patch:
diff --git a/tests/python/pants_test/build_graph/test_build_graph.py b/tests/python/pants_test/build_graph/test_build_graph.py
--- a/tests/python/pants_test/build_graph/test_build_graph.py
+++ b/tests/python/pants_test/build_graph/test_build_graph.py
@@ -324,6 +324,7 @@ def test_raise_on_duplicate_dependencies(self):
       self.inject_address_closure('//:a')
 
   def test_leveled_predicate(self):
+    # Keeping the test of this until it is removed to ensure it continues to function.
     a = self.make_target(spec='a')
     b = self.make_target(spec='b', dependencies=[a])
     c = self.make_target(spec='c', dependencies=[b])
@@ -369,6 +370,60 @@ def only_indirect_a(target, depth):
     check_funcs({a, b, c, d}, {c, d}, leveled_predicate=only_indirect_a)
     check_funcs({a, b, c}, {c}, leveled_predicate=only_indirect_a)
 
+  def test_dep_predicate(self):
+    a = self.make_target(spec='a')
+    b = self.make_target(spec='b', dependencies=[a])
+    c = self.make_target(spec='c', dependencies=[b])
+    d = self.make_target(spec='d', dependencies=[a])
+
+    subgraph_funcs = (self.build_graph.transitive_subgraph_of_addresses,
+                      self.build_graph.transitive_subgraph_of_addresses_bfs)
+
+    def check_funcs(expected, roots, dep_predicate_factory=None, **kwargs):
+      if dep_predicate_factory:
+        kwargs['dep_predicate'] = dep_predicate_factory(roots)
+      for func in subgraph_funcs:
+        seen_targets = defaultdict(lambda: 0)
+        def predicate_sees(target):
+          seen_targets[target] += 1
+          return True
+
+        result = func([t.address for t in roots], predicate=predicate_sees, **kwargs)
+        self.assertEquals(set(expected), set(result))
+        if any(ct > 1 for ct in seen_targets.values()):
+          self.fail('func {} visited {} more than once.'.format(
+            func,
+            ', '.join(t.address for t, ct in seen_targets.items() if ct > 1))
+          )
+
+    def only_roots_factory(roots):
+      def only_roots(_, __):
+        # This is a silly constraint, because it effectively turns the transitive subgraph functions
+        # into the identity function.
+        return False
+      return only_roots
+
+    def only_direct_deps_factory(roots):
+      def only_direct_deps(source, dep):
+        return source in roots
+      return only_direct_deps
+
+    def only_indirect_a_factory(roots):
+      def only_indirect_a(source, dep):
+        # This is a really weird constraint just to demonstrate functionality.
+        return dep != a or source not in roots
+      return only_indirect_a
+
+    check_funcs({a, b, d}, {b, d}, dep_predicate_factory=None)
+    check_funcs({b, d}, {b, d}, dep_predicate_factory=only_roots_factory)
+    check_funcs({a, b, d}, {b, d}, dep_predicate_factory=only_direct_deps_factory)
+    check_funcs({d, a}, {d}, dep_predicate_factory=only_direct_deps_factory)
+    check_funcs({b, c}, {c}, dep_predicate_factory=only_direct_deps_factory)
+    check_funcs({b, c, d, a}, {c, d}, dep_predicate_factory=only_direct_deps_factory)
+    check_funcs({b, d}, {b, d}, dep_predicate_factory=only_indirect_a_factory)
+    check_funcs({a, b, c, d}, {c, d}, dep_predicate_factory=only_indirect_a_factory)
+    check_funcs({a, b, c}, {c}, dep_predicate_factory=only_indirect_a_factory)
+
   def test_derivation(self):
     a = self.make_target('a')
     a_addr = a.address
diff --git a/tests/python/pants_test/build_graph/test_scopes.py b/tests/python/pants_test/build_graph/test_scopes.py
--- a/tests/python/pants_test/build_graph/test_scopes.py
+++ b/tests/python/pants_test/build_graph/test_scopes.py
@@ -120,7 +120,7 @@ def test_intransitive(self):
     self.assert_closure({b_intransitive, a}, {b_intransitive})
     self.assert_closure({e, d, a, c}, {e, d})
     self.assert_closure_dfs([d, c, b_intransitive, a], [d, c], ordered=True)
-    self.assert_closure_dfs([c, d, a, b_intransitive], [d, c], ordered=True, postorder=True)
+    self.assert_closure_dfs([a, b_intransitive, c, d], [d, c], ordered=True, postorder=True)
     self.assert_closure_bfs([d, c, b_intransitive, a], [d, c], ordered=True)
     self.assert_closure({a, b_intransitive, d, c}, {d}, respect_intransitive=False)
 
diff --git a/tests/python/pants_test/build_graph/test_target.py b/tests/python/pants_test/build_graph/test_target.py
--- a/tests/python/pants_test/build_graph/test_target.py
+++ b/tests/python/pants_test/build_graph/test_target.py
@@ -16,6 +16,7 @@
 from pants.base.payload_field import SetOfPrimitivesField
 from pants.build_graph.address import Address
 from pants.build_graph.target import Target
+from pants.build_graph.target_scopes import Scopes
 from pants.source.wrapped_globs import Globs
 from pants_test.base_test import BaseTest
 from pants_test.subsystem.subsystem_util import init_subsystem
@@ -283,11 +284,18 @@ def _generate_strict_dependencies(self):
       exports=[':C_alias'],
     )
 
+    self.lib_f = self.make_target(
+      'com/foo:F',
+      target_type=SourcesTarget,
+      sources=['com/foo/E.scala'],
+      scope=Scopes.RUNTIME
+    )
+
     self.lib_e = self.make_target(
       'com/foo:E',
       target_type=SourcesTarget,
       sources=['com/foo/E.scala'],
-      dependencies=[self.lib_d],
+      dependencies=[self.lib_d, self.lib_f],
     )
 
   def test_strict_dependencies(self):
@@ -296,6 +304,7 @@ def test_strict_dependencies(self):
     dep_context.compiler_plugin_types = ()
     dep_context.codegen_types = ()
     dep_context.alias_types = (Target,)
+    dep_context.target_closure_kwargs = {'include_scopes': Scopes.JVM_COMPILE_SCOPES}
     self.assertEqual(set(self.lib_b.strict_dependencies(dep_context)), {self.lib_a, self.lib_aa})
     self.assertEqual(set(self.lib_c.strict_dependencies(dep_context)), {self.lib_b, self.lib_a})
     self.assertEqual(set(self.lib_c_alias.strict_dependencies(dep_context)), {self.lib_c, self.lib_b, self.lib_a})
diff --git a/tests/python/pants_test/projects/test_testprojects_integration.py b/tests/python/pants_test/projects/test_testprojects_integration.py
--- a/tests/python/pants_test/projects/test_testprojects_integration.py
+++ b/tests/python/pants_test/projects/test_testprojects_integration.py
@@ -49,6 +49,7 @@ def targets(self):
       'testprojects/src/java/org/pantsbuild/testproject/missingdepswhitelist.*',
       'testprojects/src/java/org/pantsbuild/testproject/missingdirectdepswhitelist:missingdirectdepswhitelist',
       'testprojects/src/java/org/pantsbuild/testproject/missingjardepswhitelist:missingjardepswhitelist',
+      'testprojects/src/java/org/pantsbuild/testproject/runtime:compile-fail',
       'testprojects/src/scala/org/pantsbuild/testproject/compilation_failure',
       'testprojects/src/scala/org/pantsbuild/testproject/compilation_warnings:fatal',
       'testprojects/src/thrift/org/pantsbuild/thrift_exports:C-without-exports',