Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
Patrick Williams | 92b42cb | 2022-09-03 06:53:57 -0500 | [diff] [blame] | 3 | # Copyright OpenEmbedded Contributors |
| 4 | # |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 5 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 6 | # |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 7 | # 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 | |
| 17 | import os |
| 18 | import sys |
| 19 | import traceback |
| 20 | import unittest |
| 21 | import subprocess |
| 22 | import testtools |
| 23 | import threading |
| 24 | import time |
| 25 | import io |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 26 | import json |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 27 | import subunit |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 28 | |
| 29 | from queue import Queue |
| 30 | from itertools import cycle |
| 31 | from subunit import ProtocolTestCase, TestProtocolClient |
| 32 | from subunit.test_results import AutoTimingTestResultDecorator |
| 33 | from testtools import ThreadsafeForwardingResult, iterate_tests |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 34 | from testtools.content import Content |
| 35 | from testtools.content_type import ContentType |
Brad Bishop | 1932369 | 2019-04-05 15:28:33 -0400 | [diff] [blame] | 36 | from oeqa.utils.commands import get_test_layer |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 37 | |
| 38 | import bb.utils |
| 39 | import 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 | # |
| 51 | class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): |
| 52 | |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 53 | def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests, output, finalresult): |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 54 | super(BBThreadsafeForwardingResult, self).__init__(target, semaphore) |
| 55 | self.threadnum = threadnum |
| 56 | self.totalinprocess = totalinprocess |
| 57 | self.totaltests = totaltests |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 58 | self.buffer = True |
| 59 | self.outputbuf = output |
| 60 | self.finalresult = finalresult |
| 61 | self.finalresult.buffer = True |
Andrew Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame^] | 62 | self.target = target |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 63 | |
| 64 | def _add_result_with_semaphore(self, method, test, *args, **kwargs): |
| 65 | self.semaphore.acquire() |
| 66 | try: |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 67 | 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 Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame^] | 71 | self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s failed) (%s)" % ( |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 72 | 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 Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame^] | 78 | self.target.failed_tests, |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 79 | test.id()) |
| 80 | finally: |
| 81 | self.semaphore.release() |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 82 | self.finalresult._stderr_buffer = io.StringIO(initial_value=self.outputbuf.getvalue().decode("utf-8")) |
| 83 | self.finalresult._stdout_buffer = io.StringIO() |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 84 | super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs) |
| 85 | |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 86 | class ProxyTestResult: |
| 87 | # a very basic TestResult proxy, in order to modify add* calls |
| 88 | def __init__(self, target): |
| 89 | self.result = target |
Andrew Geissler | 1e34c2d | 2020-05-29 16:02:59 -0500 | [diff] [blame] | 90 | self.failed_tests = 0 |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 91 | |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 92 | def _addResult(self, method, test, *args, exception = False, **kwargs): |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 93 | return method(test, *args, **kwargs) |
| 94 | |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 95 | def addError(self, test, err = None, **kwargs): |
Andrew Geissler | 1e34c2d | 2020-05-29 16:02:59 -0500 | [diff] [blame] | 96 | self.failed_tests += 1 |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 97 | self._addResult(self.result.addError, test, err, exception = True, **kwargs) |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 98 | |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 99 | def addFailure(self, test, err = None, **kwargs): |
Andrew Geissler | 1e34c2d | 2020-05-29 16:02:59 -0500 | [diff] [blame] | 100 | self.failed_tests += 1 |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 101 | self._addResult(self.result.addFailure, test, err, exception = True, **kwargs) |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 102 | |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 103 | def addSuccess(self, test, **kwargs): |
| 104 | self._addResult(self.result.addSuccess, test, **kwargs) |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 105 | |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 106 | def addExpectedFailure(self, test, err = None, **kwargs): |
| 107 | self._addResult(self.result.addExpectedFailure, test, err, exception = True, **kwargs) |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 108 | |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 109 | def addUnexpectedSuccess(self, test, **kwargs): |
| 110 | self._addResult(self.result.addUnexpectedSuccess, test, **kwargs) |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 111 | |
Andrew Geissler | 1e34c2d | 2020-05-29 16:02:59 -0500 | [diff] [blame] | 112 | def wasSuccessful(self): |
| 113 | return self.failed_tests == 0 |
| 114 | |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 115 | def __getattr__(self, attr): |
| 116 | return getattr(self.result, attr) |
| 117 | |
| 118 | class ExtraResultsDecoderTestResult(ProxyTestResult): |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 119 | def _addResult(self, method, test, *args, exception = False, **kwargs): |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 120 | 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 | |
| 132 | class ExtraResultsEncoderTestResult(ProxyTestResult): |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 133 | def _addResult(self, method, test, *args, exception = False, **kwargs): |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 134 | 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 Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 142 | # 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 Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 147 | return method(test, *args, **kwargs) |
| 148 | |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 149 | # |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 150 | # 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 | # |
| 154 | def 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 | |
| 163 | subunit._OutSideTest.addError = outSideTestaddError |
| 164 | |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 165 | # 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 | # |
| 169 | def 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 | |
| 178 | subunit._OutSideTest.addSkip = outSideTestaddSkip |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 179 | |
| 180 | # |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 181 | # 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 | # |
| 185 | class 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 | # |
| 194 | class ConcurrentTestSuite(unittest.TestSuite): |
| 195 | |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 196 | def __init__(self, suite, processes, setupfunc, removefunc): |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 197 | super(ConcurrentTestSuite, self).__init__([suite]) |
| 198 | self.processes = processes |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 199 | self.setupfunc = setupfunc |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 200 | self.removefunc = removefunc |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 201 | |
| 202 | def run(self, result): |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 203 | testservers, totaltests = fork_for_tests(self.processes, self) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 204 | try: |
| 205 | threads = {} |
| 206 | queue = Queue() |
| 207 | semaphore = threading.Semaphore(1) |
| 208 | result.threadprogress = {} |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 209 | for i, (testserver, testnum, output) in enumerate(testservers): |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 210 | result.threadprogress[i] = [] |
Brad Bishop | 79641f2 | 2019-09-10 07:20:22 -0400 | [diff] [blame] | 211 | process_result = BBThreadsafeForwardingResult( |
| 212 | ExtraResultsDecoderTestResult(result), |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 213 | semaphore, i, testnum, totaltests, output, result) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 214 | reader_thread = threading.Thread( |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 215 | target=self._run_test, args=(testserver, process_result, queue)) |
| 216 | threads[testserver] = reader_thread, process_result |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 217 | 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 Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 227 | for testserver in testservers: |
| 228 | testserver[0]._stream.close() |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 229 | |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 230 | def _run_test(self, testserver, process_result, queue): |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 231 | try: |
| 232 | try: |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 233 | testserver.run(process_result) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 234 | 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 Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 241 | queue.put(testserver) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 242 | |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 243 | def fork_for_tests(concurrency_num, suite): |
Andrew Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 244 | testservers = [] |
Brad Bishop | 1932369 | 2019-04-05 15:28:33 -0400 | [diff] [blame] | 245 | if 'BUILDDIR' in os.environ: |
| 246 | selftestdir = get_test_layer() |
| 247 | |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 248 | 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 Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 269 | (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 270 | |
| 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 Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 278 | # Send stdout/stderr over the stream |
| 279 | os.dup2(c2pwrite, sys.stdout.fileno()) |
| 280 | os.dup2(c2pwrite, sys.stderr.fileno()) |
| 281 | |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 282 | subunit_client = TestProtocolClient(stream) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 283 | subunit_result = AutoTimingTestResultDecorator(subunit_client) |
Andrew Geissler | 1e34c2d | 2020-05-29 16:02:59 -0500 | [diff] [blame] | 284 | unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result)) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 285 | if ourpid != os.getpid(): |
| 286 | os._exit(0) |
Andrew Geissler | 1e34c2d | 2020-05-29 16:02:59 -0500 | [diff] [blame] | 287 | if newbuilddir and unittest_result.wasSuccessful(): |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 288 | suite.removefunc(newbuilddir) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 289 | 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 Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 304 | suite.removefunc(newbuilddir) |
Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 305 | 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 Geissler | 3b8a17c | 2021-04-15 15:55:55 -0500 | [diff] [blame] | 312 | # 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 Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 317 | |
| 318 | def 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 | |