diff options
| author | Aníbal Limón <anibal.limon@linux.intel.com> | 2016-11-09 10:38:37 -0600 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2017-01-23 12:05:18 +0000 |
| commit | 13c8c08b95191829705b5a4c8f0d368c251f0174 (patch) | |
| tree | 737412ed0d676dcf1982fc6b2cd0046bca760281 /meta/lib/oeqa/core | |
| parent | abb55ab304af91f68a4e09176ce9c6b995d903e0 (diff) | |
| download | poky-13c8c08b95191829705b5a4c8f0d368c251f0174.tar.gz | |
oeqa/core: Add loader, context and decorator modules
loader: Implements OETestLoader handling OETestDecorator
and filtering support when load tests. The OETestLoader is
responsible to set custom methods, attrs of the OEQA
frameowork.
[YOCTO #10231]
[YOCTO #10317]
[YOCTO #10353]
decorator: Add base class OETestDecorator to provide a common
way to define decorators to be used over OETestCase's, every
decorator has a method to be called when loading tests and
before test execution starts. Special decorators could be
implemented for filter tests on loading phase.
context: Provides HIGH level API for loadTests and runTests
of certain test component (i.e. runtime, sdk, selftest).
[YOCTO #10230]
(From OE-Core rev: 275ef03b77ef5f23b75cb01c55206d1ab0261342)
Signed-off-by: Aníbal Limón <anibal.limon@linux.intel.com>
Signed-off-by: Mariano Lopez <mariano.lopez@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/lib/oeqa/core')
| -rw-r--r-- | meta/lib/oeqa/core/context.py | 148 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/decorator/__init__.py | 71 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/loader.py | 235 |
3 files changed, 454 insertions, 0 deletions
diff --git a/meta/lib/oeqa/core/context.py b/meta/lib/oeqa/core/context.py new file mode 100644 index 0000000000..cbab2f8f5f --- /dev/null +++ b/meta/lib/oeqa/core/context.py | |||
| @@ -0,0 +1,148 @@ | |||
| 1 | # Copyright (C) 2016 Intel Corporation | ||
| 2 | # Released under the MIT license (see COPYING.MIT) | ||
| 3 | |||
| 4 | import os | ||
| 5 | import sys | ||
| 6 | import json | ||
| 7 | import time | ||
| 8 | import logging | ||
| 9 | import collections | ||
| 10 | import re | ||
| 11 | |||
| 12 | from oeqa.core.loader import OETestLoader | ||
| 13 | from oeqa.core.runner import OETestRunner, OEStreamLogger, xmlEnabled | ||
| 14 | |||
| 15 | class OETestContext(object): | ||
| 16 | loaderClass = OETestLoader | ||
| 17 | runnerClass = OETestRunner | ||
| 18 | streamLoggerClass = OEStreamLogger | ||
| 19 | |||
| 20 | files_dir = os.path.abspath(os.path.join(os.path.dirname( | ||
| 21 | os.path.abspath(__file__)), "../files")) | ||
| 22 | |||
| 23 | def __init__(self, td=None, logger=None): | ||
| 24 | if not type(td) is dict: | ||
| 25 | raise TypeError("td isn't dictionary type") | ||
| 26 | |||
| 27 | self.td = td | ||
| 28 | self.logger = logger | ||
| 29 | self._registry = {} | ||
| 30 | self._registry['cases'] = collections.OrderedDict() | ||
| 31 | self._results = {} | ||
| 32 | |||
| 33 | def _read_modules_from_manifest(self, manifest): | ||
| 34 | if not os.path.exists(manifest): | ||
| 35 | raise | ||
| 36 | |||
| 37 | modules = [] | ||
| 38 | for line in open(manifest).readlines(): | ||
| 39 | line = line.strip() | ||
| 40 | if line and not line.startswith("#"): | ||
| 41 | modules.append(line) | ||
| 42 | |||
| 43 | return modules | ||
| 44 | |||
| 45 | def loadTests(self, module_paths, modules=[], tests=[], | ||
| 46 | modules_manifest="", modules_required=[], filters={}): | ||
| 47 | if modules_manifest: | ||
| 48 | modules = self._read_modules_from_manifest(modules_manifest) | ||
| 49 | |||
| 50 | self.loader = self.loaderClass(self, module_paths, modules, tests, | ||
| 51 | modules_required, filters) | ||
| 52 | self.suites = self.loader.discover() | ||
| 53 | |||
| 54 | def runTests(self): | ||
| 55 | streamLogger = self.streamLoggerClass(self.logger) | ||
| 56 | self.runner = self.runnerClass(self, stream=streamLogger, verbosity=2) | ||
| 57 | |||
| 58 | self._run_start_time = time.time() | ||
| 59 | result = self.runner.run(self.suites) | ||
| 60 | self._run_end_time = time.time() | ||
| 61 | |||
| 62 | return result | ||
| 63 | |||
| 64 | def logSummary(self, result, component, context_msg=''): | ||
| 65 | self.logger.info("SUMMARY:") | ||
| 66 | self.logger.info("%s (%s) - Ran %d test%s in %.3fs" % (component, | ||
| 67 | context_msg, result.testsRun, result.testsRun != 1 and "s" or "", | ||
| 68 | (self._run_end_time - self._run_start_time))) | ||
| 69 | |||
| 70 | if result.wasSuccessful(): | ||
| 71 | msg = "%s - OK - All required tests passed" % component | ||
| 72 | else: | ||
| 73 | msg = "%s - FAIL - Required tests failed" % component | ||
| 74 | skipped = len(self._results['skipped']) | ||
| 75 | if skipped: | ||
| 76 | msg += " (skipped=%d)" % skipped | ||
| 77 | self.logger.info(msg) | ||
| 78 | |||
| 79 | def _getDetailsNotPassed(self, case, type, desc): | ||
| 80 | found = False | ||
| 81 | |||
| 82 | for (scase, msg) in self._results[type]: | ||
| 83 | # XXX: When XML reporting is enabled scase is | ||
| 84 | # xmlrunner.result._TestInfo instance instead of | ||
| 85 | # string. | ||
| 86 | if xmlEnabled: | ||
| 87 | if case.id() == scase.test_id: | ||
| 88 | found = True | ||
| 89 | break | ||
| 90 | scase_str = scase.test_id | ||
| 91 | else: | ||
| 92 | if case == scase: | ||
| 93 | found = True | ||
| 94 | break | ||
| 95 | scase_str = str(scase) | ||
| 96 | |||
| 97 | # When fails at module or class level the class name is passed as string | ||
| 98 | # so figure out to see if match | ||
| 99 | m = re.search("^setUpModule \((?P<module_name>.*)\)$", scase_str) | ||
| 100 | if m: | ||
| 101 | if case.__class__.__module__ == m.group('module_name'): | ||
| 102 | found = True | ||
| 103 | break | ||
| 104 | |||
| 105 | m = re.search("^setUpClass \((?P<class_name>.*)\)$", scase_str) | ||
| 106 | if m: | ||
| 107 | class_name = "%s.%s" % (case.__class__.__module__, | ||
| 108 | case.__class__.__name__) | ||
| 109 | |||
| 110 | if class_name == m.group('class_name'): | ||
| 111 | found = True | ||
| 112 | break | ||
| 113 | |||
| 114 | if found: | ||
| 115 | return (found, msg) | ||
| 116 | |||
| 117 | return (found, None) | ||
| 118 | |||
| 119 | def logDetails(self): | ||
| 120 | self.logger.info("RESULTS:") | ||
| 121 | for case_name in self._registry['cases']: | ||
| 122 | case = self._registry['cases'][case_name] | ||
| 123 | |||
| 124 | result_types = ['failures', 'errors', 'skipped', 'expectedFailures'] | ||
| 125 | result_desc = ['FAILED', 'ERROR', 'SKIPPED', 'EXPECTEDFAIL'] | ||
| 126 | |||
| 127 | fail = False | ||
| 128 | desc = None | ||
| 129 | for idx, name in enumerate(result_types): | ||
| 130 | (fail, msg) = self._getDetailsNotPassed(case, result_types[idx], | ||
| 131 | result_desc[idx]) | ||
| 132 | if fail: | ||
| 133 | desc = result_desc[idx] | ||
| 134 | break | ||
| 135 | |||
| 136 | oeid = -1 | ||
| 137 | for d in case.decorators: | ||
| 138 | if hasattr(d, 'oeid'): | ||
| 139 | oeid = d.oeid | ||
| 140 | |||
| 141 | if fail: | ||
| 142 | self.logger.info("RESULTS - %s - Testcase %s: %s" % (case.id(), | ||
| 143 | oeid, desc)) | ||
| 144 | if msg: | ||
| 145 | self.logger.info(msg) | ||
| 146 | else: | ||
| 147 | self.logger.info("RESULTS - %s - Testcase %s: %s" % (case.id(), | ||
| 148 | oeid, 'PASSED')) | ||
diff --git a/meta/lib/oeqa/core/decorator/__init__.py b/meta/lib/oeqa/core/decorator/__init__.py new file mode 100644 index 0000000000..855b6b9d28 --- /dev/null +++ b/meta/lib/oeqa/core/decorator/__init__.py | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | # Copyright (C) 2016 Intel Corporation | ||
| 2 | # Released under the MIT license (see COPYING.MIT) | ||
| 3 | |||
| 4 | from functools import wraps | ||
| 5 | from abc import abstractmethod | ||
| 6 | |||
| 7 | decoratorClasses = set() | ||
| 8 | |||
| 9 | def registerDecorator(obj): | ||
| 10 | decoratorClasses.add(obj) | ||
| 11 | return obj | ||
| 12 | |||
| 13 | class OETestDecorator(object): | ||
| 14 | case = None # Reference of OETestCase decorated | ||
| 15 | attrs = None # Attributes to be loaded by decorator implementation | ||
| 16 | |||
| 17 | def __init__(self, *args, **kwargs): | ||
| 18 | if not self.attrs: | ||
| 19 | return | ||
| 20 | |||
| 21 | for idx, attr in enumerate(self.attrs): | ||
| 22 | if attr in kwargs: | ||
| 23 | value = kwargs[attr] | ||
| 24 | else: | ||
| 25 | value = args[idx] | ||
| 26 | setattr(self, attr, value) | ||
| 27 | |||
| 28 | def __call__(self, func): | ||
| 29 | @wraps(func) | ||
| 30 | def wrapped_f(*args, **kwargs): | ||
| 31 | self.attrs = self.attrs # XXX: Enables OETestLoader discover | ||
| 32 | return func(*args, **kwargs) | ||
| 33 | return wrapped_f | ||
| 34 | |||
| 35 | # OETestLoader call it when is loading test cases. | ||
| 36 | # XXX: Most methods would change the registry for later | ||
| 37 | # processing; be aware that filtrate method needs to | ||
| 38 | # run later than bind, so there could be data (in the | ||
| 39 | # registry) of a cases that were filtered. | ||
| 40 | def bind(self, registry, case): | ||
| 41 | self.case = case | ||
| 42 | self.logger = case.tc.logger | ||
| 43 | self.case.decorators.append(self) | ||
| 44 | |||
| 45 | # OETestRunner call this method when tries to run | ||
| 46 | # the test case. | ||
| 47 | def setUpDecorator(self): | ||
| 48 | pass | ||
| 49 | |||
| 50 | # OETestRunner call it after a test method has been | ||
| 51 | # called even if the method raised an exception. | ||
| 52 | def tearDownDecorator(self): | ||
| 53 | pass | ||
| 54 | |||
| 55 | class OETestDiscover(OETestDecorator): | ||
| 56 | |||
| 57 | # OETestLoader call it after discover test cases | ||
| 58 | # needs to return the cases to be run. | ||
| 59 | @staticmethod | ||
| 60 | def discover(registry): | ||
| 61 | return registry['cases'] | ||
| 62 | |||
| 63 | class OETestFilter(OETestDecorator): | ||
| 64 | |||
| 65 | # OETestLoader call it while loading the tests | ||
| 66 | # in loadTestsFromTestCase method, it needs to | ||
| 67 | # return a bool, True if needs to be filtered. | ||
| 68 | # This method must consume the filter used. | ||
| 69 | @abstractmethod | ||
| 70 | def filtrate(self, filters): | ||
| 71 | return False | ||
diff --git a/meta/lib/oeqa/core/loader.py b/meta/lib/oeqa/core/loader.py new file mode 100644 index 0000000000..c73ef9a0fc --- /dev/null +++ b/meta/lib/oeqa/core/loader.py | |||
| @@ -0,0 +1,235 @@ | |||
| 1 | # Copyright (C) 2016 Intel Corporation | ||
| 2 | # Released under the MIT license (see COPYING.MIT) | ||
| 3 | |||
| 4 | import os | ||
| 5 | import sys | ||
| 6 | import unittest | ||
| 7 | |||
| 8 | from oeqa.core.utils.path import findFile | ||
| 9 | from oeqa.core.utils.test import getSuiteModules, getCaseID | ||
| 10 | |||
| 11 | from oeqa.core.case import OETestCase | ||
| 12 | from oeqa.core.decorator import decoratorClasses, OETestDecorator, \ | ||
| 13 | OETestFilter, OETestDiscover | ||
| 14 | |||
| 15 | def _make_failed_test(classname, methodname, exception, suiteClass): | ||
| 16 | """ | ||
| 17 | When loading tests unittest framework stores the exception in a new | ||
| 18 | class created for be displayed into run(). | ||
| 19 | |||
| 20 | For our purposes will be better to raise the exception in loading | ||
| 21 | step instead of wait to run the test suite. | ||
| 22 | """ | ||
| 23 | raise exception | ||
| 24 | unittest.loader._make_failed_test = _make_failed_test | ||
| 25 | |||
| 26 | def _find_duplicated_modules(suite, directory): | ||
| 27 | for module in getSuiteModules(suite): | ||
| 28 | path = findFile('%s.py' % module, directory) | ||
| 29 | if path: | ||
| 30 | raise ImportError("Duplicated %s module found in %s" % (module, path)) | ||
| 31 | |||
| 32 | class OETestLoader(unittest.TestLoader): | ||
| 33 | caseClass = OETestCase | ||
| 34 | |||
| 35 | kwargs_names = ['testMethodPrefix', 'sortTestMethodUsing', 'suiteClass', | ||
| 36 | '_top_level_dir'] | ||
| 37 | |||
| 38 | def __init__(self, tc, module_paths, modules, tests, modules_required, | ||
| 39 | filters, *args, **kwargs): | ||
| 40 | self.tc = tc | ||
| 41 | |||
| 42 | self.modules = modules | ||
| 43 | self.tests = tests | ||
| 44 | self.modules_required = modules_required | ||
| 45 | |||
| 46 | self.filters = filters | ||
| 47 | self.decorator_filters = [d for d in decoratorClasses if \ | ||
| 48 | issubclass(d, OETestFilter)] | ||
| 49 | self._validateFilters(self.filters, self.decorator_filters) | ||
| 50 | self.used_filters = [d for d in self.decorator_filters | ||
| 51 | for f in self.filters | ||
| 52 | if f in d.attrs] | ||
| 53 | |||
| 54 | if isinstance(module_paths, str): | ||
| 55 | module_paths = [module_paths] | ||
| 56 | elif not isinstance(module_paths, list): | ||
| 57 | raise TypeError('module_paths must be a str or a list of str') | ||
| 58 | self.module_paths = module_paths | ||
| 59 | |||
| 60 | for kwname in self.kwargs_names: | ||
| 61 | if kwname in kwargs: | ||
| 62 | setattr(self, kwname, kwargs[kwname]) | ||
| 63 | |||
| 64 | self._patchCaseClass(self.caseClass) | ||
| 65 | |||
| 66 | def _patchCaseClass(self, testCaseClass): | ||
| 67 | # Adds custom attributes to the OETestCase class | ||
| 68 | setattr(testCaseClass, 'tc', self.tc) | ||
| 69 | setattr(testCaseClass, 'td', self.tc.td) | ||
| 70 | setattr(testCaseClass, 'logger', self.tc.logger) | ||
| 71 | |||
| 72 | def _validateFilters(self, filters, decorator_filters): | ||
| 73 | # Validate if filter isn't empty | ||
| 74 | for key,value in filters.items(): | ||
| 75 | if not value: | ||
| 76 | raise TypeError("Filter %s specified is empty" % key) | ||
| 77 | |||
| 78 | # Validate unique attributes | ||
| 79 | attr_filters = [attr for clss in decorator_filters \ | ||
| 80 | for attr in clss.attrs] | ||
| 81 | dup_attr = [attr for attr in attr_filters | ||
| 82 | if attr_filters.count(attr) > 1] | ||
| 83 | if dup_attr: | ||
| 84 | raise TypeError('Detected duplicated attribute(s) %s in filter' | ||
| 85 | ' decorators' % ' ,'.join(dup_attr)) | ||
| 86 | |||
| 87 | # Validate if filter is supported | ||
| 88 | for f in filters: | ||
| 89 | if f not in attr_filters: | ||
| 90 | classes = ', '.join([d.__name__ for d in decorator_filters]) | ||
| 91 | raise TypeError('Found "%s" filter but not declared in any of ' | ||
| 92 | '%s decorators' % (f, classes)) | ||
| 93 | |||
| 94 | def _registerTestCase(self, case): | ||
| 95 | case_id = case.id() | ||
| 96 | self.tc._registry['cases'][case_id] = case | ||
| 97 | |||
| 98 | def _handleTestCaseDecorators(self, case): | ||
| 99 | def _handle(obj): | ||
| 100 | if isinstance(obj, OETestDecorator): | ||
| 101 | if not obj.__class__ in decoratorClasses: | ||
| 102 | raise Exception("Decorator %s isn't registered" \ | ||
| 103 | " in decoratorClasses." % obj.__name__) | ||
| 104 | obj.bind(self.tc._registry, case) | ||
| 105 | |||
| 106 | def _walk_closure(obj): | ||
| 107 | if hasattr(obj, '__closure__') and obj.__closure__: | ||
| 108 | for f in obj.__closure__: | ||
| 109 | obj = f.cell_contents | ||
| 110 | _handle(obj) | ||
| 111 | _walk_closure(obj) | ||
| 112 | method = getattr(case, case._testMethodName, None) | ||
| 113 | _walk_closure(method) | ||
| 114 | |||
| 115 | def _filterTest(self, case): | ||
| 116 | """ | ||
| 117 | Returns True if test case must be filtered, False otherwise. | ||
| 118 | """ | ||
| 119 | if self.filters: | ||
| 120 | filters = self.filters.copy() | ||
| 121 | case_decorators = [cd for cd in case.decorators | ||
| 122 | if cd.__class__ in self.used_filters] | ||
| 123 | |||
| 124 | # Iterate over case decorators to check if needs to be filtered. | ||
| 125 | for cd in case_decorators: | ||
| 126 | if cd.filtrate(filters): | ||
| 127 | return True | ||
| 128 | |||
| 129 | # Case is missing one or more decorators for all the filters | ||
| 130 | # being used, so filter test case. | ||
| 131 | if filters: | ||
| 132 | return True | ||
| 133 | |||
| 134 | return False | ||
| 135 | |||
| 136 | def _getTestCase(self, testCaseClass, tcName): | ||
| 137 | if not hasattr(testCaseClass, '__oeqa_loader'): | ||
| 138 | # In order to support data_vars validation | ||
| 139 | # monkey patch the default setUp/tearDown{Class} to use | ||
| 140 | # the ones provided by OETestCase | ||
| 141 | setattr(testCaseClass, 'setUpClassMethod', | ||
| 142 | getattr(testCaseClass, 'setUpClass')) | ||
| 143 | setattr(testCaseClass, 'tearDownClassMethod', | ||
| 144 | getattr(testCaseClass, 'tearDownClass')) | ||
| 145 | setattr(testCaseClass, 'setUpClass', | ||
| 146 | testCaseClass._oeSetUpClass) | ||
| 147 | setattr(testCaseClass, 'tearDownClass', | ||
| 148 | testCaseClass._oeTearDownClass) | ||
| 149 | |||
| 150 | # In order to support decorators initialization | ||
| 151 | # monkey patch the default setUp/tearDown to use | ||
| 152 | # a setUpDecorators/tearDownDecorators that methods | ||
| 153 | # will call setUp/tearDown original methods. | ||
| 154 | setattr(testCaseClass, 'setUpMethod', | ||
| 155 | getattr(testCaseClass, 'setUp')) | ||
| 156 | setattr(testCaseClass, 'tearDownMethod', | ||
| 157 | getattr(testCaseClass, 'tearDown')) | ||
| 158 | setattr(testCaseClass, 'setUp', testCaseClass._oeSetUp) | ||
| 159 | setattr(testCaseClass, 'tearDown', testCaseClass._oeTearDown) | ||
| 160 | |||
| 161 | setattr(testCaseClass, '__oeqa_loader', True) | ||
| 162 | |||
| 163 | case = testCaseClass(tcName) | ||
| 164 | setattr(case, 'decorators', []) | ||
| 165 | |||
| 166 | return case | ||
| 167 | |||
| 168 | def loadTestsFromTestCase(self, testCaseClass): | ||
| 169 | """ | ||
| 170 | Returns a suite of all tests cases contained in testCaseClass. | ||
| 171 | """ | ||
| 172 | if issubclass(testCaseClass, unittest.suite.TestSuite): | ||
| 173 | raise TypeError("Test cases should not be derived from TestSuite." \ | ||
| 174 | " Maybe you meant to derive from TestCase?") | ||
| 175 | if not issubclass(testCaseClass, self.caseClass): | ||
| 176 | raise TypeError("Test cases need to be derived from %s" % \ | ||
| 177 | caseClass.__name__) | ||
| 178 | |||
| 179 | |||
| 180 | testCaseNames = self.getTestCaseNames(testCaseClass) | ||
| 181 | if not testCaseNames and hasattr(testCaseClass, 'runTest'): | ||
| 182 | testCaseNames = ['runTest'] | ||
| 183 | |||
| 184 | suite = [] | ||
| 185 | for tcName in testCaseNames: | ||
| 186 | case = self._getTestCase(testCaseClass, tcName) | ||
| 187 | # Filer by case id | ||
| 188 | if not (self.tests and not 'all' in self.tests | ||
| 189 | and not getCaseID(case) in self.tests): | ||
| 190 | self._handleTestCaseDecorators(case) | ||
| 191 | |||
| 192 | # Filter by decorators | ||
| 193 | if not self._filterTest(case): | ||
| 194 | self._registerTestCase(case) | ||
| 195 | suite.append(case) | ||
| 196 | |||
| 197 | return self.suiteClass(suite) | ||
| 198 | |||
| 199 | def discover(self): | ||
| 200 | big_suite = self.suiteClass() | ||
| 201 | for path in self.module_paths: | ||
| 202 | _find_duplicated_modules(big_suite, path) | ||
| 203 | suite = super(OETestLoader, self).discover(path, | ||
| 204 | pattern='*.py', top_level_dir=path) | ||
| 205 | big_suite.addTests(suite) | ||
| 206 | |||
| 207 | cases = None | ||
| 208 | discover_classes = [clss for clss in decoratorClasses | ||
| 209 | if issubclass(clss, OETestDiscover)] | ||
| 210 | for clss in discover_classes: | ||
| 211 | cases = clss.discover(self.tc._registry) | ||
| 212 | |||
| 213 | return self.suiteClass(cases) if cases else big_suite | ||
| 214 | |||
| 215 | # XXX After Python 3.5, remove backward compatibility hacks for | ||
| 216 | # use_load_tests deprecation via *args and **kws. See issue 16662. | ||
| 217 | if sys.version_info >= (3,5): | ||
| 218 | def loadTestsFromModule(self, module, *args, pattern=None, **kws): | ||
| 219 | if not self.modules or "all" in self.modules or \ | ||
| 220 | module.__name__ in self.modules: | ||
| 221 | return super(OETestLoader, self).loadTestsFromModule( | ||
| 222 | module, *args, pattern=pattern, **kws) | ||
| 223 | else: | ||
| 224 | return self.suiteClass() | ||
| 225 | else: | ||
| 226 | def loadTestsFromModule(self, module, use_load_tests=True): | ||
| 227 | """ | ||
| 228 | Returns a suite of all tests cases contained in module. | ||
| 229 | """ | ||
| 230 | if not self.modules or "all" in self.modules or \ | ||
| 231 | module.__name__ in self.modules: | ||
| 232 | return super(OETestLoader, self).loadTestsFromModule( | ||
| 233 | module, use_load_tests) | ||
| 234 | else: | ||
| 235 | return self.suiteClass() | ||
