blob: 161a2f6e9046f528c2858a46e600157569a98fb2 [file] [log] [blame]
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001#!/usr/bin/env python3
2#
Brad Bishopc342db32019-05-15 21:57:59 -04003# SPDX-License-Identifier: GPL-2.0-or-later
4#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08005# Modified for use in OE by Richard Purdie, 2018
6#
7# Modified by: Corey Goldberg, 2013
8# License: GPLv2+
9#
10# Original code from:
11# Bazaar (bzrlib.tests.__init__.py, v2.6, copied Jun 01 2013)
12# Copyright (C) 2005-2011 Canonical Ltd
13# License: GPLv2+
14
15import os
16import sys
17import traceback
18import unittest
19import subprocess
20import testtools
21import threading
22import time
23import io
Brad Bishop79641f22019-09-10 07:20:22 -040024import json
Brad Bishopc342db32019-05-15 21:57:59 -040025import subunit
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080026
27from queue import Queue
28from itertools import cycle
29from subunit import ProtocolTestCase, TestProtocolClient
30from subunit.test_results import AutoTimingTestResultDecorator
31from testtools import ThreadsafeForwardingResult, iterate_tests
Brad Bishop79641f22019-09-10 07:20:22 -040032from testtools.content import Content
33from testtools.content_type import ContentType
Brad Bishop19323692019-04-05 15:28:33 -040034from oeqa.utils.commands import get_test_layer
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080035
36import bb.utils
37import oe.path
38
39_all__ = [
40 'ConcurrentTestSuite',
41 'fork_for_tests',
42 'partition_tests',
43]
44
45#
46# Patch the version from testtools to allow access to _test_start and allow
47# computation of timing information and threading progress
48#
49class BBThreadsafeForwardingResult(ThreadsafeForwardingResult):
50
Andrew Geissler3b8a17c2021-04-15 15:55:55 -050051 def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests, output, finalresult):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080052 super(BBThreadsafeForwardingResult, self).__init__(target, semaphore)
53 self.threadnum = threadnum
54 self.totalinprocess = totalinprocess
55 self.totaltests = totaltests
Andrew Geissler3b8a17c2021-04-15 15:55:55 -050056 self.buffer = True
57 self.outputbuf = output
58 self.finalresult = finalresult
59 self.finalresult.buffer = True
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080060
61 def _add_result_with_semaphore(self, method, test, *args, **kwargs):
62 self.semaphore.acquire()
63 try:
Brad Bishopc342db32019-05-15 21:57:59 -040064 if self._test_start:
65 self.result.starttime[test.id()] = self._test_start.timestamp()
66 self.result.threadprogress[self.threadnum].append(test.id())
67 totalprogress = sum(len(x) for x in self.result.threadprogress.values())
68 self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s)" % (
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080069 self.threadnum,
70 len(self.result.threadprogress[self.threadnum]),
71 self.totalinprocess,
72 totalprogress,
73 self.totaltests,
74 "{0:.2f}".format(time.time()-self._test_start.timestamp()),
75 test.id())
76 finally:
77 self.semaphore.release()
Andrew Geissler3b8a17c2021-04-15 15:55:55 -050078 self.finalresult._stderr_buffer = io.StringIO(initial_value=self.outputbuf.getvalue().decode("utf-8"))
79 self.finalresult._stdout_buffer = io.StringIO()
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080080 super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs)
81
Brad Bishop79641f22019-09-10 07:20:22 -040082class ProxyTestResult:
83 # a very basic TestResult proxy, in order to modify add* calls
84 def __init__(self, target):
85 self.result = target
Andrew Geissler1e34c2d2020-05-29 16:02:59 -050086 self.failed_tests = 0
Brad Bishop79641f22019-09-10 07:20:22 -040087
Brad Bishop00e122a2019-10-05 11:10:57 -040088 def _addResult(self, method, test, *args, exception = False, **kwargs):
Brad Bishop79641f22019-09-10 07:20:22 -040089 return method(test, *args, **kwargs)
90
Brad Bishop00e122a2019-10-05 11:10:57 -040091 def addError(self, test, err = None, **kwargs):
Andrew Geissler1e34c2d2020-05-29 16:02:59 -050092 self.failed_tests += 1
Brad Bishop00e122a2019-10-05 11:10:57 -040093 self._addResult(self.result.addError, test, err, exception = True, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -040094
Brad Bishop00e122a2019-10-05 11:10:57 -040095 def addFailure(self, test, err = None, **kwargs):
Andrew Geissler1e34c2d2020-05-29 16:02:59 -050096 self.failed_tests += 1
Brad Bishop00e122a2019-10-05 11:10:57 -040097 self._addResult(self.result.addFailure, test, err, exception = True, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -040098
Brad Bishop00e122a2019-10-05 11:10:57 -040099 def addSuccess(self, test, **kwargs):
100 self._addResult(self.result.addSuccess, test, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400101
Brad Bishop00e122a2019-10-05 11:10:57 -0400102 def addExpectedFailure(self, test, err = None, **kwargs):
103 self._addResult(self.result.addExpectedFailure, test, err, exception = True, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400104
Brad Bishop00e122a2019-10-05 11:10:57 -0400105 def addUnexpectedSuccess(self, test, **kwargs):
106 self._addResult(self.result.addUnexpectedSuccess, test, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400107
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500108 def wasSuccessful(self):
109 return self.failed_tests == 0
110
Brad Bishop79641f22019-09-10 07:20:22 -0400111 def __getattr__(self, attr):
112 return getattr(self.result, attr)
113
114class ExtraResultsDecoderTestResult(ProxyTestResult):
Brad Bishop00e122a2019-10-05 11:10:57 -0400115 def _addResult(self, method, test, *args, exception = False, **kwargs):
Brad Bishop79641f22019-09-10 07:20:22 -0400116 if "details" in kwargs and "extraresults" in kwargs["details"]:
117 if isinstance(kwargs["details"]["extraresults"], Content):
118 kwargs = kwargs.copy()
119 kwargs["details"] = kwargs["details"].copy()
120 extraresults = kwargs["details"]["extraresults"]
121 data = bytearray()
122 for b in extraresults.iter_bytes():
123 data += b
124 extraresults = json.loads(data.decode())
125 kwargs["details"]["extraresults"] = extraresults
126 return method(test, *args, **kwargs)
127
128class ExtraResultsEncoderTestResult(ProxyTestResult):
Brad Bishop00e122a2019-10-05 11:10:57 -0400129 def _addResult(self, method, test, *args, exception = False, **kwargs):
Brad Bishop79641f22019-09-10 07:20:22 -0400130 if hasattr(test, "extraresults"):
131 extras = lambda : [json.dumps(test.extraresults).encode()]
132 kwargs = kwargs.copy()
133 if "details" not in kwargs:
134 kwargs["details"] = {}
135 else:
136 kwargs["details"] = kwargs["details"].copy()
137 kwargs["details"]["extraresults"] = Content(ContentType("application", "json", {'charset': 'utf8'}), extras)
Brad Bishop00e122a2019-10-05 11:10:57 -0400138 # if using details, need to encode any exceptions into the details obj,
139 # testtools does not handle "err" and "details" together.
140 if "details" in kwargs and exception and (len(args) >= 1 and args[0] is not None):
141 kwargs["details"]["traceback"] = testtools.content.TracebackContent(args[0], test)
142 args = []
Brad Bishop79641f22019-09-10 07:20:22 -0400143 return method(test, *args, **kwargs)
144
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800145#
Brad Bishopc342db32019-05-15 21:57:59 -0400146# We have to patch subunit since it doesn't understand how to handle addError
147# outside of a running test case. This can happen if classSetUp() fails
148# for a class of tests. This unfortunately has horrible internal knowledge.
149#
150def outSideTestaddError(self, offset, line):
151 """An 'error:' directive has been read."""
152 test_name = line[offset:-1].decode('utf8')
153 self.parser._current_test = subunit.RemotedTestCase(test_name)
154 self.parser.current_test_description = test_name
155 self.parser._state = self.parser._reading_error_details
156 self.parser._reading_error_details.set_simple()
157 self.parser.subunitLineReceived(line)
158
159subunit._OutSideTest.addError = outSideTestaddError
160
Andrew Geissler82c905d2020-04-13 13:39:40 -0500161# Like outSideTestaddError above, we need an equivalent for skips
162# happening at the setUpClass() level, otherwise we will see "UNKNOWN"
163# as a result for concurrent tests
164#
165def outSideTestaddSkip(self, offset, line):
166 """A 'skip:' directive has been read."""
167 test_name = line[offset:-1].decode('utf8')
168 self.parser._current_test = subunit.RemotedTestCase(test_name)
169 self.parser.current_test_description = test_name
170 self.parser._state = self.parser._reading_skip_details
171 self.parser._reading_skip_details.set_simple()
172 self.parser.subunitLineReceived(line)
173
174subunit._OutSideTest.addSkip = outSideTestaddSkip
Brad Bishopc342db32019-05-15 21:57:59 -0400175
176#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800177# A dummy structure to add to io.StringIO so that the .buffer object
178# is available and accepts writes. This allows unittest with buffer=True
179# to interact ok with subunit which wants to access sys.stdout.buffer.
180#
181class dummybuf(object):
182 def __init__(self, parent):
183 self.p = parent
184 def write(self, data):
185 self.p.write(data.decode("utf-8"))
186
187#
188# Taken from testtools.ConncurrencyTestSuite but modified for OE use
189#
190class ConcurrentTestSuite(unittest.TestSuite):
191
Andrew Geissler475cb722020-07-10 16:00:51 -0500192 def __init__(self, suite, processes, setupfunc, removefunc):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800193 super(ConcurrentTestSuite, self).__init__([suite])
194 self.processes = processes
Andrew Geissler82c905d2020-04-13 13:39:40 -0500195 self.setupfunc = setupfunc
Andrew Geissler475cb722020-07-10 16:00:51 -0500196 self.removefunc = removefunc
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800197
198 def run(self, result):
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500199 testservers, totaltests = fork_for_tests(self.processes, self)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800200 try:
201 threads = {}
202 queue = Queue()
203 semaphore = threading.Semaphore(1)
204 result.threadprogress = {}
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500205 for i, (testserver, testnum, output) in enumerate(testservers):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800206 result.threadprogress[i] = []
Brad Bishop79641f22019-09-10 07:20:22 -0400207 process_result = BBThreadsafeForwardingResult(
208 ExtraResultsDecoderTestResult(result),
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500209 semaphore, i, testnum, totaltests, output, result)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800210 reader_thread = threading.Thread(
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500211 target=self._run_test, args=(testserver, process_result, queue))
212 threads[testserver] = reader_thread, process_result
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800213 reader_thread.start()
214 while threads:
215 finished_test = queue.get()
216 threads[finished_test][0].join()
217 del threads[finished_test]
218 except:
219 for thread, process_result in threads.values():
220 process_result.stop()
221 raise
222 finally:
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500223 for testserver in testservers:
224 testserver[0]._stream.close()
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800225
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500226 def _run_test(self, testserver, process_result, queue):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800227 try:
228 try:
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500229 testserver.run(process_result)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800230 except Exception:
231 # The run logic itself failed
232 case = testtools.ErrorHolder(
233 "broken-runner",
234 error=sys.exc_info())
235 case.run(process_result)
236 finally:
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500237 queue.put(testserver)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800238
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800239def fork_for_tests(concurrency_num, suite):
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500240 testservers = []
Brad Bishop19323692019-04-05 15:28:33 -0400241 if 'BUILDDIR' in os.environ:
242 selftestdir = get_test_layer()
243
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800244 test_blocks = partition_tests(suite, concurrency_num)
245 # Clear the tests from the original suite so it doesn't keep them alive
246 suite._tests[:] = []
247 totaltests = sum(len(x) for x in test_blocks)
248 for process_tests in test_blocks:
249 numtests = len(process_tests)
250 process_suite = unittest.TestSuite(process_tests)
251 # Also clear each split list so new suite has only reference
252 process_tests[:] = []
253 c2pread, c2pwrite = os.pipe()
254 # Clear buffers before fork to avoid duplicate output
255 sys.stdout.flush()
256 sys.stderr.flush()
257 pid = os.fork()
258 if pid == 0:
259 ourpid = os.getpid()
260 try:
261 newbuilddir = None
262 stream = os.fdopen(c2pwrite, 'wb', 1)
263 os.close(c2pread)
264
Andrew Geissler82c905d2020-04-13 13:39:40 -0500265 (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800266
267 # Leave stderr and stdout open so we can see test noise
268 # Close stdin so that the child goes away if it decides to
269 # read from stdin (otherwise its a roulette to see what
270 # child actually gets keystrokes for pdb etc).
271 newsi = os.open(os.devnull, os.O_RDWR)
272 os.dup2(newsi, sys.stdin.fileno())
273
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500274 # Send stdout/stderr over the stream
275 os.dup2(c2pwrite, sys.stdout.fileno())
276 os.dup2(c2pwrite, sys.stderr.fileno())
277
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800278 subunit_client = TestProtocolClient(stream)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800279 subunit_result = AutoTimingTestResultDecorator(subunit_client)
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500280 unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800281 if ourpid != os.getpid():
282 os._exit(0)
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500283 if newbuilddir and unittest_result.wasSuccessful():
Andrew Geissler475cb722020-07-10 16:00:51 -0500284 suite.removefunc(newbuilddir)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800285 except:
286 # Don't do anything with process children
287 if ourpid != os.getpid():
288 os._exit(1)
289 # Try and report traceback on stream, but exit with error
290 # even if stream couldn't be created or something else
291 # goes wrong. The traceback is formatted to a string and
292 # written in one go to avoid interleaving lines from
293 # multiple failing children.
294 try:
295 stream.write(traceback.format_exc().encode('utf-8'))
296 except:
297 sys.stderr.write(traceback.format_exc())
298 finally:
299 if newbuilddir:
Andrew Geissler475cb722020-07-10 16:00:51 -0500300 suite.removefunc(newbuilddir)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800301 stream.flush()
302 os._exit(1)
303 stream.flush()
304 os._exit(0)
305 else:
306 os.close(c2pwrite)
307 stream = os.fdopen(c2pread, 'rb', 1)
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500308 # Collect stdout/stderr into an io buffer
309 output = io.BytesIO()
310 testserver = ProtocolTestCase(stream, passthrough=output)
311 testservers.append((testserver, numtests, output))
312 return testservers, totaltests
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800313
314def partition_tests(suite, count):
315 # Keep tests from the same class together but allow tests from modules
316 # to go to different processes to aid parallelisation.
317 modules = {}
318 for test in iterate_tests(suite):
319 m = test.__module__ + "." + test.__class__.__name__
320 if m not in modules:
321 modules[m] = []
322 modules[m].append(test)
323
324 # Simply divide the test blocks between the available processes
325 partitions = [list() for _ in range(count)]
326 for partition, m in zip(cycle(partitions), modules):
327 partition.extend(modules[m])
328
329 # No point in empty threads so drop them
330 return [p for p in partitions if p]
331