blob: 4f77589b005305dc9eec541f57846b4583b9c82f [file] [log] [blame]
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001#!/usr/bin/env python3
2#
Patrick Williams92b42cb2022-09-03 06:53:57 -05003# Copyright OpenEmbedded Contributors
4#
Brad Bishopc342db32019-05-15 21:57:59 -04005# SPDX-License-Identifier: GPL-2.0-or-later
6#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08007# Modified for use in OE by Richard Purdie, 2018
8#
9# Modified by: Corey Goldberg, 2013
10# License: GPLv2+
11#
12# Original code from:
13# Bazaar (bzrlib.tests.__init__.py, v2.6, copied Jun 01 2013)
14# Copyright (C) 2005-2011 Canonical Ltd
15# License: GPLv2+
16
17import os
18import sys
19import traceback
20import unittest
21import subprocess
22import testtools
23import threading
24import time
25import io
Brad Bishop79641f22019-09-10 07:20:22 -040026import json
Brad Bishopc342db32019-05-15 21:57:59 -040027import subunit
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080028
29from queue import Queue
30from itertools import cycle
31from subunit import ProtocolTestCase, TestProtocolClient
32from subunit.test_results import AutoTimingTestResultDecorator
33from testtools import ThreadsafeForwardingResult, iterate_tests
Brad Bishop79641f22019-09-10 07:20:22 -040034from testtools.content import Content
35from testtools.content_type import ContentType
Brad Bishop19323692019-04-05 15:28:33 -040036from oeqa.utils.commands import get_test_layer
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080037
38import bb.utils
39import oe.path
40
41_all__ = [
42 'ConcurrentTestSuite',
43 'fork_for_tests',
44 'partition_tests',
45]
46
47#
48# Patch the version from testtools to allow access to _test_start and allow
49# computation of timing information and threading progress
50#
51class BBThreadsafeForwardingResult(ThreadsafeForwardingResult):
52
Andrew Geissler3b8a17c2021-04-15 15:55:55 -050053 def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests, output, finalresult):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080054 super(BBThreadsafeForwardingResult, self).__init__(target, semaphore)
55 self.threadnum = threadnum
56 self.totalinprocess = totalinprocess
57 self.totaltests = totaltests
Andrew Geissler3b8a17c2021-04-15 15:55:55 -050058 self.buffer = True
59 self.outputbuf = output
60 self.finalresult = finalresult
61 self.finalresult.buffer = True
Andrew Geissler517393d2023-01-13 08:55:19 -060062 self.target = target
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080063
64 def _add_result_with_semaphore(self, method, test, *args, **kwargs):
65 self.semaphore.acquire()
66 try:
Brad Bishopc342db32019-05-15 21:57:59 -040067 if self._test_start:
68 self.result.starttime[test.id()] = self._test_start.timestamp()
69 self.result.threadprogress[self.threadnum].append(test.id())
70 totalprogress = sum(len(x) for x in self.result.threadprogress.values())
Andrew Geissler517393d2023-01-13 08:55:19 -060071 self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s failed) (%s)" % (
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080072 self.threadnum,
73 len(self.result.threadprogress[self.threadnum]),
74 self.totalinprocess,
75 totalprogress,
76 self.totaltests,
77 "{0:.2f}".format(time.time()-self._test_start.timestamp()),
Andrew Geissler517393d2023-01-13 08:55:19 -060078 self.target.failed_tests,
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080079 test.id())
80 finally:
81 self.semaphore.release()
Andrew Geissler3b8a17c2021-04-15 15:55:55 -050082 self.finalresult._stderr_buffer = io.StringIO(initial_value=self.outputbuf.getvalue().decode("utf-8"))
83 self.finalresult._stdout_buffer = io.StringIO()
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080084 super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs)
85
Brad Bishop79641f22019-09-10 07:20:22 -040086class ProxyTestResult:
87 # a very basic TestResult proxy, in order to modify add* calls
88 def __init__(self, target):
89 self.result = target
Andrew Geissler1e34c2d2020-05-29 16:02:59 -050090 self.failed_tests = 0
Brad Bishop79641f22019-09-10 07:20:22 -040091
Brad Bishop00e122a2019-10-05 11:10:57 -040092 def _addResult(self, method, test, *args, exception = False, **kwargs):
Brad Bishop79641f22019-09-10 07:20:22 -040093 return method(test, *args, **kwargs)
94
Brad Bishop00e122a2019-10-05 11:10:57 -040095 def addError(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.addError, test, err, exception = True, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -040098
Brad Bishop00e122a2019-10-05 11:10:57 -040099 def addFailure(self, test, err = None, **kwargs):
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500100 self.failed_tests += 1
Brad Bishop00e122a2019-10-05 11:10:57 -0400101 self._addResult(self.result.addFailure, test, err, exception = True, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400102
Brad Bishop00e122a2019-10-05 11:10:57 -0400103 def addSuccess(self, test, **kwargs):
104 self._addResult(self.result.addSuccess, test, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400105
Brad Bishop00e122a2019-10-05 11:10:57 -0400106 def addExpectedFailure(self, test, err = None, **kwargs):
107 self._addResult(self.result.addExpectedFailure, test, err, exception = True, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400108
Brad Bishop00e122a2019-10-05 11:10:57 -0400109 def addUnexpectedSuccess(self, test, **kwargs):
110 self._addResult(self.result.addUnexpectedSuccess, test, **kwargs)
Brad Bishop79641f22019-09-10 07:20:22 -0400111
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500112 def wasSuccessful(self):
113 return self.failed_tests == 0
114
Brad Bishop79641f22019-09-10 07:20:22 -0400115 def __getattr__(self, attr):
116 return getattr(self.result, attr)
117
118class ExtraResultsDecoderTestResult(ProxyTestResult):
Brad Bishop00e122a2019-10-05 11:10:57 -0400119 def _addResult(self, method, test, *args, exception = False, **kwargs):
Brad Bishop79641f22019-09-10 07:20:22 -0400120 if "details" in kwargs and "extraresults" in kwargs["details"]:
121 if isinstance(kwargs["details"]["extraresults"], Content):
122 kwargs = kwargs.copy()
123 kwargs["details"] = kwargs["details"].copy()
124 extraresults = kwargs["details"]["extraresults"]
125 data = bytearray()
126 for b in extraresults.iter_bytes():
127 data += b
128 extraresults = json.loads(data.decode())
129 kwargs["details"]["extraresults"] = extraresults
130 return method(test, *args, **kwargs)
131
132class ExtraResultsEncoderTestResult(ProxyTestResult):
Brad Bishop00e122a2019-10-05 11:10:57 -0400133 def _addResult(self, method, test, *args, exception = False, **kwargs):
Brad Bishop79641f22019-09-10 07:20:22 -0400134 if hasattr(test, "extraresults"):
135 extras = lambda : [json.dumps(test.extraresults).encode()]
136 kwargs = kwargs.copy()
137 if "details" not in kwargs:
138 kwargs["details"] = {}
139 else:
140 kwargs["details"] = kwargs["details"].copy()
141 kwargs["details"]["extraresults"] = Content(ContentType("application", "json", {'charset': 'utf8'}), extras)
Brad Bishop00e122a2019-10-05 11:10:57 -0400142 # if using details, need to encode any exceptions into the details obj,
143 # testtools does not handle "err" and "details" together.
144 if "details" in kwargs and exception and (len(args) >= 1 and args[0] is not None):
145 kwargs["details"]["traceback"] = testtools.content.TracebackContent(args[0], test)
146 args = []
Brad Bishop79641f22019-09-10 07:20:22 -0400147 return method(test, *args, **kwargs)
148
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800149#
Brad Bishopc342db32019-05-15 21:57:59 -0400150# We have to patch subunit since it doesn't understand how to handle addError
151# outside of a running test case. This can happen if classSetUp() fails
152# for a class of tests. This unfortunately has horrible internal knowledge.
153#
154def outSideTestaddError(self, offset, line):
155 """An 'error:' directive has been read."""
156 test_name = line[offset:-1].decode('utf8')
157 self.parser._current_test = subunit.RemotedTestCase(test_name)
158 self.parser.current_test_description = test_name
159 self.parser._state = self.parser._reading_error_details
160 self.parser._reading_error_details.set_simple()
161 self.parser.subunitLineReceived(line)
162
163subunit._OutSideTest.addError = outSideTestaddError
164
Andrew Geissler82c905d2020-04-13 13:39:40 -0500165# Like outSideTestaddError above, we need an equivalent for skips
166# happening at the setUpClass() level, otherwise we will see "UNKNOWN"
167# as a result for concurrent tests
168#
169def outSideTestaddSkip(self, offset, line):
170 """A 'skip:' directive has been read."""
171 test_name = line[offset:-1].decode('utf8')
172 self.parser._current_test = subunit.RemotedTestCase(test_name)
173 self.parser.current_test_description = test_name
174 self.parser._state = self.parser._reading_skip_details
175 self.parser._reading_skip_details.set_simple()
176 self.parser.subunitLineReceived(line)
177
178subunit._OutSideTest.addSkip = outSideTestaddSkip
Brad Bishopc342db32019-05-15 21:57:59 -0400179
180#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800181# A dummy structure to add to io.StringIO so that the .buffer object
182# is available and accepts writes. This allows unittest with buffer=True
183# to interact ok with subunit which wants to access sys.stdout.buffer.
184#
185class dummybuf(object):
186 def __init__(self, parent):
187 self.p = parent
188 def write(self, data):
189 self.p.write(data.decode("utf-8"))
190
191#
192# Taken from testtools.ConncurrencyTestSuite but modified for OE use
193#
194class ConcurrentTestSuite(unittest.TestSuite):
195
Andrew Geissler475cb722020-07-10 16:00:51 -0500196 def __init__(self, suite, processes, setupfunc, removefunc):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800197 super(ConcurrentTestSuite, self).__init__([suite])
198 self.processes = processes
Andrew Geissler82c905d2020-04-13 13:39:40 -0500199 self.setupfunc = setupfunc
Andrew Geissler475cb722020-07-10 16:00:51 -0500200 self.removefunc = removefunc
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800201
202 def run(self, result):
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500203 testservers, totaltests = fork_for_tests(self.processes, self)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800204 try:
205 threads = {}
206 queue = Queue()
207 semaphore = threading.Semaphore(1)
208 result.threadprogress = {}
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500209 for i, (testserver, testnum, output) in enumerate(testservers):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800210 result.threadprogress[i] = []
Brad Bishop79641f22019-09-10 07:20:22 -0400211 process_result = BBThreadsafeForwardingResult(
212 ExtraResultsDecoderTestResult(result),
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500213 semaphore, i, testnum, totaltests, output, result)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800214 reader_thread = threading.Thread(
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500215 target=self._run_test, args=(testserver, process_result, queue))
216 threads[testserver] = reader_thread, process_result
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800217 reader_thread.start()
218 while threads:
219 finished_test = queue.get()
220 threads[finished_test][0].join()
221 del threads[finished_test]
222 except:
223 for thread, process_result in threads.values():
224 process_result.stop()
225 raise
226 finally:
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500227 for testserver in testservers:
228 testserver[0]._stream.close()
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800229
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500230 def _run_test(self, testserver, process_result, queue):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800231 try:
232 try:
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500233 testserver.run(process_result)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800234 except Exception:
235 # The run logic itself failed
236 case = testtools.ErrorHolder(
237 "broken-runner",
238 error=sys.exc_info())
239 case.run(process_result)
240 finally:
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500241 queue.put(testserver)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800242
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800243def fork_for_tests(concurrency_num, suite):
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500244 testservers = []
Brad Bishop19323692019-04-05 15:28:33 -0400245 if 'BUILDDIR' in os.environ:
246 selftestdir = get_test_layer()
247
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800248 test_blocks = partition_tests(suite, concurrency_num)
249 # Clear the tests from the original suite so it doesn't keep them alive
250 suite._tests[:] = []
251 totaltests = sum(len(x) for x in test_blocks)
252 for process_tests in test_blocks:
253 numtests = len(process_tests)
254 process_suite = unittest.TestSuite(process_tests)
255 # Also clear each split list so new suite has only reference
256 process_tests[:] = []
257 c2pread, c2pwrite = os.pipe()
258 # Clear buffers before fork to avoid duplicate output
259 sys.stdout.flush()
260 sys.stderr.flush()
261 pid = os.fork()
262 if pid == 0:
263 ourpid = os.getpid()
264 try:
265 newbuilddir = None
266 stream = os.fdopen(c2pwrite, 'wb', 1)
267 os.close(c2pread)
268
Andrew Geissler82c905d2020-04-13 13:39:40 -0500269 (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800270
271 # Leave stderr and stdout open so we can see test noise
272 # Close stdin so that the child goes away if it decides to
273 # read from stdin (otherwise its a roulette to see what
274 # child actually gets keystrokes for pdb etc).
275 newsi = os.open(os.devnull, os.O_RDWR)
276 os.dup2(newsi, sys.stdin.fileno())
277
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500278 # Send stdout/stderr over the stream
279 os.dup2(c2pwrite, sys.stdout.fileno())
280 os.dup2(c2pwrite, sys.stderr.fileno())
281
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800282 subunit_client = TestProtocolClient(stream)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800283 subunit_result = AutoTimingTestResultDecorator(subunit_client)
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500284 unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800285 if ourpid != os.getpid():
286 os._exit(0)
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500287 if newbuilddir and unittest_result.wasSuccessful():
Andrew Geissler475cb722020-07-10 16:00:51 -0500288 suite.removefunc(newbuilddir)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800289 except:
290 # Don't do anything with process children
291 if ourpid != os.getpid():
292 os._exit(1)
293 # Try and report traceback on stream, but exit with error
294 # even if stream couldn't be created or something else
295 # goes wrong. The traceback is formatted to a string and
296 # written in one go to avoid interleaving lines from
297 # multiple failing children.
298 try:
299 stream.write(traceback.format_exc().encode('utf-8'))
300 except:
301 sys.stderr.write(traceback.format_exc())
302 finally:
303 if newbuilddir:
Andrew Geissler475cb722020-07-10 16:00:51 -0500304 suite.removefunc(newbuilddir)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800305 stream.flush()
306 os._exit(1)
307 stream.flush()
308 os._exit(0)
309 else:
310 os.close(c2pwrite)
311 stream = os.fdopen(c2pread, 'rb', 1)
Andrew Geissler3b8a17c2021-04-15 15:55:55 -0500312 # Collect stdout/stderr into an io buffer
313 output = io.BytesIO()
314 testserver = ProtocolTestCase(stream, passthrough=output)
315 testservers.append((testserver, numtests, output))
316 return testservers, totaltests
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800317
318def partition_tests(suite, count):
319 # Keep tests from the same class together but allow tests from modules
320 # to go to different processes to aid parallelisation.
321 modules = {}
322 for test in iterate_tests(suite):
323 m = test.__module__ + "." + test.__class__.__name__
324 if m not in modules:
325 modules[m] = []
326 modules[m].append(test)
327
328 # Simply divide the test blocks between the available processes
329 partitions = [list() for _ in range(count)]
330 for partition, m in zip(cycle(partitions), modules):
331 partition.extend(modules[m])
332
333 # No point in empty threads so drop them
334 return [p for p in partitions if p]
335