diff options
| author | Richard Purdie <richard.purdie@linuxfoundation.org> | 2025-11-07 13:31:53 +0000 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2025-11-07 13:31:53 +0000 |
| commit | 8c22ff0d8b70d9b12f0487ef696a7e915b9e3173 (patch) | |
| tree | efdc32587159d0050a69009bdf2330a531727d95 /meta/lib/oeqa/core/utils | |
| parent | d412d2747595c1cc4a5e3ca975e3adc31b2f7891 (diff) | |
| download | poky-8c22ff0d8b70d9b12f0487ef696a7e915b9e3173.tar.gz | |
The poky repository master branch is no longer being updated.
You can either:
a) switch to individual clones of bitbake, openembedded-core, meta-yocto and yocto-docs
b) use the new bitbake-setup
You can find information about either approach in our documentation:
https://docs.yoctoproject.org/
Note that "poky" the distro setting is still available in meta-yocto as
before and we continue to use and maintain that.
Long live Poky!
Some further information on the background of this change can be found
in: https://lists.openembedded.org/g/openembedded-architecture/message/2179
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/lib/oeqa/core/utils')
| -rw-r--r-- | meta/lib/oeqa/core/utils/__init__.py | 0 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/utils/concurrencytest.py | 336 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/utils/path.py | 22 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/utils/test.py | 89 |
4 files changed, 0 insertions, 447 deletions
diff --git a/meta/lib/oeqa/core/utils/__init__.py b/meta/lib/oeqa/core/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/meta/lib/oeqa/core/utils/__init__.py +++ /dev/null | |||
diff --git a/meta/lib/oeqa/core/utils/concurrencytest.py b/meta/lib/oeqa/core/utils/concurrencytest.py deleted file mode 100644 index d10f8f7f04..0000000000 --- a/meta/lib/oeqa/core/utils/concurrencytest.py +++ /dev/null | |||
| @@ -1,336 +0,0 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # Copyright OpenEmbedded Contributors | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: GPL-2.0-or-later | ||
| 6 | # | ||
| 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 | ||
| 26 | import json | ||
| 27 | import subunit | ||
| 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 | ||
| 34 | from testtools.content import Content | ||
| 35 | from testtools.content_type import ContentType | ||
| 36 | from oeqa.utils.commands import get_test_layer | ||
| 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 | |||
| 53 | def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests, output, finalresult): | ||
| 54 | super(BBThreadsafeForwardingResult, self).__init__(target, semaphore) | ||
| 55 | self.threadnum = threadnum | ||
| 56 | self.totalinprocess = totalinprocess | ||
| 57 | self.totaltests = totaltests | ||
| 58 | self.buffer = True | ||
| 59 | self.outputbuf = output | ||
| 60 | self.finalresult = finalresult | ||
| 61 | self.finalresult.buffer = True | ||
| 62 | self.target = target | ||
| 63 | |||
| 64 | def _add_result_with_semaphore(self, method, test, *args, **kwargs): | ||
| 65 | self.semaphore.acquire() | ||
| 66 | try: | ||
| 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()) | ||
| 71 | self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s failed) (%s)" % ( | ||
| 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()), | ||
| 78 | self.target.failed_tests, | ||
| 79 | test.id()) | ||
| 80 | finally: | ||
| 81 | self.semaphore.release() | ||
| 82 | self.finalresult._stderr_buffer = io.StringIO(initial_value=self.outputbuf.getvalue().decode("utf-8")) | ||
| 83 | self.finalresult._stdout_buffer = io.StringIO() | ||
| 84 | super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs) | ||
| 85 | |||
| 86 | class ProxyTestResult: | ||
| 87 | # a very basic TestResult proxy, in order to modify add* calls | ||
| 88 | def __init__(self, target): | ||
| 89 | self.result = target | ||
| 90 | self.failed_tests = 0 | ||
| 91 | |||
| 92 | def _addResult(self, method, test, *args, exception = False, **kwargs): | ||
| 93 | return method(test, *args, **kwargs) | ||
| 94 | |||
| 95 | def addError(self, test, err = None, **kwargs): | ||
| 96 | self.failed_tests += 1 | ||
| 97 | self._addResult(self.result.addError, test, err, exception = True, **kwargs) | ||
| 98 | |||
| 99 | def addFailure(self, test, err = None, **kwargs): | ||
| 100 | self.failed_tests += 1 | ||
| 101 | self._addResult(self.result.addFailure, test, err, exception = True, **kwargs) | ||
| 102 | |||
| 103 | def addSuccess(self, test, **kwargs): | ||
| 104 | self._addResult(self.result.addSuccess, test, **kwargs) | ||
| 105 | |||
| 106 | def addExpectedFailure(self, test, err = None, **kwargs): | ||
| 107 | self._addResult(self.result.addExpectedFailure, test, err, exception = True, **kwargs) | ||
| 108 | |||
| 109 | def addUnexpectedSuccess(self, test, **kwargs): | ||
| 110 | self._addResult(self.result.addUnexpectedSuccess, test, **kwargs) | ||
| 111 | |||
| 112 | def wasSuccessful(self): | ||
| 113 | return self.failed_tests == 0 | ||
| 114 | |||
| 115 | def __getattr__(self, attr): | ||
| 116 | return getattr(self.result, attr) | ||
| 117 | |||
| 118 | class ExtraResultsDecoderTestResult(ProxyTestResult): | ||
| 119 | def _addResult(self, method, test, *args, exception = False, **kwargs): | ||
| 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): | ||
| 133 | def _addResult(self, method, test, *args, exception = False, **kwargs): | ||
| 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) | ||
| 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 = [] | ||
| 147 | return method(test, *args, **kwargs) | ||
| 148 | |||
| 149 | # | ||
| 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 | |||
| 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 | ||
| 179 | |||
| 180 | # | ||
| 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 | |||
| 196 | def __init__(self, suite, processes, setupfunc, removefunc, bb_vars): | ||
| 197 | super(ConcurrentTestSuite, self).__init__([suite]) | ||
| 198 | self.processes = processes | ||
| 199 | self.setupfunc = setupfunc | ||
| 200 | self.removefunc = removefunc | ||
| 201 | self.bb_vars = bb_vars | ||
| 202 | |||
| 203 | def run(self, result): | ||
| 204 | testservers, totaltests = fork_for_tests(self.processes, self) | ||
| 205 | try: | ||
| 206 | threads = {} | ||
| 207 | queue = Queue() | ||
| 208 | semaphore = threading.Semaphore(1) | ||
| 209 | result.threadprogress = {} | ||
| 210 | for i, (testserver, testnum, output) in enumerate(testservers): | ||
| 211 | result.threadprogress[i] = [] | ||
| 212 | process_result = BBThreadsafeForwardingResult( | ||
| 213 | ExtraResultsDecoderTestResult(result), | ||
| 214 | semaphore, i, testnum, totaltests, output, result) | ||
| 215 | reader_thread = threading.Thread( | ||
| 216 | target=self._run_test, args=(testserver, process_result, queue)) | ||
| 217 | threads[testserver] = reader_thread, process_result | ||
| 218 | reader_thread.start() | ||
| 219 | while threads: | ||
| 220 | finished_test = queue.get() | ||
| 221 | threads[finished_test][0].join() | ||
| 222 | del threads[finished_test] | ||
| 223 | except: | ||
| 224 | for thread, process_result in threads.values(): | ||
| 225 | process_result.stop() | ||
| 226 | raise | ||
| 227 | finally: | ||
| 228 | for testserver in testservers: | ||
| 229 | testserver[0]._stream.close() | ||
| 230 | |||
| 231 | def _run_test(self, testserver, process_result, queue): | ||
| 232 | try: | ||
| 233 | try: | ||
| 234 | testserver.run(process_result) | ||
| 235 | except Exception: | ||
| 236 | # The run logic itself failed | ||
| 237 | case = testtools.ErrorHolder( | ||
| 238 | "broken-runner", | ||
| 239 | error=sys.exc_info()) | ||
| 240 | case.run(process_result) | ||
| 241 | finally: | ||
| 242 | queue.put(testserver) | ||
| 243 | |||
| 244 | def fork_for_tests(concurrency_num, suite): | ||
| 245 | testservers = [] | ||
| 246 | if 'BUILDDIR' in os.environ: | ||
| 247 | selftestdir = get_test_layer(suite.bb_vars['BBLAYERS']) | ||
| 248 | |||
| 249 | test_blocks = partition_tests(suite, concurrency_num) | ||
| 250 | # Clear the tests from the original suite so it doesn't keep them alive | ||
| 251 | suite._tests[:] = [] | ||
| 252 | totaltests = sum(len(x) for x in test_blocks) | ||
| 253 | for process_tests in test_blocks: | ||
| 254 | numtests = len(process_tests) | ||
| 255 | process_suite = unittest.TestSuite(process_tests) | ||
| 256 | # Also clear each split list so new suite has only reference | ||
| 257 | process_tests[:] = [] | ||
| 258 | c2pread, c2pwrite = os.pipe() | ||
| 259 | # Clear buffers before fork to avoid duplicate output | ||
| 260 | sys.stdout.flush() | ||
| 261 | sys.stderr.flush() | ||
| 262 | pid = os.fork() | ||
| 263 | if pid == 0: | ||
| 264 | ourpid = os.getpid() | ||
| 265 | try: | ||
| 266 | newbuilddir = None | ||
| 267 | stream = os.fdopen(c2pwrite, 'wb') | ||
| 268 | os.close(c2pread) | ||
| 269 | |||
| 270 | (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite) | ||
| 271 | |||
| 272 | # Leave stderr and stdout open so we can see test noise | ||
| 273 | # Close stdin so that the child goes away if it decides to | ||
| 274 | # read from stdin (otherwise its a roulette to see what | ||
| 275 | # child actually gets keystrokes for pdb etc). | ||
| 276 | newsi = os.open(os.devnull, os.O_RDWR) | ||
| 277 | os.dup2(newsi, sys.stdin.fileno()) | ||
| 278 | |||
| 279 | # Send stdout/stderr over the stream | ||
| 280 | os.dup2(c2pwrite, sys.stdout.fileno()) | ||
| 281 | os.dup2(c2pwrite, sys.stderr.fileno()) | ||
| 282 | |||
| 283 | subunit_client = TestProtocolClient(stream) | ||
| 284 | subunit_result = AutoTimingTestResultDecorator(subunit_client) | ||
| 285 | unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result)) | ||
| 286 | if ourpid != os.getpid(): | ||
| 287 | os._exit(0) | ||
| 288 | if newbuilddir and unittest_result.wasSuccessful(): | ||
| 289 | suite.removefunc(newbuilddir) | ||
| 290 | except: | ||
| 291 | # Don't do anything with process children | ||
| 292 | if ourpid != os.getpid(): | ||
| 293 | os._exit(1) | ||
| 294 | # Try and report traceback on stream, but exit with error | ||
| 295 | # even if stream couldn't be created or something else | ||
| 296 | # goes wrong. The traceback is formatted to a string and | ||
| 297 | # written in one go to avoid interleaving lines from | ||
| 298 | # multiple failing children. | ||
| 299 | try: | ||
| 300 | stream.write(traceback.format_exc().encode('utf-8')) | ||
| 301 | except: | ||
| 302 | sys.stderr.write(traceback.format_exc()) | ||
| 303 | finally: | ||
| 304 | if newbuilddir: | ||
| 305 | suite.removefunc(newbuilddir) | ||
| 306 | stream.flush() | ||
| 307 | os._exit(1) | ||
| 308 | stream.flush() | ||
| 309 | os._exit(0) | ||
| 310 | else: | ||
| 311 | os.close(c2pwrite) | ||
| 312 | stream = os.fdopen(c2pread, 'rb') | ||
| 313 | # Collect stdout/stderr into an io buffer | ||
| 314 | output = io.BytesIO() | ||
| 315 | testserver = ProtocolTestCase(stream, passthrough=output) | ||
| 316 | testservers.append((testserver, numtests, output)) | ||
| 317 | return testservers, totaltests | ||
| 318 | |||
| 319 | def partition_tests(suite, count): | ||
| 320 | # Keep tests from the same class together but allow tests from modules | ||
| 321 | # to go to different processes to aid parallelisation. | ||
| 322 | modules = {} | ||
| 323 | for test in iterate_tests(suite): | ||
| 324 | m = test.__module__ + "." + test.__class__.__name__ | ||
| 325 | if m not in modules: | ||
| 326 | modules[m] = [] | ||
| 327 | modules[m].append(test) | ||
| 328 | |||
| 329 | # Simply divide the test blocks between the available processes | ||
| 330 | partitions = [list() for _ in range(count)] | ||
| 331 | for partition, m in zip(cycle(partitions), modules): | ||
| 332 | partition.extend(modules[m]) | ||
| 333 | |||
| 334 | # No point in empty threads so drop them | ||
| 335 | return [p for p in partitions if p] | ||
| 336 | |||
diff --git a/meta/lib/oeqa/core/utils/path.py b/meta/lib/oeqa/core/utils/path.py deleted file mode 100644 index c086dcb0b0..0000000000 --- a/meta/lib/oeqa/core/utils/path.py +++ /dev/null | |||
| @@ -1,22 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2016 Intel Corporation | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: MIT | ||
| 5 | # | ||
| 6 | |||
| 7 | import os | ||
| 8 | import sys | ||
| 9 | |||
| 10 | def findFile(file_name, directory): | ||
| 11 | """ | ||
| 12 | Search for a file in directory and returns its complete path. | ||
| 13 | """ | ||
| 14 | for r, d, f in os.walk(directory): | ||
| 15 | if file_name in f: | ||
| 16 | return os.path.join(r, file_name) | ||
| 17 | return None | ||
| 18 | |||
| 19 | def remove_safe(path): | ||
| 20 | if os.path.exists(path): | ||
| 21 | os.remove(path) | ||
| 22 | |||
diff --git a/meta/lib/oeqa/core/utils/test.py b/meta/lib/oeqa/core/utils/test.py deleted file mode 100644 index d38cab8a51..0000000000 --- a/meta/lib/oeqa/core/utils/test.py +++ /dev/null | |||
| @@ -1,89 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2016 Intel Corporation | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: MIT | ||
| 5 | # | ||
| 6 | |||
| 7 | import os | ||
| 8 | import inspect | ||
| 9 | import unittest | ||
| 10 | |||
| 11 | def getSuiteCases(suite): | ||
| 12 | """ | ||
| 13 | Returns individual test from a test suite. | ||
| 14 | """ | ||
| 15 | tests = [] | ||
| 16 | |||
| 17 | if isinstance(suite, unittest.TestCase): | ||
| 18 | tests.append(suite) | ||
| 19 | elif isinstance(suite, unittest.suite.TestSuite): | ||
| 20 | for item in suite: | ||
| 21 | tests.extend(getSuiteCases(item)) | ||
| 22 | |||
| 23 | return tests | ||
| 24 | |||
| 25 | def getSuiteModules(suite): | ||
| 26 | """ | ||
| 27 | Returns modules in a test suite. | ||
| 28 | """ | ||
| 29 | modules = set() | ||
| 30 | for test in getSuiteCases(suite): | ||
| 31 | modules.add(getCaseModule(test)) | ||
| 32 | return modules | ||
| 33 | |||
| 34 | def getSuiteCasesInfo(suite, func): | ||
| 35 | """ | ||
| 36 | Returns test case info from suite. Info is fetched from func. | ||
| 37 | """ | ||
| 38 | tests = [] | ||
| 39 | for test in getSuiteCases(suite): | ||
| 40 | tests.append(func(test)) | ||
| 41 | return tests | ||
| 42 | |||
| 43 | def getSuiteCasesNames(suite): | ||
| 44 | """ | ||
| 45 | Returns test case names from suite. | ||
| 46 | """ | ||
| 47 | return getSuiteCasesInfo(suite, getCaseMethod) | ||
| 48 | |||
| 49 | def getSuiteCasesIDs(suite): | ||
| 50 | """ | ||
| 51 | Returns test case ids from suite. | ||
| 52 | """ | ||
| 53 | return getSuiteCasesInfo(suite, getCaseID) | ||
| 54 | |||
| 55 | def getSuiteCasesFiles(suite): | ||
| 56 | """ | ||
| 57 | Returns test case files paths from suite. | ||
| 58 | """ | ||
| 59 | return getSuiteCasesInfo(suite, getCaseFile) | ||
| 60 | |||
| 61 | def getCaseModule(test_case): | ||
| 62 | """ | ||
| 63 | Returns test case module name. | ||
| 64 | """ | ||
| 65 | return test_case.__module__ | ||
| 66 | |||
| 67 | def getCaseClass(test_case): | ||
| 68 | """ | ||
| 69 | Returns test case class name. | ||
| 70 | """ | ||
| 71 | return test_case.__class__.__name__ | ||
| 72 | |||
| 73 | def getCaseID(test_case): | ||
| 74 | """ | ||
| 75 | Returns test case complete id. | ||
| 76 | """ | ||
| 77 | return test_case.id() | ||
| 78 | |||
| 79 | def getCaseFile(test_case): | ||
| 80 | """ | ||
| 81 | Returns test case file path. | ||
| 82 | """ | ||
| 83 | return inspect.getsourcefile(test_case.__class__) | ||
| 84 | |||
| 85 | def getCaseMethod(test_case): | ||
| 86 | """ | ||
| 87 | Returns test case method name. | ||
| 88 | """ | ||
| 89 | return getCaseID(test_case).split('.')[-1] | ||
