Skip to content

Commit d0ea126

Browse files
authored
Add new flag '--junit-xml=PATH' to write a junit.xml file (#2326)
This format is used by CI systems. We create a single "test" representing the entire mypy run.
1 parent 7d5e3ca commit d0ea126

File tree

4 files changed

+52
-3
lines changed

4 files changed

+52
-3
lines changed

mypy/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import os
66
import re
77
import sys
8+
import time
89

910
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple
1011

1112
from mypy import build
1213
from mypy import defaults
1314
from mypy import git
1415
from mypy import experiments
16+
from mypy import util
1517
from mypy.build import BuildSource, BuildResult, PYTHON_EXTENSIONS
1618
from mypy.errors import CompileError
1719
from mypy.options import Options, BuildType
@@ -28,20 +30,25 @@ def main(script_path: str) -> None:
2830
Args:
2931
script_path: Path to the 'mypy' script (used for finding data files).
3032
"""
33+
t0 = time.time()
3134
if script_path:
3235
bin_dir = find_bin_directory(script_path)
3336
else:
3437
bin_dir = None
3538
sources, options = process_options(sys.argv[1:])
36-
f = sys.stdout
39+
serious = False
3740
try:
3841
res = type_check_only(sources, bin_dir, options)
3942
a = res.errors
4043
except CompileError as e:
4144
a = e.messages
4245
if not e.use_stdout:
43-
f = sys.stderr
46+
serious = True
47+
if options.junit_xml:
48+
t1 = time.time()
49+
util.write_junit_xml(t1 - t0, serious, a, options.junit_xml)
4450
if a:
51+
f = sys.stderr if serious else sys.stdout
4552
for m in a:
4653
f.write(m + '\n')
4754
sys.exit(1)
@@ -182,6 +189,7 @@ def process_options(args: List[str],
182189
"(experimental -- read documentation before using!). "
183190
"Implies --strict-optional. Has the undesirable side-effect of "
184191
"suppressing other errors in non-whitelisted files.")
192+
parser.add_argument('--junit-xml', help="write junit.xml to the given file")
185193
parser.add_argument('--pdb', action='store_true', help="invoke pdb on fatal error")
186194
parser.add_argument('--show-traceback', '--tb', action='store_true',
187195
help="show traceback on fatal error")
@@ -443,6 +451,7 @@ def get_init_file(dir: str) -> Optional[str]:
443451
'python_version': lambda s: tuple(map(int, s.split('.'))),
444452
'strict_optional_whitelist': lambda s: s.split(),
445453
'custom_typing_module': str,
454+
'junit_xml': str,
446455
}
447456

448457

mypy/options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ def __init__(self) -> None:
7979
# Config file name
8080
self.config_file = None # type: Optional[str]
8181

82+
# Write junit.xml to given file
83+
self.junit_xml = None # type: Optional[str]
84+
8285
# Per-file options (raw)
8386
self.per_file_options = {} # type: Dict[str, Dict[str, object]]
8487

mypy/util.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import subprocess
5+
from xml.sax.saxutils import escape
56
from typing import TypeVar, List, Tuple, Optional, Sequence
67

78

@@ -113,3 +114,39 @@ def try_find_python2_interpreter() -> Optional[str]:
113114
except OSError:
114115
pass
115116
return None
117+
118+
119+
PASS_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
120+
<testsuite errors="0" failures="0" name="mypy" skips="0" tests="1" time="{time:.3f}">
121+
<testcase classname="mypy" file="mypy" line="1" name="mypy" time="{time:.3f}">
122+
</testcase>
123+
</testsuite>
124+
"""
125+
126+
FAIL_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
127+
<testsuite errors="0" failures="1" name="mypy" skips="0" tests="1" time="{time:.3f}">
128+
<testcase classname="mypy" file="mypy" line="1" name="mypy" time="{time:.3f}">
129+
<failure message="mypy produced messages">{text}</failure>
130+
</testcase>
131+
</testsuite>
132+
"""
133+
134+
ERROR_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
135+
<testsuite errors="1" failures="0" name="mypy" skips="0" tests="1" time="{time:.3f}">
136+
<testcase classname="mypy" file="mypy" line="1" name="mypy" time="{time:.3f}">
137+
<error message="mypy produced errors">{text}</error>
138+
</testcase>
139+
</testsuite>
140+
"""
141+
142+
143+
def write_junit_xml(dt: float, serious: bool, messages: List[str], path: str) -> None:
144+
"""XXX"""
145+
if not messages and not serious:
146+
xml = PASS_TEMPLATE.format(time=dt)
147+
elif not serious:
148+
xml = FAIL_TEMPLATE.format(text=escape('\n'.join(messages)), time=dt)
149+
else:
150+
xml = ERROR_TEMPLATE.format(text=escape('\n'.join(messages)), time=dt)
151+
with open(path, 'wb') as f:
152+
f.write(xml.encode('utf-8'))

0 commit comments

Comments
 (0)