Repository: pantsbuild/pants

Issue:
Test reporting errors for empty test targets under --test-pytest-junit-xml-dir 
Success without the option:
```
$ mkdir -p tests/python/pants_test/empty && echo "python_tests()" > tests/python/pants_test/empty/BUILD && ./pants test.pytest --cache-ignore tests/python/pants_test/empty 
17:06:58 [WARN] Globs did not match. Excludes were: []. Unmatched globs were: ["tests/python/pants_test/empty/*_test.py", "tests/python/pants_test/empty/test_*.py"].

11:06:58 00:00 [main]
               (To run a reporting server: ./pants server)
...
11:06:59 00:01   [test]
11:06:59 00:01     [test-jvm-prep-command]
11:06:59 00:01       [jvm_prep_command]
11:06:59 00:01     [test-prep-command]
11:06:59 00:01     [legacy]
11:06:59 00:01     [pytest-prep]
11:06:59 00:01     [pytest]
                   Invalidated 1 target.
                   tests/python/pants_test/empty                                                   .....   SUCCESS
11:07:00 00:02     [junit]
11:07:00 00:02     [go]
11:07:00 00:02     [node]
               Waiting for background workers to finish.
11:07:00 00:02   [complete]
               SUCCESS
```

Failure with:
```
$ mkdir -p tests/python/pants_test/empty && echo "python_tests()" > tests/python/pants_test/empty/BUILD && ./pants test.pytest --cache-ignore tests/python/pants_test/empty --test-pytest-junit-xml-dir=dist/test-results
17:12:26 [WARN] Globs did not match. Excludes were: []. Unmatched globs were: ["tests/python/pants_test/empty/*_test.py", "tests/python/pants_test/empty/test_*.py"].

11:12:26 00:00 [main]
               (To run a reporting server: ./pants server)
...
11:12:28 00:02   [test]
11:12:28 00:02     [test-jvm-prep-command]
11:12:28 00:02       [jvm_prep_command]
11:12:28 00:02     [test-prep-command]
11:12:28 00:02     [legacy]
11:12:28 00:02     [pytest-prep]
11:12:28 00:02     [pytest]
                   Invalidated 1 target.
               Waiting for background workers to finish.
11:12:28 00:02   [complete]
               FAILURE
timestamp: 2019-05-17T11:12:28.446453
Exception caught: (builtins.FileNotFoundError)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/pants_loader.py", line 89, in <module>
    main()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/pants_loader.py", line 85, in main
    PantsLoader.run()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/pants_loader.py", line 81, in run
    cls.load_and_execute(entrypoint)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/pants_loader.py", line 74, in load_and_execute
    entrypoint_main()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/pants_exe.py", line 39, in main
    PantsRunner(exiter, start_time=start_time).run()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/pants_runner.py", line 88, in run
    return runner.run()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/local_pants_runner.py", line 233, in run
    self._run()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/local_pants_runner.py", line 294, in _run
    goal_runner_result = self._maybe_run_v1()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/local_pants_runner.py", line 261, in _maybe_run_v1
    return goal_runner_factory.create().run()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/goal_runner.py", line 209, in run
    return self._run_goals()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/goal_runner.py", line 180, in _run_goals
    result = self._execute_engine()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/goal_runner.py", line 168, in _execute_engine
    result = engine.execute(self._context, self._goals)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/engine/legacy_engine.py", line 26, in execute
    self.attempt(context, goals)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/engine/round_engine.py", line 234, in attempt
    goal_executor.attempt(explain)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/engine/round_engine.py", line 50, in attempt
    task.execute()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/task/testrunner_task_mixin.py", line 143, in execute
    self._execute(all_targets)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/task/testrunner_task_mixin.py", line 478, in _execute
    rv = self._run_partition(fail_fast, partition, *args)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/task/testrunner_task_mixin.py", line 569, in _run_partition
    result = self.run_tests(fail_fast, invalid_test_tgts, *args).checked()
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/backend/python/tasks/pytest_run.py", line 643, in run_tests
    self._expose_results(test_targets, workdirs)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/backend/python/tasks/pytest_run.py", line 661, in _expose_results
    shutil.copy2(junitxml_path, external_junit_xml_dir)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.py37.venv/lib/python3.7/shutil.py", line 263, in copy2
    copyfile(src, dst, follow_symlinks=follow_symlinks)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/build-support/pants_dev_deps.py37.venv/lib/python3.7/shutil.py", line 120, in copyfile
    with open(src, 'rb') as fsrc:

Exception message: [Errno 2] No such file or directory: '/home/jsirois/dev/pantsbuild/jsirois-pants/.pants.d/test/pytest/tests.python.pants_test.empty.empty/junitxml/TEST-tests.python.pants_test.empty.empty.xml'

```


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/tasks/pytest_run.py b/src/python/pants/backend/python/tasks/pytest_run.py
--- a/src/python/pants/backend/python/tasks/pytest_run.py
+++ b/src/python/pants/backend/python/tasks/pytest_run.py
@@ -652,13 +652,14 @@ def collect_files(self, workdirs):
   def _expose_results(self, invalid_tgts, workdirs):
     external_junit_xml_dir = self.get_options().junit_xml_dir
     if external_junit_xml_dir:
-      # Either we just ran pytest for a set of invalid targets and generated a junit xml file
-      # specific to that (sub)set or else we hit the cache for the whole partition and skipped
-      # running pytest, simply retrieving the partition's full junit xml file.
-      junitxml_path = workdirs.junitxml_path(*invalid_tgts)
-
       safe_mkdir(external_junit_xml_dir)
-      shutil.copy2(junitxml_path, external_junit_xml_dir)
+
+      junitxml_path = workdirs.junitxml_path(*invalid_tgts)
+      if os.path.exists(junitxml_path):
+        # Either we just ran pytest for a set of invalid targets and generated a junit xml file
+        # specific to that (sub)set or else we hit the cache for the whole partition and skipped
+        # running pytest, simply retrieving the partition's full junit xml file.
+        shutil.copy2(junitxml_path, external_junit_xml_dir)
 
     if self.get_options().coverage:
       coverage_output_dir = self.get_options().coverage_output_dir


Test patch:
diff --git a/tests/python/pants_test/backend/python/tasks/BUILD b/tests/python/pants_test/backend/python/tasks/BUILD
--- a/tests/python/pants_test/backend/python/tasks/BUILD
+++ b/tests/python/pants_test/backend/python/tasks/BUILD
@@ -111,6 +111,7 @@ python_tests(
   dependencies = [
     '3rdparty/python:coverage',
     '3rdparty/python:future',
+    'src/python/pants/backend/python/targets',
     'src/python/pants/backend/python/tasks',
     'src/python/pants/base:exceptions',
     'src/python/pants/build_graph',
@@ -118,7 +119,6 @@ python_tests(
     'src/python/pants/util:contextutil',
     'src/python/pants/util:dirutil',
     'src/python/pants/util:py2_compat',
-    'src/python/pants/util:process_handler',
     'tests/python/pants_test:task_test_base',
     'tests/python/pants_test/subsystem:subsystem_utils',
     ':python_task_test_base',
diff --git a/tests/python/pants_test/backend/python/tasks/test_pytest_run.py b/tests/python/pants_test/backend/python/tasks/test_pytest_run.py
--- a/tests/python/pants_test/backend/python/tasks/test_pytest_run.py
+++ b/tests/python/pants_test/backend/python/tasks/test_pytest_run.py
@@ -12,6 +12,7 @@
 
 import coverage
 
+from pants.backend.python.targets.python_tests import PythonTests
 from pants.backend.python.tasks.gather_sources import GatherSources
 from pants.backend.python.tasks.pytest_prep import PytestPrep
 from pants.backend.python.tasks.pytest_run import PytestResult, PytestRun
@@ -844,3 +845,20 @@ def test_options_shlexed(self):
       assert_test_not_run('test_three')
       assert_test_run('test_four')
       assert_test_not_run('test_five')
+
+  @contextmanager
+  def run_with_junit_xml_dir(self, targets):
+    with temporary_dir() as dist:
+      junit_xml_dir = os.path.join(dist, 'test-results')
+      self.run_tests(targets, junit_xml_dir=junit_xml_dir)
+      assert os.path.exists(junit_xml_dir)
+      yield os.listdir(junit_xml_dir)
+
+  def test_junit_xml_dir(self):
+    with self.run_with_junit_xml_dir([self.green]) as junit_xml_files:
+      assert ['TEST-{}.xml'.format(self.green.id)] == junit_xml_files
+
+  def test_issue_7749(self):
+    empty_test_target = self.make_target(spec='empty', target_type=PythonTests)
+    with self.run_with_junit_xml_dir([empty_test_target]) as junit_xml_files:
+      assert [] == junit_xml_files