Repository: pantsbuild/pants

Issue:
Handle failed junit tests with no identifiable target owner more gracefully.
@ericzundel stopped outright blow-ups with #4055 in the 1.2.x branch, but this patch still needs to be applied to master and it seems we could do a better job reporting in these cases with more complex error reporting logic or a null object / change of the key type in the dict returned by `parse_failed_targets` from `Target` to `str`. It seems to be the case that all users of the key immediately reach in to `target.address.spec`.


Discussion:
Thanks @jsirois ! Do you want to tackle the rest of this issue before `1.3.0` (ie, this friday most likely)? Or does it not need to go in `1.3.0` slash should someone else tackle it?
I have a solid slot Thursday am through noon that I can try to knock this out in. I'll give it a go then unless someone else wants it more.

Resolving patch:
diff --git a/src/python/pants/backend/jvm/tasks/junit_run.py b/src/python/pants/backend/jvm/tasks/junit_run.py
--- a/src/python/pants/backend/jvm/tasks/junit_run.py
+++ b/src/python/pants/backend/jvm/tasks/junit_run.py
@@ -434,12 +434,19 @@ def error_handler(parse_error):
                                .format(path=parse_error.junit_xml_path, cause=parse_error.cause))
 
       target_to_failed_test = parse_failed_targets(test_registry, output_dir, error_handler)
-      failed_targets = sorted(target_to_failed_test, key=lambda t: t.address.spec)
+
+      def sort_owning_target(t):
+        return t.address.spec if t else None
+
+      failed_targets = sorted(target_to_failed_test, key=sort_owning_target)
       error_message_lines = []
       if self._failure_summary:
+        def render_owning_target(t):
+          return t.address.spec if t else '<Unknown Target>'
+
         for target in failed_targets:
-          error_message_lines.append('\n{indent}{address}'.format(indent=' ' * 4,
-                                                                  address=target.address.spec))
+          error_message_lines.append('\n{indent}{owner}'.format(indent=' ' * 4,
+                                                                owner=render_owning_target(target)))
           for test in sorted(target_to_failed_test[target]):
             error_message_lines.append('{indent}{classname}#{methodname}'
                                        .format(indent=' ' * 8,
diff --git a/src/python/pants/java/junit/junit_xml_parser.py b/src/python/pants/java/junit/junit_xml_parser.py
--- a/src/python/pants/java/junit/junit_xml_parser.py
+++ b/src/python/pants/java/junit/junit_xml_parser.py
@@ -130,7 +130,8 @@ def cause(self):
 def parse_failed_targets(test_registry, junit_xml_path, error_handler):
   """Parses junit xml reports and maps targets to the set of individual tests that failed.
 
-  Targets with no failed tests are omitted from the returned mapping.
+  Targets with no failed tests are omitted from the returned mapping and failed tests with no
+  identifiable owning target are keyed under `None`.
 
   :param test_registry: A registry of tests that were run.
   :type test_registry: :class:`RegistryOfTests`
@@ -138,7 +139,8 @@ def parse_failed_targets(test_registry, junit_xml_path, error_handler):
                                 to analyze.
   :param error_handler: An error handler that will be called with any junit xml parsing errors.
   :type error_handler: callable that accepts a single :class:`ParseError` argument.
-  :returns: A mapping from targets to the set of individual tests that failed.
+  :returns: A mapping from targets to the set of individual tests that failed. Any failed tests
+            that belong to no identifiable target will be mapped to `None`.
   :rtype: dict from :class:`pants.build_graph.target.Target` to a set of :class:`Test`
   """
   failed_targets = defaultdict(set)
@@ -156,11 +158,7 @@ def parse_junit_xml_file(path):
             test = Test(classname=testcase.getAttribute('classname'),
                         methodname=testcase.getAttribute('name'))
             target = test_registry.get_owning_target(test)
-            # There are some cases where we'll fail to find an owning target; in which case its
-            # better to have partial failure results, than none at all due to an internal error.
-            # See: https://github.com/pantsbuild/pants/pull/4055
-            if target:
-              failed_targets[target].add(test)
+            failed_targets[target].add(test)
     except (XmlParser.XmlError, ValueError) as e:
       error_handler(ParseError(path, e))
 


Test patch:
diff --git a/tests/python/pants_test/java/junit/test_junit_xml_parser.py b/tests/python/pants_test/java/junit/test_junit_xml_parser.py
--- a/tests/python/pants_test/java/junit/test_junit_xml_parser.py
+++ b/tests/python/pants_test/java/junit/test_junit_xml_parser.py
@@ -183,4 +183,4 @@ def test_parse_failed_targets_error_continue(self):
       self.assertEqual(2, len(collect_handler.errors))
       self.assertEqual({bad_file1, bad_file2}, {e.junit_xml_path for e in collect_handler.errors})
 
-      self.assertEqual({}, failed_targets)
+      self.assertEqual({None: {JUnitTest('org.pantsbuild.Error', 'testError')}}, failed_targets)