Coverage for genbadge/xunitparser_copy.py: 70%

155 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-11-10 20:37 +0000

1""" A copy of xunitparser, to temporary fix the issue in https://github.com/smarie/python-genbadge/issues/18 """ 

2import math 

3import unittest 

4from datetime import timedelta 

5 

6try: 

7 # security patch: see https://docs.python.org/3/library/xml.etree.elementtree.html 

8 from defusedxml import ElementTree 

9except ImportError as e: 

10 ee = e # save it 

11 class FakeDefusedXmlImport(object): # noqa 

12 def __getattribute__(self, item): 

13 raise ImportError("Could not import `defusedxml.ElementTree`, please install `defusedxml`. " 

14 "Note that all dependencies for the tests command can be installed with " 

15 "`pip install genbadge[tests]`. Caught: %r" % ee) 

16 ElementTree = FakeDefusedXmlImport() 

17 

18 

19def to_timedelta(val): 

20 if val is None: 

21 return None 

22 

23 secs = float(val) 

24 if math.isnan(secs): 24 ↛ 25line 24 didn't jump to line 25, because the condition on line 24 was never true

25 return None 

26 

27 return timedelta(seconds=secs) 

28 

29 

30class TestResult(unittest.TestResult): 

31 def _exc_info_to_string(self, err, test): 

32 err = (e for e in err if e) 

33 return ': '.join(err) 

34 

35 

36class TestCase(unittest.TestCase): 

37 TR_CLASS = TestResult 

38 stdout = None 

39 stderr = None 

40 

41 def __init__(self, classname, methodname): 

42 super(TestCase, self).__init__() 

43 self.classname = classname 

44 self.methodname = methodname 

45 

46 def __str__(self): 

47 return "%s (%s)" % (self.methodname, self.classname) 

48 

49 def __repr__(self): 

50 return "<%s testMethod=%s>" % \ 

51 (self.classname, self.methodname) 

52 

53 def __hash__(self): 

54 return hash((type(self), self.classname, self.methodname)) 

55 

56 def id(self): 

57 return "%s.%s" % (self.classname, self.methodname) 

58 

59 def seed(self, result, typename=None, message=None, trace=None): 

60 """ Provide the expected result """ 

61 self.result, self.typename, self.message, self.trace = result, typename, message, trace 

62 

63 def run(self, tr=None): 

64 """ Fake run() that produces the seeded result """ 

65 tr = tr or self.TR_CLASS() 

66 

67 tr.startTest(self) 

68 if self.result == 'success': 

69 tr.addSuccess(self) 

70 elif self.result == 'skipped': 

71 tr.addSkip(self, '%s: %s' % (self.typename, self._textMessage())) 

72 elif self.result == 'error': 

73 tr.addError(self, (self.typename, self._textMessage())) 

74 elif self.result == 'failure': 74 ↛ 76line 74 didn't jump to line 76, because the condition on line 74 was never false

75 tr.addFailure(self, (self.typename, self._textMessage())) 

76 tr.stopTest(self) 

77 

78 return tr 

79 

80 def _textMessage(self): 

81 msg = (e for e in (self.message, self.trace) if e) 

82 return '\n\n'.join(msg) or None 

83 

84 @property 

85 def alltext(self): 

86 err = (e for e in (self.typename, self.message) if e) 

87 err = ': '.join(err) 

88 txt = (e for e in (err, self.trace) if e) 

89 return '\n\n'.join(txt) or None 

90 

91 def setUp(self): 

92 """ Dummy method so __init__ does not fail """ 

93 pass 

94 

95 def tearDown(self): 

96 """ Dummy method so __init__ does not fail """ 

97 pass 

98 

99 def runTest(self): 

100 """ Dummy method so __init__ does not fail """ 

101 self.run() 

102 

103 @property 

104 def basename(self): 

105 return self.classname.rpartition('.')[2] 

106 

107 @property 

108 def success(self): 

109 return self.result == 'success' 

110 

111 @property 

112 def skipped(self): 

113 return self.result == 'skipped' 

114 

115 @property 

116 def failed(self): 

117 return self.result == 'failure' 

118 

119 @property 

120 def errored(self): 

121 return self.result == 'error' 

122 

123 @property 

124 def good(self): 

125 return self.skipped or self.success 

126 

127 @property 

128 def bad(self): 

129 return not self.good 

130 

131 @property 

132 def stdall(self): 

133 """ All system output """ 

134 return '\n'.join([out for out in (self.stdout, self.stderr) if out]) 

135 

136 

137class TestSuite(unittest.TestSuite): 

138 def __init__(self, *args, **kwargs): 

139 super(TestSuite, self).__init__(*args, **kwargs) 

140 self.properties = {} 

141 self.stdout = None 

142 self.stderr = None 

143 

144 

145class Parser(object): 

146 TC_CLASS = TestCase 

147 TS_CLASS = TestSuite 

148 TR_CLASS = TestResult 

149 

150 def parse(self, source): 

151 xml = ElementTree.parse(source) 

152 root = xml.getroot() 

153 return self.parse_root(root) 

154 

155 def parse_root(self, root): 

156 ts = self.TS_CLASS() 

157 if root.tag == 'testsuites': 157 ↛ 161line 157 didn't jump to line 161, because the condition on line 157 was never false

158 for subroot in root: 

159 self.parse_testsuite(subroot, ts) 

160 else: 

161 self.parse_testsuite(root, ts) 

162 

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

164 

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

166 

167 # check totals if they are in the root XML element 

168 if 'errors' in root.attrib: 168 ↛ 169line 168 didn't jump to line 169, because the condition on line 168 was never true

169 assert len(tr.errors) == int(root.attrib['errors']) 

170 if 'failures' in root.attrib: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true

171 assert len(tr.failures) == int(root.attrib['failures']) 

172 if 'skip' in root.attrib: 172 ↛ 173line 172 didn't jump to line 173, because the condition on line 172 was never true

173 assert len(tr.skipped) == int(root.attrib['skip']) 

174 if 'tests' in root.attrib: 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true

175 assert len(list(ts)) == int(root.attrib['tests']) 

176 

177 return (ts, tr) 

178 

179 def parse_testsuite(self, root, ts): 

180 assert root.tag == 'testsuite' 

181 ts.name = root.attrib.get('name') 

182 ts.package = root.attrib.get('package') 

183 for el in root: 

184 if el.tag == 'testcase': 184 ↛ 186line 184 didn't jump to line 186, because the condition on line 184 was never false

185 self.parse_testcase(el, ts) 

186 if el.tag == 'properties': 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true

187 self.parse_properties(el, ts) 

188 if el.tag == 'system-out' and el.text: 188 ↛ 189line 188 didn't jump to line 189, because the condition on line 188 was never true

189 ts.stdout = el.text.strip() 

190 if el.tag == 'system-err' and el.text: 190 ↛ 191line 190 didn't jump to line 191, because the condition on line 190 was never true

191 ts.stderr = el.text.strip() 

192 

193 def parse_testcase(self, el, ts): 

194 tc_classname = el.attrib.get('classname') or ts.name 

195 tc = self.TC_CLASS(tc_classname, el.attrib['name']) 

196 tc.seed('success', trace=el.text or None) 

197 tc.time = to_timedelta(el.attrib.get('time')) 

198 message = None 

199 text = None 

200 for e in el: 

201 # error takes over failure in JUnit 4 

202 if e.tag in ('failure', 'error', 'skipped'): 202 ↛ 213line 202 didn't jump to line 213, because the condition on line 202 was never false

203 tc = self.TC_CLASS(tc_classname, el.attrib['name']) 

204 result = e.tag 

205 typename = e.attrib.get('type') 

206 

207 # reuse old if empty 

208 message = e.attrib.get('message') or message 

209 text = e.text or text 

210 

211 tc.seed(result, typename, message, text) 

212 tc.time = to_timedelta(el.attrib.get('time')) 

213 if e.tag == 'system-out' and e.text: 213 ↛ 214line 213 didn't jump to line 214, because the condition on line 213 was never true

214 tc.stdout = e.text.strip() 

215 if e.tag == 'system-err' and e.text: 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true

216 tc.stderr = e.text.strip() 

217 

218 # add either the original "success" tc or a tc created by elements 

219 ts.addTest(tc) 

220 

221 def parse_properties(self, el, ts): 

222 for e in el: 

223 if e.tag == 'property': 

224 assert e.attrib['name'] not in ts.properties 

225 ts.properties[e.attrib['name']] = e.attrib['value'] 

226 

227 

228def parse(source): 

229 return Parser().parse(source)