Skip to content

Commit 883deaf

Browse files
authored
Merge pull request #8 from bosonogi/junit-xml-improvements
jUnit XML improvements
2 parents 7d45057 + 3ecc28e commit 883deaf

File tree

5 files changed

+90
-42
lines changed

5 files changed

+90
-42
lines changed

cwltest/__init__.py

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
import shutil
99
import tempfile
10+
import junit_xml
1011
import ruamel.yaml as yaml
1112
import ruamel.yaml.scanner as yamlscanner
1213
import pipes
@@ -35,6 +36,22 @@ def format(cls, expected, actual, cause=None):
3536
return cls(message)
3637

3738

39+
class TestResult(object):
40+
41+
"""Encapsulate relevant test result data."""
42+
43+
def __init__(self, return_code, standard_output, error_output):
44+
# type: (int, str, str) -> None
45+
self.return_code = return_code
46+
self.standard_output = standard_output
47+
self.error_output = error_output
48+
49+
def create_test_case(self, test):
50+
# type: (Dict[Text, Any]) -> junit_xml.TestCase
51+
doc = test.get(u'doc', 'N/A').strip()
52+
return junit_xml.TestCase(doc, stdout=self.standard_output, stderr=self.error_output)
53+
54+
3855
def compare_file(expected, actual):
3956
# type: (Dict[str,Any], Dict[str,Any]) -> None
4057
if "path" in expected:
@@ -127,9 +144,9 @@ def compare(expected, actual): # type: (Any, Any) -> None
127144
raise CompareFail(str(e))
128145

129146

130-
def run_test(args, i, tests): # type: (argparse.Namespace, int, List[Dict[str, str]]) -> int
147+
def run_test(args, i, tests): # type: (argparse.Namespace, int, List[Dict[str, str]]) -> TestResult
131148
out = {} # type: Dict[str,Any]
132-
outdir, outstr, test_command = None, None, None
149+
outdir = outstr = outerr = test_command = None
133150
t = tests[i]
134151
try:
135152
test_command = [args.tool]
@@ -148,23 +165,32 @@ def run_test(args, i, tests): # type: (argparse.Namespace, int, List[Dict[str,
148165

149166
sys.stderr.write("\rTest [%i/%i] " % (i + 1, len(tests)))
150167
sys.stderr.flush()
151-
outstr = subprocess.check_output(test_command)
168+
169+
process = subprocess.Popen(test_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
170+
outstr, outerr = process.communicate()
171+
return_code = process.poll()
172+
if return_code:
173+
raise subprocess.CalledProcessError(return_code, " ".join(test_command))
174+
152175
out = json.loads(outstr)
153176
except ValueError as v:
154177
_logger.error(str(v))
155178
_logger.error(outstr)
179+
_logger.error(outerr)
156180
except subprocess.CalledProcessError as err:
157181
if err.returncode == UNSUPPORTED_FEATURE:
158-
return UNSUPPORTED_FEATURE
182+
return TestResult(UNSUPPORTED_FEATURE, outstr, outerr)
159183
else:
160184
_logger.error(u"""Test failed: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
161185
_logger.error(t.get("doc"))
162186
_logger.error("Returned non-zero")
163-
return 1
187+
_logger.error(outerr)
188+
return TestResult(1, outstr, outerr)
164189
except (yamlscanner.ScannerError, TypeError) as e:
165190
_logger.error(u"""Test failed: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
166191
_logger.error(outstr)
167192
_logger.error(u"Parse error %s", str(e))
193+
_logger.error(outerr)
168194
except KeyboardInterrupt:
169195
_logger.error(u"""Test interrupted: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
170196
raise
@@ -182,10 +208,7 @@ def run_test(args, i, tests): # type: (argparse.Namespace, int, List[Dict[str,
182208
if outdir:
183209
shutil.rmtree(outdir, True)
184210

185-
if failed:
186-
return 1
187-
else:
188-
return 0
211+
return TestResult((1 if failed else 0), outstr, outerr)
189212

190213

191214
def main(): # type: () -> int
@@ -215,7 +238,8 @@ def main(): # type: () -> int
215238
failures = 0
216239
unsupported = 0
217240
passed = 0
218-
xml_lines = [] # type: List[Text]
241+
suite_name, _ = os.path.splitext(os.path.basename(args.test))
242+
report = junit_xml.TestSuite(suite_name, [])
219243

220244
if args.only_tools:
221245
alltests = tests
@@ -251,28 +275,26 @@ def main(): # type: () -> int
251275
for i in ntest]
252276
try:
253277
for i, job in zip(ntest, jobs):
254-
rt = job.result()
278+
test_result = job.result()
279+
test_case = test_result.create_test_case(tests[i])
255280
total += 1
256-
if rt == 1:
281+
if test_result.return_code == 1:
257282
failures += 1
258-
elif rt == UNSUPPORTED_FEATURE:
283+
test_case.add_failure_info("N/A")
284+
elif test_result.return_code == UNSUPPORTED_FEATURE:
259285
unsupported += 1
286+
test_case.add_skipped_info("Unsupported")
260287
else:
261288
passed += 1
262-
xml_lines += make_xml_lines(tests[i], rt, args.test)
289+
report.test_cases.append(test_case)
263290
except KeyboardInterrupt:
264291
for job in jobs:
265292
job.cancel()
266293
_logger.error("Tests interrupted")
267294

268295
if args.junit_xml:
269296
with open(args.junit_xml, 'w') as fp:
270-
fp.write('<testsuites>\n')
271-
fp.write(' <testsuite name="%s" tests="%s" failures="%s" skipped="%s">\n' % (
272-
args.tool, len(ntest), failures, unsupported))
273-
fp.writelines(xml_lines)
274-
fp.write(' </testsuite>\n')
275-
fp.write('</testsuites>\n')
297+
junit_xml.TestSuite.to_file(fp, [report])
276298

277299
if failures == 0 and unsupported == 0:
278300
_logger.info("All tests passed")
@@ -285,25 +307,5 @@ def main(): # type: () -> int
285307
return 1
286308

287309

288-
def make_xml_lines(test, rt, test_case_group='N/A'):
289-
# type: (Dict[Text, Any], int, Text) -> List[Text]
290-
doc = test.get('doc', 'N/A').strip()
291-
test_case_group = test_case_group.replace(".yaml", "").replace(".yml", "")
292-
elem = ' <testcase name="%s" classname="%s"' % (doc, test_case_group.replace(".", "_"))
293-
if rt == 0:
294-
return [ elem + '/>\n' ]
295-
if rt == UNSUPPORTED_FEATURE:
296-
return [
297-
elem + '>\n',
298-
' <skipped/>\n'
299-
'</testcase>\n',
300-
]
301-
return [
302-
elem + '>\n',
303-
' <failure message="N/A">N/A</failure>\n'
304-
'</testcase>\n',
305-
]
306-
307-
308310
if __name__ == "__main__":
309311
sys.exit(main())

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
schema-salad >= 1.14
22
typing >= 3.5.2
33
futures >= 3.0.5; python_version == '2.7'
4+
junit-xml >= 1.7

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
packages=["cwltest"],
2727
install_requires=[
2828
'schema-salad >= 1.14',
29-
'typing >= 3.5.2'
29+
'typing >= 3.5.2',
30+
'junit-xml >= 1.7'
3031
],
3132
extras_require={
3233
':python_version == "2.7"': [

typeshed/2.7/__builtin__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ class file(BinaryIO):
911911
@overload
912912
def __init__(self, file: unicode, mode: str = 'r', buffering: int = ...) -> None: ...
913913
@overload
914-
def __init__(file: int, mode: str = 'r', buffering: int = ...) -> None: ...
914+
def __init__(self, file: int, mode: str = 'r', buffering: int = ...) -> None: ...
915915
def __iter__(self) -> Iterator[str]: ...
916916
def read(self, n: int = ...) -> str: ...
917917
def __enter__(self) -> BinaryIO: ...

typeshed/2.7/junit_xml.pyi

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Stubs for junit_xml (Python 3.5)
2+
#
3+
# NOTE: This dynamically typed stub was automatically generated by stubgen.
4+
5+
from typing import Any, Optional
6+
7+
unichr = ... # type: Any
8+
9+
def decode(var, encoding): ...
10+
11+
class TestSuite:
12+
name = ... # type: Any
13+
test_cases = ... # type: Any
14+
hostname = ... # type: Any
15+
id = ... # type: Any
16+
package = ... # type: Any
17+
timestamp = ... # type: Any
18+
properties = ... # type: Any
19+
def __init__(self, name, test_cases: Optional[Any] = ..., hostname: Optional[Any] = ..., id: Optional[Any] = ..., package: Optional[Any] = ..., timestamp: Optional[Any] = ..., properties: Optional[Any] = ...) -> None: ...
20+
def build_xml_doc(self, encoding: Optional[Any] = ...): ...
21+
@staticmethod
22+
def to_xml_string(test_suites, prettyprint: bool = ..., encoding: Optional[Any] = ...): ...
23+
@staticmethod
24+
def to_file(file_descriptor, test_suites, prettyprint: bool = ..., encoding: Optional[Any] = ...): ...
25+
26+
class TestCase:
27+
name = ... # type: Any
28+
elapsed_sec = ... # type: Any
29+
stdout = ... # type: Any
30+
stderr = ... # type: Any
31+
classname = ... # type: Any
32+
error_message = ... # type: Any
33+
error_output = ... # type: Any
34+
failure_message = ... # type: Any
35+
failure_output = ... # type: Any
36+
skipped_message = ... # type: Any
37+
skipped_output = ... # type: Any
38+
def __init__(self, name, classname: Optional[Any] = ..., elapsed_sec: Optional[Any] = ..., stdout: Optional[Any] = ..., stderr: Optional[Any] = ...) -> None: ...
39+
def add_error_info(self, message: Optional[Any] = ..., output: Optional[Any] = ...): ...
40+
def add_failure_info(self, message: Optional[Any] = ..., output: Optional[Any] = ...): ...
41+
def add_skipped_info(self, message: Optional[Any] = ..., output: Optional[Any] = ...): ...
42+
def is_failure(self): ...
43+
def is_error(self): ...
44+
def is_skipped(self): ...

0 commit comments

Comments
 (0)