Repository: python/mypy

Issue:
Support writing junit.xml in dmypy
Daemon mode doesn't support the `--junit-xml=FILE` flag -- it is silently ignored. This is because the implementation happens at the very end of `main()` which is never called in daemon mode.

We should probably add this flag to `dmypy check` so multiple check calls can easily be made to write different XML files (handy as CI artifacts). And `dmypy [re]start` should special-case the flag and reject it.


Discussion:

Resolving patch:
diff --git a/mypy/dmypy.py b/mypy/dmypy.py
--- a/mypy/dmypy.py
+++ b/mypy/dmypy.py
@@ -17,6 +17,7 @@
 from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, TypeVar
 
 from mypy.dmypy_util import STATUS_FILE, receive
+from mypy.util import write_junit_xml
 
 # Argument parser.  Subparsers are tied to action functions by the
 # @action(subparse) decorator.
@@ -50,12 +51,14 @@
                                          help="Check some files (requires running daemon)")
 p.add_argument('-v', '--verbose', action='store_true', help="Print detailed status")
 p.add_argument('-q', '--quiet', action='store_true', help=argparse.SUPPRESS)  # Deprecated
+p.add_argument('--junit-xml', help="write junit.xml to the given file")
 p.add_argument('files', metavar='FILE', nargs='+', help="File (or directory) to check")
 
 recheck_parser = p = subparsers.add_parser('recheck',
     help="Check the same files as the most previous  check run (requires running daemon)")
 p.add_argument('-v', '--verbose', action='store_true', help="Print detailed status")
 p.add_argument('-q', '--quiet', action='store_true', help=argparse.SUPPRESS)  # Deprecated
+p.add_argument('--junit-xml', help="write junit.xml to the given file")
 
 hang_parser = p = subparsers.add_parser('hang', help="Hang for 100 seconds")
 
@@ -221,7 +224,7 @@ def do_check(args: argparse.Namespace) -> None:
     response = request('check', files=args.files)
     t1 = time.time()
     response['roundtrip_time'] = t1 - t0
-    check_output(response, args.verbose)
+    check_output(response, args.verbose, args.junit_xml)
 
 
 @action(recheck_parser)
@@ -234,10 +237,10 @@ def do_recheck(args: argparse.Namespace) -> None:
     response = request('recheck')
     t1 = time.time()
     response['roundtrip_time'] = t1 - t0
-    check_output(response, args.verbose)
+    check_output(response, args.verbose, args.junit_xml)
 
 
-def check_output(response: Dict[str, Any], verbose: bool) -> None:
+def check_output(response: Dict[str, Any], verbose: bool, junit_xml: Optional[str]) -> None:
     """Print the output from a check or recheck command.
 
     Call sys.exit() unless the status code is zero.
@@ -252,6 +255,9 @@ def check_output(response: Dict[str, Any], verbose: bool) -> None:
     sys.stderr.write(err)
     if verbose:
         show_stats(response)
+    if junit_xml:
+        messages = (out + err).splitlines()
+        write_junit_xml(response['roundtrip_time'], bool(err), messages, junit_xml)
     if status_code:
         sys.exit(status_code)
 
diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py
--- a/mypy/dmypy_server.py
+++ b/mypy/dmypy_server.py
@@ -95,6 +95,9 @@ def __init__(self, flags: List[str]) -> None:
             sys.exit("dmypy: start/restart does not accept sources")
         if options.report_dirs:
             sys.exit("dmypy: start/restart cannot generate reports")
+        if options.junit_xml:
+            sys.exit("dmypy: start/restart does not support --junit-xml; "
+                     "pass it to check/recheck instead")
         if not options.incremental:
             sys.exit("dmypy: start/restart should not disable incremental mode")
         if options.quick_and_dirty:
@@ -222,6 +225,7 @@ def check_default(self, sources: List[mypy.build.BuildSource],
                       alt_lib_path: Optional[str] = None) -> Dict[str, Any]:
         """Check using the default (per-file) incremental mode."""
         self.last_manager = None
+        blockers = False
         with GcLogger() as gc_result:
             try:
                 # saved_cache is mutated in place.
@@ -231,10 +235,15 @@ def check_default(self, sources: List[mypy.build.BuildSource],
                 msgs = res.errors
                 self.last_manager = res.manager  # type: Optional[mypy.build.BuildManager]
             except mypy.errors.CompileError as err:
+                blockers = True
                 msgs = err.messages
         if msgs:
             msgs.append("")
-            response = {'out': "\n".join(msgs), 'err': "", 'status': 1}
+            text = "\n".join(msgs)
+            if blockers:
+                response = {'out': "", 'err': text, 'status': 2}
+            else:
+                response = {'out': text, 'err': "", 'status': 1}
         else:
             response = {'out': "", 'err': "", 'status': 0}
         response.update(gc_result.get_stats())


Test patch: