diff --git a/avocado/core/job.py b/avocado/core/job.py index f3f8a1499695712d8b0efe6545e98046ad7246ac..c257ed5531b55843edd868c7872d81d9a9a27119 100644 --- a/avocado/core/job.py +++ b/avocado/core/job.py @@ -114,7 +114,6 @@ class Job(object): self.test_index = 1 self.status = "RUNNING" self.result_proxy = result.TestResultProxy() - self.test_loader = loader.TestLoaderProxy() self.sysinfo = None self.timeout = getattr(self.args, 'job_timeout', 0) @@ -151,18 +150,6 @@ class Job(object): def _remove_job_results(self): shutil.rmtree(self.logdir, ignore_errors=True) - def _make_test_loader(self): - for key in self.args.__dict__.keys(): - loader_class_candidate = getattr(self.args, key) - try: - if issubclass(loader_class_candidate, loader.TestLoader): - loader_plugin = loader_class_candidate(self) - self.test_loader.add_loader_plugin(loader_plugin) - except TypeError: - pass - filesystem_loader = loader.TestLoader(self) - self.test_loader.add_loader_plugin(filesystem_loader) - def _make_test_runner(self): if hasattr(self.args, 'test_runner'): test_runner_class = self.args.test_runner @@ -244,10 +231,6 @@ class Job(object): if isinstance(urls, str): urls = urls.split() - if not urls: - e_msg = "Empty test ID. A test path or alias must be provided" - raise exceptions.OptionValidationError(e_msg) - return urls def _make_test_suite(self, urls=None): @@ -259,27 +242,15 @@ class Job(object): :returns: a test suite (a list of test factories) """ urls = self._handle_urls(urls) - - self._make_test_loader() - - return self.test_loader.discover(urls) - - def _validate_test_suite(self, test_suite): + loader.loader.load_plugins(self.args) try: - # Do not attempt to validate the tests given on the command line if - # the tests will not be copied from this system to a remote one - # using the remote plugin features - if not getattr(self.args, 'remote_no_copy', False): - error_msg_parts = self.test_loader.validate_ui(test_suite) - else: - error_msg_parts = [] + return loader.loader.discover(urls) + except loader.LoaderUnhandledUrlError, details: + self._remove_job_results() + raise exceptions.OptionValidationError(details) except KeyboardInterrupt: - raise exceptions.JobError('Command interrupted by user...') - - if error_msg_parts: self._remove_job_results() - e_msg = '\n'.join(error_msg_parts) - raise exceptions.OptionValidationError(e_msg) + raise exceptions.JobError('Command interrupted by user...') def _filter_test_suite(self, test_suite): # Filter tests methods with params.filter and methodName @@ -441,14 +412,14 @@ class Job(object): that configure a job failure. """ self._setup_job_results() - + self.view.start_file_logging(self.logfile, + self.loglevel, + self.unique_id) test_suite = self._make_test_suite(urls) - self._validate_test_suite(test_suite) - test_suite = self._filter_test_suite(test_suite) if not test_suite: - e_msg = ("No tests found within the specified path(s) " - "(Possible reasons: File ownership, permissions, " - "filters, typos)") + self._remove_job_results() + e_msg = ("No tests found for given urls, try 'avocado list -V %s' " + "for details" % (" ".join(urls) if urls else "\b")) raise exceptions.OptionValidationError(e_msg) try: @@ -461,10 +432,6 @@ class Job(object): self._make_test_runner() self._start_sysinfo() - self.view.start_file_logging(self.logfile, - self.loglevel, - self.unique_id) - self._log_job_debug_info(mux) self.view.logfile = self.logfile diff --git a/avocado/core/loader.py b/avocado/core/loader.py index ceab8ba1d30ff44d4c077e8a2e4a17fedb21ba1c..d190243a0ecdc2bbbac3f172518341ddcab78678 100644 --- a/avocado/core/loader.py +++ b/avocado/core/loader.py @@ -22,11 +22,14 @@ import inspect import os import re import sys +import fnmatch from . import data_dir -from . import test from . import output -from ..utils import path, stacktrace +from . import test +from .settings import settings +from ..utils import path +from ..utils import stacktrace try: import cStringIO as StringIO @@ -34,122 +37,149 @@ except ImportError: import StringIO -class _DebugJob(object): +DEFAULT = False # Show default tests (for execution) +AVAILABLE = None # Available tests (for listing purposes) +ALL = True # All tests (inicluding broken ones) - def __init__(self, args=None): - self.logdir = '.' - self.args = args +class LoaderError(Exception): + + """ Loader exception """ -class BrokenSymlink(object): pass -class AccessDeniedPath(object): - pass +class InvalidLoaderPlugin(LoaderError): + """ Invalid loader plugin """ -class InvalidLoaderPlugin(Exception): pass +class LoaderUnhandledUrlError(LoaderError): + + """ Urls not handled by any loader """ + + def __init__(self, unhandled_urls, plugins): + super(LoaderUnhandledUrlError, self).__init__() + self.unhandled_urls = unhandled_urls + self.plugins = [_.name for _ in plugins] + + def __str__(self): + return ("Unable to discover url(s) '%s' with loader plugins(s) '%s', " + "try running 'avocado list -V %s' to see the details." + % ("', '" .join(self.unhandled_urls), + "', '".join(self.plugins), + " ".join(self.unhandled_urls))) + + class TestLoaderProxy(object): def __init__(self): - self.loader_plugins = [] + self._initialized_plugins = [] + self.registered_plugins = [] self.url_plugin_mapping = {} - def add_loader_plugin(self, plugin): - if not isinstance(plugin, TestLoader): + def register_plugin(self, plugin): + try: + if issubclass(plugin, TestLoader): + self.registered_plugins.append(plugin) + else: + raise ValueError + except ValueError: raise InvalidLoaderPlugin("Object %s is not an instance of " "TestLoader" % plugin) - self.loader_plugins.append(plugin) def load_plugins(self, args): - for key in args.__dict__.keys(): - loader_class_candidate = getattr(args, key) - try: - if issubclass(loader_class_candidate, TestLoader): - loader_plugin = loader_class_candidate(args=args) - self.add_loader_plugin(loader_plugin) - except TypeError: - pass - filesystem_loader = TestLoader() - self.add_loader_plugin(filesystem_loader) - - def get_extra_listing(self, args): - for loader_plugin in self.loader_plugins: - loader_plugin.get_extra_listing(args) + self._initialized_plugins = [] + # Add (default) file loader if not already registered + if FileLoader not in self.registered_plugins: + self.registered_plugins.append(FileLoader) + # Load plugin by the priority from settings + names = [_.name for _ in self.registered_plugins] + priority = settings.get_value("plugins", "loader_plugins_priority", + list, []) + sorted_plugins = [self.registered_plugins[names.index(name)] + for name in priority + if name in names] + for plugin in sorted_plugins: + self._initialized_plugins.append(plugin(args)) + for plugin in self.registered_plugins: + if plugin in sorted_plugins: + continue + self._initialized_plugins.append(plugin(args)) + + def get_extra_listing(self): + for loader_plugin in self._initialized_plugins: + loader_plugin.get_extra_listing() def get_base_keywords(self): base_path = [] - for loader_plugin in self.loader_plugins: + for loader_plugin in self._initialized_plugins: base_path += loader_plugin.get_base_keywords() return base_path def get_type_label_mapping(self): mapping = {} - for loader_plugin in self.loader_plugins: + for loader_plugin in self._initialized_plugins: mapping.update(loader_plugin.get_type_label_mapping()) return mapping def get_decorator_mapping(self): mapping = {} - for loader_plugin in self.loader_plugins: + for loader_plugin in self._initialized_plugins: mapping.update(loader_plugin.get_decorator_mapping()) return mapping - def discover(self, urls, list_non_tests=False): + def discover(self, urls, list_tests=False): """ Discover (possible) tests from test urls. :param urls: a list of tests urls; if [] use plugin defaults :type urls: list - :param list_non_tests: Whether to list non tests (for listing methods) - :type list_non_tests: bool + :param list_tests: Limit tests to be displayed (loader.ALL|DEFAULT...) :return: A list of test factories (tuples (TestClass, test_params)) """ - test_factories = [] - for loader_plugin in self.loader_plugins: - if urls: - _urls = urls - else: - _urls = loader_plugin.get_base_keywords() - for url in _urls: - if url in self.url_plugin_mapping: - continue + def handle_exception(plugin, details): + # FIXME: Introduce avocado.exceptions logger and use here + stacktrace.log_message("Test discovery plugin %s failed: " + "%s" % (plugin, details), + 'avocado.app.exceptions') + # FIXME: Introduce avocado.traceback logger and use here + stacktrace.log_exc_info(sys.exc_info(), + 'avocado.app.tracebacks') + tests = [] + unhandled_urls = [] + if not urls: + for loader_plugin in self._initialized_plugins: try: - params_list_from_url = loader_plugin.discover_url(url) - if list_non_tests: - for params in params_list_from_url: - params['omit_non_tests'] = False - if params_list_from_url: - test_factory = loader_plugin.discover(params_list_from_url) - self.url_plugin_mapping[url] = loader_plugin - test_factories += test_factory + tests.extend(loader_plugin.discover(None, + list_tests)) except Exception, details: - # FIXME: Introduce avocado.exceptions logger and use here - stacktrace.log_message("Test discovery plugin %s failed: " - "%s" % (loader_plugin, details), - 'avocado.app.exceptions') - # FIXME: Introduce avocado.traceback logger and use here - stacktrace.log_exc_info(sys.exc_info(), - 'avocado.app.tracebacks') - return test_factories - - def validate_ui(self, test_suite, ignore_missing=False, - ignore_not_test=False, ignore_broken_symlinks=False, - ignore_access_denied=False): - e_msg = [] - for tuple_class_params in test_suite: - for key in self.url_plugin_mapping: - if tuple_class_params[1]['params']['id'].startswith(key): - loader_plugin = self.url_plugin_mapping[key] - e_msg += loader_plugin.validate_ui(test_suite=[tuple_class_params], ignore_missing=ignore_missing, - ignore_not_test=ignore_not_test, - ignore_broken_symlinks=ignore_broken_symlinks, - ignore_access_denied=ignore_access_denied) - return e_msg + handle_exception(loader_plugin, details) + else: + for url in urls: + handled = False + for loader_plugin in self._initialized_plugins: + try: + _test = loader_plugin.discover(url, list_tests) + if _test: + tests.extend(_test) + handled = True + if not list_tests: + break # Don't process other plugins + except Exception, details: + handle_exception(loader_plugin, details) + if not handled: + unhandled_urls.append(url) + if unhandled_urls: + if list_tests: + tests.extend([(test.MissingTest, {'name': url}) + for url in list_tests]) + else: + raise LoaderUnhandledUrlError(unhandled_urls, + self._initialized_plugins) + return tests def load_test(self, test_factory): """ @@ -167,48 +197,82 @@ class TestLoaderProxy(object): class TestLoader(object): """ - Test loader class. + Base for test loader classes """ - def __init__(self, job=None, args=None): - - if job is None: - job = _DebugJob(args=args) - self.job = job + def __init__(self, args): + self.args = args - def get_extra_listing(self, args): + def get_extra_listing(self): pass - def get_base_keywords(self): + def get_type_label_mapping(self): """ - Get base keywords to locate tests (path to test dir in this case). - - Used to list all tests available in virt-test. + Get label mapping for display in test listing. - :return: list with path strings. + :return: Dict {TestClass: 'TEST_LABEL_STRING'} """ - return [data_dir.get_test_dir()] + raise NotImplementedError - def get_type_label_mapping(self): + def get_decorator_mapping(self): """ Get label mapping for display in test listing. - :return: Dict {TestClass: 'TEST_LABEL_STRING'} + :return: Dict {TestClass: decorator function} """ + raise NotImplementedError + + def discover(self, url, list_tests=False): + """ + Discover (possible) tests from an url. + + :param url: the url to be inspected. + :type url: str + :return: a list of test matching the url as params. + """ + raise NotImplementedError + + +class BrokenSymlink(object): + + """ Dummy object to represent url pointing to a BrokenSymlink path """ + + pass + + +class AccessDeniedPath(object): + + """ Dummy object to represent url pointing to a inaccessible path """ + + pass + + +class FilteredOut(object): + + """ Dummy object to represent test filtered out by the optional mask """ + + pass + + +class FileLoader(TestLoader): + + """ + Test loader class. + """ + + name = 'file' + + def get_type_label_mapping(self): return {test.SimpleTest: 'SIMPLE', test.BuggyTest: 'BUGGY', test.NotATest: 'NOT_A_TEST', test.MissingTest: 'MISSING', BrokenSymlink: 'BROKEN_SYMLINK', AccessDeniedPath: 'ACCESS_DENIED', - test.Test: 'INSTRUMENTED'} + test.Test: 'INSTRUMENTED', + FilteredOut: 'FILTERED'} def get_decorator_mapping(self): - """ - Get label mapping for display in test listing. - - :return: Dict {TestClass: decorator function} - """ term_support = output.TermSupport() return {test.SimpleTest: term_support.healthy_str, test.BuggyTest: term_support.fail_header_str, @@ -216,7 +280,64 @@ class TestLoader(object): test.MissingTest: term_support.fail_header_str, BrokenSymlink: term_support.fail_header_str, AccessDeniedPath: term_support.fail_header_str, - test.Test: term_support.healthy_str} + test.Test: term_support.healthy_str, + FilteredOut: term_support.warn_header_str} + + def discover(self, url, list_tests=DEFAULT): + """ + Discover (possible) tests from a directory. + + Recursively walk in a directory and find tests params. + The tests are returned in alphabetic order. + + :param url: the directory path to inspect. + :param list_tests: list corrupted/invalid tests too + :return: list of matching tests + """ + if url is None: + if list_tests is DEFAULT: + return [] # Return empty set when not listing details + else: + url = data_dir.get_test_dir() + ignore_suffix = ('.data', '.pyc', '.pyo', '__init__.py', + '__main__.py') + + # Look for filename:test_method pattern + subtests_filter = None + if ':' in url: + _url, _subtests_filter = url.split(':', 1) + if os.path.exists(_url): # otherwise it's ':' in the file name + url = _url + subtests_filter = _subtests_filter + + if not os.path.isdir(url): # Single file + return self._make_tests(url, list_tests, subtests_filter) + + tests = [] + + def add_test_from_exception(exception): + """ If the exc.filename is valid test it's added to tests """ + tests.extend(self._make_tests(exception.filename, list_tests)) + + def skip_non_test(exception): + """ Always return None """ + return None + + if list_tests: # ALL => include everything + onerror = add_test_from_exception + else: # DEFAULT, AVAILABLE => skip missing tests + onerror = skip_non_test + + for dirpath, _, filenames in os.walk(url, onerror=onerror): + for file_name in filenames: + if not file_name.startswith('.'): + for suffix in ignore_suffix: + if file_name.endswith(suffix): + break + else: + pth = os.path.join(dirpath, file_name) + tests.extend(self._make_tests(pth, list_tests)) + return tests def _is_unittests_like(self, test_class, pattern='test'): for name, _ in inspect.getmembers(test_class, inspect.ismethod): @@ -231,39 +352,13 @@ class TestLoader(object): test_methods.append((name, obj)) return test_methods - def _make_missing_test(self, test_name, params): - test_class = test.MissingTest - test_parameters = {'name': test_name, - 'base_logdir': self.job.logdir, - 'params': params, - 'job': self.job} - return test_class, test_parameters - - def _make_not_a_test(self, test_name, params): - test_class = test.NotATest - test_parameters = {'name': test_name, - 'base_logdir': self.job.logdir, - 'params': params, - 'job': self.job} - return test_class, test_parameters - - def _make_simple_test(self, test_path, params): - test_class = test.SimpleTest - test_parameters = {'name': test_path, - 'base_logdir': self.job.logdir, - 'params': params, - 'job': self.job} - return test_class, test_parameters - - def _make_tests(self, test_name, test_path, params): + def _make_avocado_tests(self, test_path, make_broken, subtests_filter, + test_name=None): + if test_name is None: + test_name = test_path module_name = os.path.basename(test_path).split('.')[0] test_module_dir = os.path.dirname(test_path) sys.path.append(test_module_dir) - test_class = None - test_parameters = {'name': test_name, - 'base_logdir': self.job.logdir, - 'params': params, - 'job': self.job} stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr try: sys.stdin = None @@ -272,34 +367,44 @@ class TestLoader(object): f, p, d = imp.find_module(module_name, [test_module_dir]) test_module = imp.load_module(module_name, f, p, d) f.close() - for name, obj in inspect.getmembers(test_module): - if inspect.isclass(obj) and inspect.getmodule(obj) == test_module: + for _, obj in inspect.getmembers(test_module): + if (inspect.isclass(obj) and + inspect.getmodule(obj) == test_module): if issubclass(obj, test.Test): test_class = obj break + else: + if os.access(test_path, os.X_OK): + # Module does not have an avocado test class inside but + # it's executable, let's execute it. + return self._make_test(test.SimpleTest, test_path) + else: + # Module does not have an avocado test class inside, and + # it's not executable. Not a Test. + return make_broken(test.NotATest, test_path) if test_class is not None: # Module is importable and does have an avocado test class # inside, let's proceed. if self._is_unittests_like(test_class): test_factories = [] + test_parameters = {'name': test_name} + if subtests_filter: + test_parameters['params'] = {'filter': subtests_filter} for test_method in self._make_unittests_like(test_class): - copy_test_parameters = test_parameters.copy() - copy_test_parameters['methodName'] = test_method[0] - class_and_method_name = ':%s.%s' % (test_class.__name__, test_method[0]) - copy_test_parameters['name'] += class_and_method_name - test_factories.append([test_class, copy_test_parameters]) + name = test_name + ':%s.%s' % (test_class.__name__, + test_method[0]) + if (subtests_filter is not None and + not fnmatch.fnmatch(test_method[0], + subtests_filter)): + test_factories.extend(make_broken(FilteredOut, + name)) + else: + tst = (test_class, {'name': name, + 'methodName': test_method[0]}) + test_factories.append(tst) return test_factories - else: - if os.access(test_path, os.X_OK): - # Module does not have an avocado test class inside but - # it's executable, let's execute it. - test_class = test.SimpleTest - test_parameters['name'] = test_path else: - # Module does not have an avocado test class inside, and - # it's not executable. Not a Test. - test_class = test.NotATest - test_parameters['name'] = test_path + return self._make_test(test_class, test_name) # Since a lot of things can happen here, the broad exception is # justified. The user will get it unadulterated anyway, and avocado @@ -310,250 +415,85 @@ class TestLoader(object): if os.access(test_path, os.X_OK): # Module can't be imported, and it's executable. Let's try to # execute it. - test_class = test.SimpleTest - test_parameters['name'] = test_path + return self._make_test(test.SimpleTest, test_path) else: # Module can't be imported and it's not an executable. Let's # see if there's an avocado import into the test. Although # not entirely reliable, we hope it'll be good enough. - likely_avocado_test = False with open(test_path, 'r') as test_file_obj: test_contents = test_file_obj.read() # Actual tests will have imports starting on column 0 patterns = ['^from avocado.* import', '^import avocado.*'] for pattern in patterns: if re.search(pattern, test_contents, re.MULTILINE): - likely_avocado_test = True break - if likely_avocado_test: - test_class = test.BuggyTest - params['exception'] = details - else: - test_class = test.NotATest + else: + return make_broken(test.NotATest, test_path) + return make_broken(test.BuggyTest, test_path, + {'exception': details}) finally: sys.stdin = stdin sys.stdout = stdout sys.stderr = stderr + sys.path.remove(test_module_dir) - sys.path.pop(sys.path.index(test_module_dir)) - - return [(test_class, test_parameters)] - - def discover_tests(self, params): + @staticmethod + def _make_test(klass, uid, params=None): + """ + Create test template + :param klass: test class + :param uid: test uid (by default used as id and name) + :param params: optional params (id won't be overriden when present) """ - Try to discover and resolve tests. + if not params: + params = {} + params.setdefault('id', uid) + return [(klass, {'name': uid, 'params': params})] - :param params: dictionary with test parameters. - :type params: dict - :return: a list of test factories (a pair of test class and test parameters). + def _make_tests(self, test_path, list_non_tests, subtests_filter=None): + """ + Create test templates from given path + :param test_path: File system path + :param list_non_tests: include bad tests (NotATest, BrokenSymlink,...) + :param subtests_filter: optional filter of methods for avocado tests """ - test_name = test_path = params.get('id') + def ignore_broken(klass, uid, params=None): + """ Always return empty list """ + return [] + + if list_non_tests: # return broken test with params + make_broken = self._make_test + else: # return empty set instead + make_broken = ignore_broken + test_name = test_path if os.path.exists(test_path): if os.access(test_path, os.R_OK) is False: - return [(AccessDeniedPath, {'params': {'id': test_path}})] + return make_broken(AccessDeniedPath, test_path) path_analyzer = path.PathInspector(test_path) if path_analyzer.is_python(): - test_factories = self._make_tests(test_name, - test_path, - params) - return test_factories + return self._make_avocado_tests(test_path, make_broken, + subtests_filter) else: if os.access(test_path, os.X_OK): - test_class, test_parameters = self._make_simple_test(test_path, - params) + return self._make_test(test.SimpleTest, test_path) else: - test_class, test_parameters = self._make_not_a_test(test_path, - params) + return make_broken(test.NotATest, test_path) else: if os.path.islink(test_path): try: if not os.path.isfile(os.readlink(test_path)): - return [(BrokenSymlink, {'params': {'id': test_path}})] + return make_broken(BrokenSymlink, test_path) except OSError: - return [(AccessDeniedPath, {'params': {'id': test_path}})] + return make_broken(AccessDeniedPath, test_path) # Try to resolve test ID (keep compatibility) rel_path = '%s.py' % test_name test_path = os.path.join(data_dir.get_test_dir(), rel_path) if os.path.exists(test_path): - test_factories = self._make_tests(rel_path, test_path, params) - return test_factories - else: - test_class, test_parameters = self._make_missing_test( - test_name, params) - return [(test_class, test_parameters)] - - def discover_url(self, url): - """ - Discover (possible) tests from a directory. - - Recursively walk in a directory and find tests params. - The tests are returned in alphabetic order. - - :param dir_path: the directory path to inspect. - :type dir_path: str - :param ignore_suffix: list of suffix to ignore in paths. - :type ignore_suffix: list - :return: a list of test params (each one a dictionary). - """ - ignore_suffix = ('.data', '.pyc', '.pyo', '__init__.py', - '__main__.py') - params_list = [] - - # Look for filename:test_method pattern - if ':' in url: - url, filter_pattern = url.split(':', 1) - else: - filter_pattern = None - - def onerror(exception): - norm_url = os.path.abspath(url) - norm_error_filename = os.path.abspath(exception.filename) - if os.path.isdir(norm_url) and norm_url != norm_error_filename: - omit_non_tests = True + return self._make_avocado_tests(test_path, list_non_tests, + subtests_filter, rel_path) else: - omit_non_tests = False + return make_broken(test.MissingTest, test_name) - params_list.append({'id': exception.filename, - 'filter': filter_pattern, - 'omit_non_tests': omit_non_tests}) - for dirpath, dirnames, filenames in os.walk(url, onerror=onerror): - for dir_name in dirnames: - if dir_name.startswith('.'): - dirnames.pop(dirnames.index(dir_name)) - for file_name in filenames: - if not file_name.startswith('.'): - ignore = False - for suffix in ignore_suffix: - if file_name.endswith(suffix): - ignore = True - if not ignore: - pth = os.path.join(dirpath, file_name) - params_list.append({'id': pth, - 'omit_non_tests': True}) - return params_list - - def discover_urls(self, urls): - """ - Discover (possible) tests from test urls. - - :param urls: a list of tests urls. - :type urls: list - :return: a list of test params (each one a dictionary). - """ - params_list = [] - for url in urls: - if url == '': - continue - params_list.extend(self.discover_url(url)) - return params_list - - def discover(self, params_list): - """ - Discover tests for test suite. - - :param params_list: a list of test parameters. - :type params_list: list - :return: a test suite (a list of test factories). - """ - test_suite = [] - for params in params_list: - test_factories = self.discover_tests(params) - for test_factory in test_factories: - if test_factory is None: - continue - test_class, test_parameters = test_factory - if test_class in [test.NotATest, BrokenSymlink, AccessDeniedPath]: - if not params.get('omit_non_tests'): - test_suite.append((test_class, test_parameters)) - else: - test_suite.append((test_class, test_parameters)) - return test_suite - - @staticmethod - def validate(test_suite): - """ - Find missing files/non-tests provided by the user in the input. - - Used mostly for user input validation. - - :param test_suite: List with tuples (test_class, test_params) - :return: list of missing files. - """ - missing = [] - not_test = [] - broken_symlink = [] - access_denied = [] - for suite in test_suite: - if suite[0] == test.MissingTest: - missing.append(suite[1]['params']['id']) - elif suite[0] == test.NotATest: - not_test.append(suite[1]['params']['id']) - elif suite[0] == BrokenSymlink: - broken_symlink.append(suite[1]['params']['id']) - elif suite[0] == AccessDeniedPath: - access_denied.append(suite[1]['params']['id']) - - return missing, not_test, broken_symlink, access_denied - - def validate_ui(self, test_suite, ignore_missing=False, - ignore_not_test=False, ignore_broken_symlinks=False, - ignore_access_denied=False): - """ - Validate test suite and deliver error messages to the UI - :param test_suite: List of tuples (test_class, test_params) - :type test_suite: list - :return: List with error messages - :rtype: list - """ - (missing, not_test, broken_symlink, - access_denied) = self.validate(test_suite) - broken_symlink_msg = '' - if (not ignore_broken_symlinks) and broken_symlink: - if len(broken_symlink) == 1: - broken_symlink_msg = ("Cannot access '%s': Broken symlink" % - ", ".join(broken_symlink)) - elif len(broken_symlink) > 1: - broken_symlink_msg = ("Cannot access '%s': Broken symlinks" % - ", ".join(broken_symlink)) - access_denied_msg = '' - if (not ignore_access_denied) and access_denied: - if len(access_denied) == 1: - access_denied_msg = ("Cannot access '%s': Access denied" % - ", ".join(access_denied)) - elif len(access_denied) > 1: - access_denied_msg = ("Cannot access '%s': Access denied" % - ", ".join(access_denied)) - missing_msg = '' - if (not ignore_missing) and missing: - if len(missing) == 1: - missing_msg = ("Cannot access '%s': File not found" % - ", ".join(missing)) - elif len(missing) > 1: - missing_msg = ("Cannot access '%s': Files not found" % - ", ".join(missing)) - not_test_msg = '' - if (not ignore_not_test) and not_test: - if len(not_test) == 1: - not_test_msg = ("File '%s' is not an avocado test" % - ", ".join(not_test)) - elif len(not_test) > 1: - not_test_msg = ("Files '%s' are not avocado tests" % - ", ".join(not_test)) - - return [msg for msg in - [access_denied_msg, broken_symlink_msg, missing_msg, - not_test_msg] if msg] - - def load_test(self, test_factory): - """ - Load test from the test factory. - - :param test_factory: a pair of test class and parameters. - :type params: tuple - :return: an instance of :class:`avocado.core.test.Test`. - """ - test_class, test_parameters = test_factory - test_instance = test_class(**test_parameters) - return test_instance +loader = TestLoaderProxy() diff --git a/avocado/core/log.py b/avocado/core/log.py index 916ef9381e927ea99d962c360ff05d09824e3532..544f61ad236416aa938d9b23df0e461ca84ae1d4 100644 --- a/avocado/core/log.py +++ b/avocado/core/log.py @@ -78,6 +78,11 @@ DEFAULT_LOGGING = { 'level': 'INFO', 'propagate': False, }, + 'avocado.app.tracebacks': { + 'handlers': ['null'], # change this to 'error' to see tracebacks + 'level': 'ERROR', + 'propagate': False, + }, 'avocado.test': { 'handlers': ['null'], 'level': 'DEBUG', diff --git a/avocado/core/multiplexer.py b/avocado/core/multiplexer.py index 4abc63ca3d921cec4e5dfed9d093961b20a93a7d..b8c1760a1c7c132e970da1ce70b0e83c2f959d8c 100644 --- a/avocado/core/multiplexer.py +++ b/avocado/core/multiplexer.py @@ -421,7 +421,7 @@ class Mux(object): i = None for i, variant in enumerate(self.variants): test_factory = [template[0], template[1].copy()] - inject_params = test_factory[1]['params'].get( + inject_params = test_factory[1].get('params', {}).get( 'avocado_inject_params', False) # Test providers might want to keep their original params and # only append avocado parameters to a special 'avocado_params' diff --git a/avocado/core/output.py b/avocado/core/output.py index 857ab77c97f2ef1157a0274fd58b407274cab730..6e1474a172a2caf8e3d292057a4459531d9ad2bc 100644 --- a/avocado/core/output.py +++ b/avocado/core/output.py @@ -609,15 +609,17 @@ class View(object): self.file_handler = logging.FileHandler(filename=logfile) self.file_handler.setLevel(loglevel) - fmt = ('%(asctime)s %(module)-10.10s L%(lineno)-.4d %(' + fmt = ('%(asctime)s %(module)-16.16s L%(lineno)-.4d %(' 'levelname)-5.5s| %(message)s') formatter = logging.Formatter(fmt=fmt, datefmt='%H:%M:%S') self.file_handler.setFormatter(formatter) test_logger = logging.getLogger('avocado.test') - linux_logger = logging.getLogger('avocado.linux') test_logger.addHandler(self.file_handler) - linux_logger.addHandler(self.file_handler) + test_logger.setLevel(loglevel) + root_logger = logging.getLogger() + root_logger.addHandler(self.file_handler) + root_logger.setLevel(loglevel) def stop_file_logging(self): """ @@ -625,6 +627,8 @@ class View(object): """ test_logger = logging.getLogger('avocado.test') linux_logger = logging.getLogger('avocado.linux') + root_logger = logging.getLogger() test_logger.removeHandler(self.file_handler) linux_logger.removeHandler(self.file_handler) + root_logger.removeHandler(self.file_handler) self.file_handler.close() diff --git a/avocado/core/plugins/runner.py b/avocado/core/plugins/runner.py index cc5a922be4af98dfa883f2e7cbf42bd2da190db5..f8599efa9bf4645fea0fc673227caa0e83dc2aaf 100644 --- a/avocado/core/plugins/runner.py +++ b/avocado/core/plugins/runner.py @@ -46,7 +46,7 @@ class TestRunner(plugin.Plugin): 'run', help='Run one or more tests (native test, test alias, binary or script)') - self.parser.add_argument('url', type=str, default=[], nargs='+', + self.parser.add_argument('url', type=str, default=[], nargs='*', help='List of test IDs (aliases or paths)') self.parser.add_argument('-z', '--archive', action='store_true', default=False, diff --git a/avocado/core/plugins/test_list.py b/avocado/core/plugins/test_list.py index 6e767e380df4b05b1ebafacce7068ee5a479852b..b6588a98e9b4f001ad12e8c0315dd5bc91134bc4 100644 --- a/avocado/core/plugins/test_list.py +++ b/avocado/core/plugins/test_list.py @@ -32,38 +32,27 @@ class TestLister(object): use_paginator = args.paginator == 'on' self.view = output.View(app_args=args, use_paginator=use_paginator) self.term_support = output.TermSupport() - self.test_loader = loader.TestLoaderProxy() - self.test_loader.load_plugins(args) + loader.loader.load_plugins(args) self.args = args def _extra_listing(self): - self.test_loader.get_extra_listing(self.args) - - def _get_keywords(self): - keywords = self.test_loader.get_base_keywords() - if self.args.keywords: - keywords = self.args.keywords - return keywords + loader.loader.get_extra_listing() def _get_test_suite(self, paths): - return self.test_loader.discover(paths, list_non_tests=self.args.verbose) - - def _validate_test_suite(self, test_suite): - error_msg_parts = self.test_loader.validate_ui(test_suite, - ignore_not_test=True, - ignore_access_denied=True, - ignore_broken_symlinks=True) - if error_msg_parts: - for error_msg in error_msg_parts: - self.view.notify(event='error', msg=error_msg) + list_tests = loader.ALL if self.args.verbose else loader.AVAILABLE + try: + return loader.loader.discover(paths, + list_tests=list_tests) + except loader.LoaderUnhandledUrlError, details: + self.view.notify(event="error", msg=str(details)) self.view.cleanup() sys.exit(exit_codes.AVOCADO_FAIL) def _get_test_matrix(self, test_suite): test_matrix = [] - type_label_mapping = self.test_loader.get_type_label_mapping() - decorator_mapping = self.test_loader.get_decorator_mapping() + type_label_mapping = loader.loader.get_type_label_mapping() + decorator_mapping = loader.loader.get_decorator_mapping() stats = {} for value in type_label_mapping.values(): @@ -115,7 +104,6 @@ class TestLister(object): def _list(self): self._extra_listing() test_suite = self._get_test_suite(self.args.keywords) - self._validate_test_suite(test_suite) test_matrix, stats = self._get_test_matrix(test_suite) self._display(test_matrix, stats) diff --git a/avocado/core/runner.py b/avocado/core/runner.py index 8eb144b980ad3655a08f2ab654d53a1f7004745c..428384be970931bddf2eec3f339db915e8f76a14 100644 --- a/avocado/core/runner.py +++ b/avocado/core/runner.py @@ -30,6 +30,7 @@ from . import exceptions from . import output from . import status from . import exit_codes +from .loader import loader from ..utils import wait from ..utils import stacktrace from ..utils import runtime @@ -78,7 +79,7 @@ class TestRunner(object): sys.stderr = output.LoggingFile(logger=logger_list_stderr) try: - instance = self.job.test_loader.load_test(test_factory) + instance = loader.load_test(test_factory) if instance.runner_queue is None: instance.runner_queue = queue runtime.CURRENT_TEST = instance @@ -261,6 +262,8 @@ class TestRunner(object): deadline = None for test_template in test_suite: + test_template[1]['base_logdir'] = self.job.logdir + test_template[1]['job'] = self.job for test_factory in mux.itertests(test_template): if deadline is not None and time.time() > deadline: test_parameters = test_factory[1] diff --git a/avocado/utils/debug.py b/avocado/utils/debug.py index 36b64442d49b05bfe97664e5f58dc96d04d515e8..f493b27652adc59e22215ba0dd79efa0ef0fb48b 100644 --- a/avocado/utils/debug.py +++ b/avocado/utils/debug.py @@ -17,6 +17,7 @@ This file contains tools for (not only) Avocado developers. """ import logging import time +import os # Use this for debug logging @@ -43,3 +44,49 @@ def measure_duration(func): LOGGER.debug("PERF: %s: (%ss, %ss)", func, duration, __MEASURE_DURATION[func]) return wrapper + + +def log_calls_class(length=None): + """ + Use this as decorator to log the function methods' calls. + :param length: Max message length + """ + def wrap(orig_cls): + for key, attr in orig_cls.__dict__.iteritems(): + if callable(attr): + setattr(orig_cls, key, + _log_calls(attr, length, orig_cls.__name__)) + return orig_cls + return wrap + + +def _log_calls(func, length=None, cls_name=None): + """ + log_calls wrapper function + """ + def wrapper(*args, **kwargs): + """ Wrapper function """ + msg = ("CALL: %s:%s%s(%s, %s)" + % (os.path.relpath(func.func_code.co_filename), + cls_name, func.func_name, + ", ".join([str(_) for _ in args]), + ", ".join(["%s=%s" % (key, value) + for key, value in kwargs.iteritems()]))) + if length: + msg = msg[:length] + LOGGER.debug(msg) + return func(*args, **kwargs) + if cls_name: + cls_name = cls_name + "." + return wrapper + + +def log_calls(length=None, cls_name=None): + """ + Use this as decorator to log the function call altogether with arguments. + :param length: Max message length + :param cls_name: Optional class name prefix + """ + def wrap(func): + return _log_calls(func, length, cls_name) + return wrap diff --git a/etc/avocado/avocado.conf b/etc/avocado/avocado.conf index 859db4bcce0071a72f58998bf125a9e5405a3b88..0d3c4d668c17d1024cbbe6534938c67f70e1bc74 100644 --- a/etc/avocado/avocado.conf +++ b/etc/avocado/avocado.conf @@ -56,3 +56,6 @@ password = # avocado.core.plugins.htmlresult ImportError No module named pystache # add 'avocado.core.plugins.htmlresult' as an element of the list below. skip_broken_plugin_notification = [] +# Optionally you can specify the priority of loader plugins. Unspecified +# plugins will be sorted accordingly to the plugin priorities. +loader_plugins_priority = ['file'] diff --git a/selftests/all/functional/avocado/basic_tests.py b/selftests/all/functional/avocado/basic_tests.py index fb748b89e459496fc23d47f11dd9daca08d56664..eb1c67189c6544f9bb14fbf2d8f2d49fb64ff3e2 100644 --- a/selftests/all/functional/avocado/basic_tests.py +++ b/selftests/all/functional/avocado/basic_tests.py @@ -190,7 +190,7 @@ class RunnerOperationTest(unittest.TestCase): cmd_line = './scripts/avocado run --sysinfo=off' result = process.run(cmd_line, ignore_status=True) expected_rc = 2 - expected_output = 'too few arguments' + expected_output = 'No tests found for given urls' self.assertEqual(result.exit_status, expected_rc) self.assertIn(expected_output, result.stderr) @@ -200,8 +200,8 @@ class RunnerOperationTest(unittest.TestCase): result = process.run(cmd_line, ignore_status=True) expected_rc = 2 self.assertEqual(result.exit_status, expected_rc) - self.assertIn('File not found', result.stderr) - self.assertNotIn('File not found', result.stdout) + self.assertIn('Unable to discover url', result.stderr) + self.assertNotIn('Unable to discover url', result.stdout) def test_invalid_unique_id(self): cmd_line = './scripts/avocado run --sysinfo=off --force-job-id foobar passtest' @@ -411,7 +411,7 @@ class PluginsTest(unittest.TestCase): self.assertEqual(result.exit_status, expected_rc, "Avocado did not return rc %d:\n%s" % (expected_rc, result)) - self.assertIn("File not found", output) + self.assertIn("Unable to discover url", output) def test_plugin_list(self): os.chdir(basedir) diff --git a/selftests/all/functional/avocado/loader_tests.py b/selftests/all/functional/avocado/loader_tests.py index 2e38c95606cb8aa5e99e6d172667e0aeb4a6500a..e7274857867cb8c27f872d066e30f6edc0105be3 100644 --- a/selftests/all/functional/avocado/loader_tests.py +++ b/selftests/all/functional/avocado/loader_tests.py @@ -63,100 +63,39 @@ true class LoaderTestFunctional(unittest.TestCase): def setUp(self): + os.chdir(basedir) self.tmpdir = tempfile.mkdtemp() + def _test(self, name, content, exp_str, mode=0664): + test_script = script.TemporaryScript(name, content, + 'avocado_loader_test', + mode=mode) + test_script.save() + cmd_line = ('./scripts/avocado list -V %s' % test_script.path) + result = process.run(cmd_line) + self.assertIn('%s: 1' % exp_str, result.stdout) + test_script.remove() + def test_simple(self): - os.chdir(basedir) - simple_test = script.TemporaryScript('simpletest.sh', SIMPLE_TEST, - 'avocado_loader_test') - simple_test.save() - cmd_line = './scripts/avocado run --job-results-dir %s --sysinfo=off %s' % (self.tmpdir, simple_test.path) - process.run(cmd_line) - simple_test.remove() + self._test('simpletest.sh', SIMPLE_TEST, 'SIMPLE', 0775) def test_simple_not_exec(self): - os.chdir(basedir) - simple_test = script.TemporaryScript('simpletest.sh', SIMPLE_TEST, - 'avocado_loader_test', - mode=0664) - simple_test.save() - cmd_line = './scripts/avocado run --job-results-dir %s --sysinfo=off %s' % (self.tmpdir, simple_test.path) - result = process.run(cmd_line, ignore_status=True) - expected_rc = 2 - self.assertEqual(result.exit_status, expected_rc, - "Avocado did not return rc %d:\n%s" % - (expected_rc, result)) - self.assertIn('is not an avocado test', result.stderr) - self.assertNotIn('is not an avocado test', result.stdout) - simple_test.remove() + self._test('simpletest.sh', SIMPLE_TEST, 'NOT_A_TEST') def test_pass(self): - avocado_pass_test = script.TemporaryScript('passtest.py', - AVOCADO_TEST_OK, - 'avocado_loader_test') - avocado_pass_test.save() - cmd_line = './scripts/avocado run --job-results-dir %s --sysinfo=off %s' % (self.tmpdir, avocado_pass_test.path) - result = process.run(cmd_line, ignore_status=True) - expected_rc = 0 - self.assertEqual(result.exit_status, expected_rc, - "Avocado did not return rc %d:\n%s" % - (expected_rc, result)) + self._test('passtest.py', AVOCADO_TEST_OK, 'INSTRUMENTED') def test_buggy_exec(self): - avocado_buggy_test = script.TemporaryScript('buggytest.py', - AVOCADO_TEST_BUGGY, - 'avocado_loader_test') - avocado_buggy_test.save() - cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off %s' % - (self.tmpdir, avocado_buggy_test.path)) - result = process.run(cmd_line, ignore_status=True) - expected_rc = 1 - self.assertEqual(result.exit_status, expected_rc, - "Avocado did not return rc %d:\n%s" % - (expected_rc, result)) + self._test('buggytest.py', AVOCADO_TEST_BUGGY, 'SIMPLE', 0775) def test_buggy_not_exec(self): - avocado_buggy_test = script.TemporaryScript('buggytest.py', - AVOCADO_TEST_BUGGY, - 'avocado_loader_test', - mode=0664) - avocado_buggy_test.save() - cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off %s' % - (self.tmpdir, avocado_buggy_test.path)) - result = process.run(cmd_line, ignore_status=True) - expected_rc = 1 - self.assertEqual(result.exit_status, expected_rc, - "Avocado did not return rc %d:\n%s" % - (expected_rc, result)) - avocado_buggy_test.remove() + self._test('buggytest.py', AVOCADO_TEST_BUGGY, 'BUGGY') def test_load_not_a_test(self): - avocado_not_a_test = script.TemporaryScript('notatest.py', NOT_A_TEST, - 'avocado_loader_test') - avocado_not_a_test.save() - cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off %s' % - (self.tmpdir, avocado_not_a_test.path)) - result = process.run(cmd_line, ignore_status=True) - expected_rc = 1 - self.assertEqual(result.exit_status, expected_rc, - "Avocado did not return rc %d:\n%s" % - (expected_rc, result)) - avocado_not_a_test.remove() + self._test('notatest.py', NOT_A_TEST, 'SIMPLE', 0775) def test_load_not_a_test_not_exec(self): - avocado_not_a_test = script.TemporaryScript('notatest.py', NOT_A_TEST, - 'avocado_loader_test', - mode=0664) - avocado_not_a_test.save() - cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off %s' % - (self.tmpdir, avocado_not_a_test.path)) - result = process.run(cmd_line, ignore_status=True) - expected_rc = 2 - self.assertEqual(result.exit_status, expected_rc, - "Avocado did not return rc %d:\n%s" % - (expected_rc, result)) - self.assertIn('is not an avocado test', result.stderr) - avocado_not_a_test.remove() + self._test('notatest.py', NOT_A_TEST, 'NOT_A_TEST') def tearDown(self): shutil.rmtree(self.tmpdir) diff --git a/selftests/all/unit/avocado/loader_unittest.py b/selftests/all/unit/avocado/loader_unittest.py index 84fdbf5e6ec462f7af2be3fd43d15a7676bfb1a5..014316785409a2578f587b766d534ba46b684f5f 100644 --- a/selftests/all/unit/avocado/loader_unittest.py +++ b/selftests/all/unit/avocado/loader_unittest.py @@ -17,6 +17,7 @@ from avocado.core import exceptions from avocado.core import loader from avocado.utils import script +# We need to access protected members pylint: disable=W0212 AVOCADO_TEST_OK = """#!/usr/bin/python from avocado import Test @@ -84,15 +85,10 @@ class MultipleMethods(Test): """ -class _DebugJob(object): - logdir = tempfile.mkdtemp() - - class LoaderTest(unittest.TestCase): def setUp(self): - self.job = _DebugJob - self.loader = loader.TestLoader(job=self.job) + self.loader = loader.FileLoader({}) self.queue = multiprocessing.Queue() def test_load_simple(self): @@ -100,7 +96,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') simple_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': simple_test.path})[0]) + self.loader.discover(simple_test.path, True)[0]) self.assertTrue(test_class == test.SimpleTest, test_class) tc = test_class(**test_parameters) tc.test() @@ -112,7 +108,7 @@ class LoaderTest(unittest.TestCase): mode=0664) simple_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': simple_test.path})[0]) + self.loader.discover(simple_test.path, True)[0]) self.assertTrue(test_class == test.NotATest, test_class) tc = test_class(**test_parameters) self.assertRaises(exceptions.NotATestError, tc.test) @@ -124,7 +120,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') avocado_pass_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_pass_test.path})[0]) + self.loader.discover(avocado_pass_test.path, True)[0]) self.assertTrue(str(test_class) == "", str(test_class)) self.assertTrue(issubclass(test_class, test.Test)) @@ -138,7 +134,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') avocado_base_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_base_test.path})[0]) + self.loader.discover(avocado_base_test.path, True)[0]) self.assertTrue(str(test_class) == "", str(test_class)) @@ -147,7 +143,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') avocado_inherited_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_inherited_test.path})[0]) + self.loader.discover(avocado_inherited_test.path, True)[0]) self.assertTrue(str(test_class) == "", str(test_class)) avocado_base_test.remove() @@ -159,7 +155,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') avocado_buggy_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_buggy_test.path})[0]) + self.loader.discover(avocado_buggy_test.path, True)[0]) self.assertTrue(test_class == test.SimpleTest, test_class) tc = test_class(**test_parameters) self.assertRaises(exceptions.TestFail, tc.test) @@ -172,7 +168,7 @@ class LoaderTest(unittest.TestCase): mode=0664) avocado_buggy_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_buggy_test.path})[0]) + self.loader.discover(avocado_buggy_test.path, True)[0]) self.assertTrue(test_class == test.BuggyTest, test_class) tc = test_class(**test_parameters) self.assertRaises(ImportError, tc.test) @@ -185,7 +181,7 @@ class LoaderTest(unittest.TestCase): mode=0664) avocado_not_a_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_not_a_test.path})[0]) + self.loader.discover(avocado_not_a_test.path, True)[0]) self.assertTrue(test_class == test.NotATest, test_class) tc = test_class(**test_parameters) self.assertRaises(exceptions.NotATestError, tc.test) @@ -196,7 +192,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') avocado_not_a_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_not_a_test.path})[0]) + self.loader.discover(avocado_not_a_test.path, True)[0]) self.assertTrue(test_class == test.SimpleTest, test_class) tc = test_class(**test_parameters) # The test can't be executed (no shebang), raising an OSError @@ -210,7 +206,7 @@ class LoaderTest(unittest.TestCase): 'avocado_loader_unittest') avocado_simple_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_simple_test.path})[0]) + self.loader.discover(avocado_simple_test.path, True)[0]) self.assertTrue(test_class == test.SimpleTest) tc = test_class(**test_parameters) tc.test() @@ -223,7 +219,7 @@ class LoaderTest(unittest.TestCase): mode=0664) avocado_simple_test.save() test_class, test_parameters = ( - self.loader.discover_tests(params={'id': avocado_simple_test.path})[0]) + self.loader.discover(avocado_simple_test.path, True)[0]) self.assertTrue(test_class == test.NotATest) tc = test_class(**test_parameters) self.assertRaises(exceptions.NotATestError, tc.test) @@ -235,13 +231,10 @@ class LoaderTest(unittest.TestCase): 'avocado_multiple_tests_unittest', mode=0664) avocado_multiple_tests.save() - suite = self.loader.discover_tests(params={'id': avocado_multiple_tests.path}) + suite = self.loader.discover(avocado_multiple_tests.path, True) self.assertEqual(len(suite), 2) avocado_multiple_tests.remove() - def tearDown(self): - if os.path.isdir(self.job.logdir): - shutil.rmtree(self.job.logdir) if __name__ == '__main__': unittest.main()