Skip to content

Commit

Permalink
Removed dependency to xunitparser because its packaging relies on `…
Browse files Browse the repository at this point in the history
…use_2to3` which is not supported anymore in setuptools. Fixes #18
  • Loading branch information
Sylvain MARIE committed Sep 17, 2021
1 parent c973e7d commit 7c3d361
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 22 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 1.0.5 - Bugfix

- Removed dependency to `xunitparser` because its packaging relies on `use_2to3` which is not supported anymore in setuptools. Fixes [#18](https://github.com/smarie/python-genbadge/issues/18).

### 1.0.4 - Bugfix

- `genbadge coverage`: fixed `ZeroDivisionError` when `coverage.xml` contains 0 branches (in particular when `--no-branch` option is set). Fixes [#15](https://github.com/smarie/python-genbadge/issues/15)
Expand Down
4 changes: 3 additions & 1 deletion genbadge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@
__all__ = [
'__version__',
# submodules
'main', 'utils_junit', 'utils_coverage', 'utils_flake8', 'utils_badge', 'Badge'
'main', 'utils_junit', 'utils_coverage', 'utils_flake8', 'utils_badge', 'xunitparser_copy',
# symbols
'Badge'
]
22 changes: 4 additions & 18 deletions genbadge/utils_junit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,8 @@
except ImportError:
pass

try:
# xunitparser is an optional dependency, do not fail too soon if it cant be loaded
import xunitparser
# security patch: see https://docs.python.org/3/library/xml.etree.elementtree.html
# to remove when https://github.com/laurentb/xunitparser/issues/14 is fixed
from defusedxml import ElementTree
xunitparser.ElementTree = ElementTree
except ImportError as e:
ee = e # save it
class FakeXunitParserImport(object): # noqa
def __getattribute__(self, item):
raise ImportError("Could not import `xunitparser` or `defusedxml` module, please install it. "
"Note that all dependencies for the tests command can be installed with "
"`pip install genbadge[tests]`. Caught: %r" % ee)
xunitparser = FakeXunitParserImport()

# use our own copy so as to use defusedxml and to be compliant with setuptools>=58
from .xunitparser_copy import parse
from .utils_badge import Badge


Expand Down Expand Up @@ -75,10 +61,10 @@ def get_test_stats(junit_xml_file='reports/junit/junit.xml' # type: Union[str,
if isinstance(junit_xml_file, str):
# assume a file path
with open(junit_xml_file) as f:
ts, tr = xunitparser.parse(f)
ts, tr = parse(f)
else:
# assume a stream already
ts, tr = xunitparser.parse(junit_xml_file)
ts, tr = parse(junit_xml_file)

runned = tr.testsRun
skipped = len(tr.skipped)
Expand Down
229 changes: 229 additions & 0 deletions genbadge/xunitparser_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
""" A copy of xunitparser, to temporary fix the issue in https://github.com/smarie/python-genbadge/issues/18 """
import math
import unittest
from datetime import timedelta

try:
# security patch: see https://docs.python.org/3/library/xml.etree.elementtree.html
from defusedxml import ElementTree
except ImportError as e:
ee = e # save it
class FakeDefusedXmlImport(object): # noqa
def __getattribute__(self, item):
raise ImportError("Could not import `defusedxml.ElementTree`, please install `defusedxml`. "
"Note that all dependencies for the tests command can be installed with "
"`pip install genbadge[tests]`. Caught: %r" % ee)
ElementTree = FakeDefusedXmlImport()


def to_timedelta(val):
if val is None:
return None

secs = float(val)
if math.isnan(secs):
return None

return timedelta(seconds=secs)


class TestResult(unittest.TestResult):
def _exc_info_to_string(self, err, test):
err = (e for e in err if e)
return ': '.join(err)


class TestCase(unittest.TestCase):
TR_CLASS = TestResult
stdout = None
stderr = None

def __init__(self, classname, methodname):
super(TestCase, self).__init__()
self.classname = classname
self.methodname = methodname

def __str__(self):
return "%s (%s)" % (self.methodname, self.classname)

def __repr__(self):
return "<%s testMethod=%s>" % \
(self.classname, self.methodname)

def __hash__(self):
return hash((type(self), self.classname, self.methodname))

def id(self):
return "%s.%s" % (self.classname, self.methodname)

def seed(self, result, typename=None, message=None, trace=None):
""" Provide the expected result """
self.result, self.typename, self.message, self.trace = result, typename, message, trace

def run(self, tr=None):
""" Fake run() that produces the seeded result """
tr = tr or self.TR_CLASS()

tr.startTest(self)
if self.result == 'success':
tr.addSuccess(self)
elif self.result == 'skipped':
tr.addSkip(self, '%s: %s' % (self.typename, self._textMessage()))
elif self.result == 'error':
tr.addError(self, (self.typename, self._textMessage()))
elif self.result == 'failure':
tr.addFailure(self, (self.typename, self._textMessage()))
tr.stopTest(self)

return tr

def _textMessage(self):
msg = (e for e in (self.message, self.trace) if e)
return '\n\n'.join(msg) or None

@property
def alltext(self):
err = (e for e in (self.typename, self.message) if e)
err = ': '.join(err)
txt = (e for e in (err, self.trace) if e)
return '\n\n'.join(txt) or None

def setUp(self):
""" Dummy method so __init__ does not fail """
pass

def tearDown(self):
""" Dummy method so __init__ does not fail """
pass

def runTest(self):
""" Dummy method so __init__ does not fail """
self.run()

@property
def basename(self):
return self.classname.rpartition('.')[2]

@property
def success(self):
return self.result == 'success'

@property
def skipped(self):
return self.result == 'skipped'

@property
def failed(self):
return self.result == 'failure'

@property
def errored(self):
return self.result == 'error'

@property
def good(self):
return self.skipped or self.success

@property
def bad(self):
return not self.good

@property
def stdall(self):
""" All system output """
return '\n'.join([out for out in (self.stdout, self.stderr) if out])


class TestSuite(unittest.TestSuite):
def __init__(self, *args, **kwargs):
super(TestSuite, self).__init__(*args, **kwargs)
self.properties = {}
self.stdout = None
self.stderr = None


class Parser(object):
TC_CLASS = TestCase
TS_CLASS = TestSuite
TR_CLASS = TestResult

def parse(self, source):
xml = ElementTree.parse(source)
root = xml.getroot()
return self.parse_root(root)

def parse_root(self, root):
ts = self.TS_CLASS()
if root.tag == 'testsuites':
for subroot in root:
self.parse_testsuite(subroot, ts)
else:
self.parse_testsuite(root, ts)

tr = ts.run(self.TR_CLASS())

tr.time = to_timedelta(root.attrib.get('time'))

# check totals if they are in the root XML element
if 'errors' in root.attrib:
assert len(tr.errors) == int(root.attrib['errors'])
if 'failures' in root.attrib:
assert len(tr.failures) == int(root.attrib['failures'])
if 'skip' in root.attrib:
assert len(tr.skipped) == int(root.attrib['skip'])
if 'tests' in root.attrib:
assert len(list(ts)) == int(root.attrib['tests'])

return (ts, tr)

def parse_testsuite(self, root, ts):
assert root.tag == 'testsuite'
ts.name = root.attrib.get('name')
ts.package = root.attrib.get('package')
for el in root:
if el.tag == 'testcase':
self.parse_testcase(el, ts)
if el.tag == 'properties':
self.parse_properties(el, ts)
if el.tag == 'system-out' and el.text:
ts.stdout = el.text.strip()
if el.tag == 'system-err' and el.text:
ts.stderr = el.text.strip()

def parse_testcase(self, el, ts):
tc_classname = el.attrib.get('classname') or ts.name
tc = self.TC_CLASS(tc_classname, el.attrib['name'])
tc.seed('success', trace=el.text or None)
tc.time = to_timedelta(el.attrib.get('time'))
message = None
text = None
for e in el:
# error takes over failure in JUnit 4
if e.tag in ('failure', 'error', 'skipped'):
tc = self.TC_CLASS(tc_classname, el.attrib['name'])
result = e.tag
typename = e.attrib.get('type')

# reuse old if empty
message = e.attrib.get('message') or message
text = e.text or text

tc.seed(result, typename, message, text)
tc.time = to_timedelta(el.attrib.get('time'))
if e.tag == 'system-out' and e.text:
tc.stdout = e.text.strip()
if e.tag == 'system-err' and e.text:
tc.stderr = e.text.strip()

# add either the original "success" tc or a tc created by elements
ts.addTest(tc)

def parse_properties(self, el, ts):
for e in el:
if e.tag == 'property':
assert e.attrib['name'] not in ts.properties
ts.properties[e.attrib['name']] = e.attrib['value']


def parse(source):
return Parser().parse(source)
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def tests(session: PowerSession, coverage, pkg_specs):
session.run2("python -m pytest --cache-clear -v %s/tests/" % pkg_name)
else:
# coverage + junit html reports + badge generation
session.install_reqs(phase="coverage", phase_reqs=["coverage", "pytest-html", "requests", "xunitparser"],
session.install_reqs(phase="coverage", phase_reqs=["coverage", "pytest-html", "requests"],
versions_dct=pkg_specs)

# --coverage + junit html reports
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ exclude =
[options.extras_require]
tests =
defusedxml
xunitparser
; xunitparser
coverage =
defusedxml
flake8 =
flake8-html
all =
defusedxml
xunitparser
; xunitparser
flake8-html

# -------------- Packaging -----------
Expand Down

0 comments on commit 7c3d361

Please sign in to comment.