1 # Copyright (c) 2017 Mark D. Hill and David A. Wood
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met: redistributions of source code must retain the above copyright
7 # notice, this list of conditions and the following disclaimer;
8 # redistributions in binary form must reproduce the above copyright
9 # notice, this list of conditions and the following disclaimer in the
10 # documentation and/or other materials provided with the distribution;
11 # neither the name of the copyright holders nor the names of its
12 # contributors may be used to endorse or promote products derived from
13 # this software without specific prior written permission.
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 # Authors: Sean Wilson
31 import xml
.sax
.saxutils
33 from config
import config
38 def _create_uid_index(iterable
):
41 assert item
.uid
not in index
42 index
[item
.uid
] = item
46 class _CommonMetadataMixin
:
49 return self
._metadata
.name
52 return self
._metadata
.uid
55 return self
._metadata
.result
57 def result(self
, result
):
58 self
._metadata
.result
= result
61 def unsucessful(self
):
62 return self
._metadata
.result
.value
!= state
.Result
.Passed
65 class InternalTestResult(object, _CommonMetadataMixin
):
66 def __init__(self
, obj
, suite
, directory
):
67 self
._metadata
= obj
.metadata
70 self
.stderr
= os
.path
.join(
71 InternalSavedResults
.output_path(self
.uid
, suite
.uid
),
74 self
.stdout
= os
.path
.join(
75 InternalSavedResults
.output_path(self
.uid
, suite
.uid
),
80 class InternalSuiteResult(object, _CommonMetadataMixin
):
81 def __init__(self
, obj
, directory
):
82 self
._metadata
= obj
.metadata
83 self
.directory
= directory
86 def _wrap_tests(self
, obj
):
87 self
._tests
= [InternalTestResult(test
, self
, self
.directory
)
89 self
._tests
_index
= _create_uid_index(self
._tests
)
91 def get_test(self
, uid
):
92 return self
._tests
_index
[uid
]
95 return iter(self
._tests
)
97 def get_test_result(self
, uid
):
98 return self
.get_test(uid
)
100 def aggregate_test_results(self
):
103 helper
.append_dictlist(results
, test
.result
.value
, test
)
107 class InternalLibraryResults(object, _CommonMetadataMixin
):
108 def __init__(self
, obj
, directory
):
109 self
.directory
= directory
110 self
._metadata
= obj
.metadata
111 self
._wrap
_suites
(obj
)
114 return iter(self
._suites
)
116 def _wrap_suites(self
, obj
):
117 self
._suites
= [InternalSuiteResult(suite
, self
.directory
)
119 self
._suites
_index
= _create_uid_index(self
._suites
)
121 def add_suite(self
, suite
):
122 if suite
.uid
in self
._suites
:
123 raise ValueError('Cannot have duplicate suite UIDs.')
124 self
._suites
[suite
.uid
] = suite
126 def get_suite_result(self
, suite_uid
):
127 return self
._suites
_index
[suite_uid
]
129 def get_test_result(self
, test_uid
, suite_uid
):
130 return self
.get_suite_result(suite_uid
).get_test_result(test_uid
)
132 def aggregate_test_results(self
):
134 for suite
in self
._suites
:
136 helper
.append_dictlist(results
, test
.result
.value
, test
)
139 class InternalSavedResults
:
141 def output_path(test_uid
, suite_uid
, base
=None):
143 Return the path which results for a specific test case should be
147 base
= config
.result_path
150 str(suite_uid
).replace(os
.path
.sep
, '-'),
151 str(test_uid
).replace(os
.path
.sep
, '-'))
154 def save(results
, path
, protocol
=pickle
.HIGHEST_PROTOCOL
):
155 if not os
.path
.exists(os
.path
.dirname(path
)):
157 os
.makedirs(os
.path
.dirname(path
))
158 except OSError as exc
: # Guard against race condition
159 if exc
.errno
!= errno
.EEXIST
:
162 with
open(path
, 'w') as f
:
163 pickle
.dump(results
, f
, protocol
)
167 with
open(path
, 'r') as f
:
168 return pickle
.load(f
)
171 class XMLElement(object):
172 def write(self
, file_
):
176 def begin(self
, file_
):
178 file_
.write(self
.name
)
179 for attr
in self
.attributes
:
186 def body(self
, file_
):
187 for elem
in self
.elements
:
192 def end(self
, file_
):
193 file_
.write('</%s>' % self
.name
)
195 class XMLAttribute(object):
196 def __init__(self
, name
, value
):
200 def write(self
, file_
):
201 file_
.write('%s=%s' % (self
.name
,
202 xml
.sax
.saxutils
.quoteattr(self
.value
)))
205 class JUnitTestSuites(XMLElement
):
208 state
.Result
.Errored
: 'errors',
209 state
.Result
.Failed
: 'failures',
210 state
.Result
.Passed
: 'tests'
213 def __init__(self
, internal_results
):
214 results
= internal_results
.aggregate_test_results()
217 for result
, tests
in results
.items():
218 self
.attributes
.append(self
.result_attribute(result
,
222 for suite
in internal_results
:
223 self
.elements
.append(JUnitTestSuite(suite
))
225 def result_attribute(self
, result
, count
):
226 return XMLAttribute(self
.result_map
[result
], count
)
228 class JUnitTestSuite(JUnitTestSuites
):
231 state
.Result
.Errored
: 'errors',
232 state
.Result
.Failed
: 'failures',
233 state
.Result
.Passed
: 'tests',
234 state
.Result
.Skipped
: 'skipped'
237 def __init__(self
, suite_result
):
238 results
= suite_result
.aggregate_test_results()
241 XMLAttribute('name', suite_result
.name
)
243 for result
, tests
in results
.items():
244 self
.attributes
.append(self
.result_attribute(result
,
248 for test
in suite_result
:
249 self
.elements
.append(JUnitTestCase(test
))
251 def result_attribute(self
, result
, count
):
252 return XMLAttribute(self
.result_map
[result
], count
)
254 class JUnitTestCase(XMLElement
):
256 def __init__(self
, test_result
):
258 XMLAttribute('name', test_result
.name
),
259 # TODO JUnit expects class of test.. add as test metadata.
260 XMLAttribute('classname', str(test_result
.uid
)),
261 XMLAttribute('status', str(test_result
.result
)),
264 # TODO JUnit expects a message for the reason a test was
265 # skipped or errored, save this with the test metadata.
266 # http://llg.cubic.org/docs/junit/
268 LargeFileElement('system-err', test_result
.stderr
),
269 LargeFileElement('system-out', test_result
.stdout
),
272 class LargeFileElement(XMLElement
):
273 def __init__(self
, name
, filename
):
275 self
.filename
= filename
278 def body(self
, file_
):
280 with
open(self
.filename
, 'r') as f
:
282 file_
.write(xml
.sax
.saxutils
.escape(line
))
284 # TODO Better error logic, this is sometimes O.K.
285 # if there was no stdout/stderr captured for the test
287 # TODO If that was the case, the file should still be made and it
288 # should just be empty instead of not existing.
293 class JUnitSavedResults
:
295 def save(results
, path
):
297 Compile the internal results into JUnit format writting it to the
300 results
= JUnitTestSuites(results
)
301 with
open(path
, 'w') as f
: