def get_decorated_bugs(): # pragma: no cover """Using Robozilla parser, get all IDs from skip_if_bug_open decorator and return the dictionary containing fetched data. Important information is stored on `bug_data` key:: bugs[BUG_ID]['bug_data']['resolution|status|flags|whiteboard'] """ if not settings.configured: settings.configure() # look for settings bugzilla credentials # any way Parser bugzilla reader will check for exported environment names # BUGZILLA_USER_NAME and BUGZILLA_USER_PASSWORD if no credentials was # supplied bz_reader_options = {} bz_credentials = {} if setting_is_set('bugzilla'): bz_credentials = settings.bugzilla.get_credentials() bz_reader_options['credentials'] = bz_credentials parser = Parser(BASE_PATH, filters=[BZDecorator], reader_options=bz_reader_options) bugs = parser.parse() return bugs
def get_deselect_bug_ids(bugs=None, log=None): # pragma: no cover """returns the IDs of bugs to be deselected from test collection""" if not settings.configured: settings.configure() if settings.bugzilla.wontfix_lookup is not True: return [] if log is None: log = log_debug log('Fetching WONTFIX BZs on Bugzilla API...') bugs = bugs or get_decorated_bugs() wontfixes = [] for bug_id, data in bugs.items(): bug_data = data.get('bug_data') # when not authenticated, private bugs will have no bug data if bug_data: if bug_data['resolution'] in ('WONTFIX', 'CANTFIX', 'DEFERRED'): wontfixes.append(bug_id) else: log('bug data for bug id "{}" was not retrieved,' ' please review bugzilla credentials'.format(bug_id)) return wontfixes
def setUpClass(cls): # noqa super(TestCase, cls).setUpClass() if not settings.configured: settings.configure() cls.logger = logging.getLogger('robottelo') # NOTE: longMessage defaults to True in Python 3.1 and above cls.longMessage = True
def pytest_collection_modifyitems(items, config): """ called after collection has been performed, may filter or re-order the items in-place. Deselecting all tests skipped due to WONTFIX BZ. """ if not settings.configured: settings.configure() if settings.bugzilla.wontfix_lookup is not True: # if lookup is disable return all collection unmodified log('Deselect of WONTFIX BZs is disabled in settings') return items deselected_items = [] wontfix_ids = pytest.bugzilla.wontfix_ids decorated_functions = group_by_key(pytest.bugzilla.decorated_functions) log("Found WONTFIX in decorated tests %s" % wontfix_ids) log("Collected %s test cases" % len(items)) for item in items: name = get_func_name(item.function) bug_ids = decorated_functions.get(name) if bug_ids: for bug_id in bug_ids: if bug_id in wontfix_ids: deselected_items.append(item) log("Deselected test %s due to WONTFIX" % name) break config.hook.pytest_deselected(items=deselected_items) items[:] = [item for item in items if item not in deselected_items]
def filtered_datapoint(func): """Overrides the data creator functions in this class to return 1 value and transforms data dictionary to pytest's parametrize acceptable format for new style generators. If run_one_datapoint=false, return the entire data set. (default: False) If run_one_datapoint=true, return a random data. """ if not settings.configured: settings.configure() @wraps(func) def func_wrapper(*args, **kwargs): """Perform smoke test attribute check""" dataset = func(*args, **kwargs) if isinstance(dataset, dict): # New UI tests are written using pytest, update dict to support pytest's parametrize if ('ui' in args or kwargs.get('interface') == 'ui' and settings.ui.webdriver == 'chrome'): # Chromedriver only supports BMP chars utf8 = dataset.pop('utf8', None) if utf8: dataset['utf8'] = gen_utf8(len(utf8), smp=False) if settings.robottelo.run_one_datapoint: key = random.choice(list(dataset.keys())) dataset = {key: dataset[key]} else: # Otherwise use list for backwards compatibility dataset = list(dataset) if settings.robottelo.run_one_datapoint: dataset = [random.choice(dataset)] return dataset return func_wrapper
def pytest_collection_modifyitems(items, config): """ called after collection has been performed, may filter or re-order the items in-place. Deselecting all tests skipped due to WONTFIX BZ. """ if not settings.configured: settings.configure() if settings.bugzilla.wontfix_lookup is not True: # if lookup is disable return all collection unmodified log('BZ deselect is disabled in settings') return items deselected_items = [] decorated_functions = group_by_key(pytest.bugzilla.decorated_functions) log("Collected %s test cases" % len(items)) for item in items: name = get_func_name(item.function, test_item=item) bug_ids = list(decorated_functions.get(name, [])) bug_ids.extend(_extract_setup_class_ids(item)) if any(bug_id in pytest.bugzilla.removal_ids for bug_id in bug_ids): deselected_items.append(item) log("Deselected test %s" % name) config.hook.pytest_deselected(items=deselected_items) items[:] = [item for item in items if item not in deselected_items]
def __init__(self): """Configure the settings and initiate report portal properties""" if not settings.configured: settings.configure() self.rp_url = settings.report_portal.rp_url self.rp_project = settings.report_portal.rp_project self.rp_api_key = settings.report_portal.rp_key
def setUpClass(cls): # noqa super().setUpClass() if not settings.configured: settings.configure() cls.logger = logging.getLogger('robottelo') cls.logger.info(f'Started setUpClass: {cls.__module__}/{cls.__name__}') # NOTE: longMessage defaults to True in Python 3.1 and above cls.longMessage = True cls.foreman_user = settings.server.admin_username cls.foreman_password = settings.server.admin_password
def setting_is_set(option): """Return either ``True`` or ``False`` if a Robottelo section setting is set or not respectively. """ if not settings.configured: settings.configure() # Example: `settings.clients` if getattr(settings, option).validate(): return False return True
def browse(entity, browser, host): """Opens a page in defined browser for interaction:\n All parameters defaults to what is in robottelo.properties file.\n example: $ manage ui browse activationkey\n Opens a browser in ActivationKey scope and gives you interactive shell to play with the page\n """ settings.configure() if host: settings.server.hostname = host current_browser = _get_browser(browser) with Session(current_browser) as session: # noqa ui_crud = _import_ui_crud() # Make all UI CRUD entities available in a dict # each entity initialized with current_browser instance ui_entities = {name.lower(): crud_class(current_browser) for name, crud_class in ui_crud.items()} if entity: # if entity name specified navigate to the page # example: manage ui browse user ui_entities[entity.lower()].navigate_to_entity() # gets all functions from ui.factory module which starts with 'make_' ui_factory_members = getmembers(ui_factory, lambda i: callable(i) and i.__name__.startswith("make")) # Usually we need to pass the `session` as 1st argument # e.g `make_user(session, username='******')` # using `partial` we make session the default 1st argument # it allows the use as: `make_user(username='******') # and `session` is implicit there. ui_factory_functions = {name: partial(function, session=session) for name, function in ui_factory_members} # now we "inject" the factories and entities under `session.ui` session.ui = Storage(ui_entities, ui_factory_functions) # The same for nailgun.entities.* under `session.api` session.api = Storage(dict(getmembers(entities))) def close(): """Hook to close the session and browser atexit it also flushes the session history content to the specified file """ session.close() # FIXME: if --out=/path/to/file should save session history # see ipython.readthedocs.io/en/stable/config/options/terminal.html extra_vars = { "session": session, "browser": current_browser, "current_browser": current_browser, "host": settings.server.hostname, "api_factory": entities, "ui_factory": ui_factory, } create_shell("ipython", extra_vars=extra_vars, exit_hooks=[close])
def setUpClass(cls): # noqa super(TestCase, cls).setUpClass() if not settings.configured: settings.configure() cls.logger = logging.getLogger('robottelo') cls.logger.info('Started setUpClass: {0}/{1}'.format( cls.__module__, cls.__name__)) # NOTE: longMessage defaults to True in Python 3.1 and above cls.longMessage = True cls.foreman_user = settings.server.admin_username cls.foreman_password = settings.server.admin_password
def config_picker(): """Return a dict with config for robozilla.decorators it is implemented because `settings` object can't be configured at module level, so we make it lazy by this function""" if not settings.configured: settings.configure() config = {'upstream': settings.upstream} if setting_is_set('bugzilla'): config['bz_credentials'] = settings.bugzilla.get_credentials() config['wontfix_lookup'] = settings.bugzilla.wontfix_lookup return config
def wrapper(*args, **kwargs): if not settings.configured: settings.configure() missing = [] for option in options: # Example: `settings.clients` if getattr(settings, option).validate(): # List of all sections that are not fully configured missing.append(option) if not missing: return func(*args, **kwargs) raise unittest2.SkipTest( 'Missing configuration for: {0}.'.format(', '.join(missing)))
def wrapper(*args, **kwargs): if not settings.configured: settings.configure() missing = [] for option in options: # Example: `settings.clients` if getattr(settings, option).validate(): # List of all sections that are not fully configured missing.append(option) if not missing: return func(*args, **kwargs) raise unittest2.SkipTest('Missing configuration for: {0}.'.format( ', '.join(missing)))
def setUpClass(cls): # noqa super(TestCase, cls).setUpClass() if not settings.configured: settings.configure() cls.logger = logging.getLogger('robottelo') cls.logger.debug('Started setUpClass: {0}/{1}'.format( cls.__module__, cls.__name__)) # NOTE: longMessage defaults to True in Python 3.1 and above cls.longMessage = True cls.foreman_user = settings.server.admin_username cls.foreman_password = settings.server.admin_password if settings.cleanup: cls.cleaner = EntitiesCleaner(entities.Organization, entities.Host, entities.HostGroup)
def __init__(self, rp_url, rp_api_key, rp_project): """Configure the settings and initiate report portal properties""" if not settings.configured: settings.configure() self.rp_url = rp_url self.rp_project = rp_project self.rp_api_key = rp_api_key self.rp_project_settings = None # fetch the project settings settings_req = requests.get(url=f'{self.api_url}/settings', headers=self.headers, verify=False) settings_req.raise_for_status() self.rp_project_settings = settings_req.json()
def setUpClass(cls): # noqa super(TestCase, cls).setUpClass() if not settings.configured: settings.configure() cls.logger = logging.getLogger('robottelo') cls.logger.debug('Started setUpClass: {0}/{1}'.format( cls.__module__, cls.__name__)) # NOTE: longMessage defaults to True in Python 3.1 and above cls.longMessage = True cls.foreman_user = settings.server.admin_username cls.foreman_password = settings.server.admin_password if settings.cleanup: cls.cleaner = EntitiesCleaner( entities.Organization, entities.Host, entities.HostGroup )
def module_roles(self): """ Initializes class attribute ``dct_roles`` with several random roles saved on sat. roles is a dict so keys are role's id respective value is the role itself """ settings.configure() include_list = [gen_string("alphanumeric", 100)] def roles_helper(): """Generator funcion which creates several Roles to be used on tests """ for role_name in valid_usernames_list() + include_list: yield make_role({'name': role_name}) stubbed_roles = {role['id']: role for role in roles_helper()} yield stubbed_roles
def wrapper(*args, **kwargs): """Wrapper that will skip the test if one of defined versions match host's version. """ def log_version_info(msg, template): LOGGER.debug(template, func.__name__, func.__module__, msg) if not settings.configured: settings.configure() host_version = get_host_os_version() if any(host_version.startswith(version) for version in versions): skip_msg = f'host {host_version} in ignored versions {versions}' skip_template = 'Skipping test %s in module %s due to %s' log_version_info(skip_msg, skip_template) raise unittest2.SkipTest(skip_msg) return func(*args, **kwargs)
def get_deselect_bug_ids(bugs=None, log=None, lookup=None): # pragma: no cover """returns the IDs of bugs to be deselected from test collection""" if not settings.configured: settings.configure() lookup = lookup or settings.bugzilla.wontfix_lookup if lookup is not True: return [] if log is None: log = log_debug log('Fetching BZs to deselect...') bugs = bugs or get_decorated_bugs() resolution_list = [] flag_list = [] backlog_list = [] for bug_id, data in bugs.items(): bug_data = data.get('bug_data') # when not authenticated, private bugs will have no bug data if bug_data: if 'sat-backlog' in bug_data.get('flags', {}): backlog_list.append(bug_id) if bug_data['resolution'] in ('WONTFIX', 'CANTFIX', 'DEFERRED'): resolution_list.append(bug_id) if not any([flag in bug_data.get('flags', {}) for flag in VFLAGS]): # If the BZ is not flagged with any of `sat-x.x.x` flag_list.append(bug_id) else: log('bug data for bug id "{}" was not retrieved,' ' please review bugzilla credentials'.format(bug_id)) if resolution_list: log('Deselected tests reason: BZ resolution %s' % resolution_list) if flag_list: log('Deselected tests reason: missing version flag %s' % flag_list) if backlog_list: log('Deselected tests reason: sat-backlog %s' % backlog_list) return set(resolution_list + flag_list + backlog_list)
def setUpClass(cls): """ Initializes class attribute ``dct_roles`` with several random roles saved on sat. roles is a dict so keys are role's id respective value is the role itself """ super(UserWithCleanUpTestCase, cls).setUpClass() settings.configure() include_list = [gen_string("alphanumeric", 100)] def roles_helper(): """Generator funcion which creates several Roles to be used on tests """ for role_name in valid_usernames_list() + include_list: yield make_role({'name': role_name}) cls.stubbed_roles = {role['id']: role for role in roles_helper()} cls.all_roles = {role['id']: role for role in Role.list()}
def wrapper(*args, **kwargs): """Wrapper that will skip the test if one of defined versions match host's version. """ def log_version_info(msg, template): LOGGER.debug(template, func.__name__, func.__module__, msg) if not settings.configured: settings.configure() host_version = get_host_os_version() if any(host_version.startswith(version) for version in versions): skip_msg = 'host {0} in ignored versions {1}'.format( host_version, versions ) skip_template = 'Skipping test %s in module %s due to %s' log_version_info(skip_msg, skip_template) raise unittest2.SkipTest(skip_msg) return func(*args, **kwargs)
def align_xdist_satellites(worker_id): """Set a different Satellite per worker when available in robottelo's config""" settings.configure() settings.server.hostname = settings.server.get_hostname(worker_id) settings.configure_nailgun() settings.configure_airgun()
"""Only External Repos url specific constants module""" from robottelo.config import settings if not settings.configured: settings.configure() REPOS_URL = settings.repos_hosting_url CUSTOM_FILE_REPO = 'https://fixtures.pulpproject.org/file/' CUSTOM_KICKSTART_REPO = 'http://ftp.cvut.cz/centos/8/BaseOS/x86_64/kickstart/' CUSTOM_RPM_REPO = 'https://fixtures.pulpproject.org/rpm-signed/' CUSTOM_RPM_SHA_512 = 'https://fixtures.pulpproject.org/rpm-with-sha-512/' CUSTOM_MODULE_STREAM_REPO_1 = f'{REPOS_URL}/module_stream1' CUSTOM_MODULE_STREAM_REPO_2 = f'{REPOS_URL}/module_stream2' CUSTOM_SWID_TAG_REPO = f'{REPOS_URL}/swid_zoo' FAKE_0_YUM_REPO = f'{REPOS_URL}/fake_yum0' FAKE_1_YUM_REPO = f'{REPOS_URL}/fake_yum1' FAKE_2_YUM_REPO = f'{REPOS_URL}/fake_yum2' FAKE_3_YUM_REPO = f'{REPOS_URL}/fake_yum3' FAKE_4_YUM_REPO = f'{REPOS_URL}/fake_yum4' FAKE_5_YUM_REPO = 'http://{0}:{1}@rplevka.fedorapeople.org/fakerepo01/' FAKE_6_YUM_REPO = f'{REPOS_URL}/needed_errata' FAKE_7_YUM_REPO = f'{REPOS_URL}/pulp/demo_repos/large_errata/zoo/' FAKE_8_YUM_REPO = f'{REPOS_URL}/lots_files' FAKE_9_YUM_REPO = f'{REPOS_URL}/multiple_errata' FAKE_10_YUM_REPO = f'{REPOS_URL}/modules_rpms' FAKE_11_YUM_REPO = f'{REPOS_URL}/rpm_deps' FAKE_YUM_DRPM_REPO = 'https://fixtures.pulpproject.org/drpm-signed/' FAKE_YUM_SRPM_REPO = 'https://fixtures.pulpproject.org/srpm-signed/' FAKE_YUM_SRPM_DUPLICATE_REPO = 'https://fixtures.pulpproject.org/srpm-duplicate/' FAKE_YUM_MIXED_REPO = f'{REPOS_URL}/yum_mixed'
def configured_settings(): if not settings.configured: settings.configure() return settings
def browse(entity, browser, host): """Opens a page in defined browser for interaction:\n All parameters defaults to what is in robottelo.properties file.\n example: $ manage ui browse activationkey\n Opens a browser in ActivationKey scope and gives you interactive shell to play with the page\n """ settings.configure() if host: settings.server.hostname = host current_browser = _get_browser(browser) with Session(current_browser) as session: # noqa ui_crud = _import_ui_crud() # Make all UI CRUD entities available in a dict # each entity initialized with current_browser instance ui_entities = { name.lower(): crud_class(current_browser) for name, crud_class in ui_crud.items() } if entity: # if entity name specified navigate to the page # example: manage ui browse user ui_entities[entity.lower()].navigate_to_entity() # gets all functions from ui.factory module which starts with 'make_' ui_factory_members = getmembers( ui_factory, lambda i: callable(i) and i.__name__.startswith('make')) # Usually we need to pass the `session` as 1st argument # e.g `make_user(session, username='******')` # using `partial` we make session the default 1st argument # it allows the use as: `make_user(username='******') # and `session` is implicit there. ui_factory_functions = { name: partial(function, session=session) for name, function in ui_factory_members } # now we "inject" the factories and entities under `session.ui` session.ui = Storage(ui_entities, ui_factory_functions) # The same for nailgun.entities.* under `session.api` session.api = Storage(dict(getmembers(entities))) def close(): """Hook to close the session and browser atexit it also flushes the session history content to the specified file """ session.close() # FIXME: if --out=/path/to/file should save session history # see ipython.readthedocs.io/en/stable/config/options/terminal.html extra_vars = { 'session': session, 'browser': current_browser, 'current_browser': current_browser, 'host': settings.server.hostname, 'api_factory': entities, 'ui_factory': ui_factory } create_shell('ipython', extra_vars=extra_vars, exit_hooks=[close])
import json from robottelo import ssh from robottelo.cli import hammer from robottelo.config import settings def generate_command_tree(command): """Recursively walk trhough the hammer commands and subcommands and fetch their help. Return a dictionary with the contents. """ output = ssh.command('{0} --help'.format(command)).stdout contents = hammer.parse_help(output) if len(contents['subcommands']) > 0: for subcommand in contents['subcommands']: subcommand.update(generate_command_tree( '{0} {1}'.format(command, subcommand['name']) )) return contents settings.configure() # Generate the json file in the working directory with open('hammer_commands.json', 'w') as f: f.write(json.dumps( generate_command_tree('hammer'), indent=2, sort_keys=True ))
def generate_issue_collection(items, config): # pragma: no cover """Generates a dictionary with the usage of Issue blockers Arguments: items {list} - List of pytest test case objects. config {dict} - Pytest config object. Returns: [List of dicts] - Dicts indexed by "<handler>:<issue>" Example of return data:: { "XX:1625783" { "data": { # data taken from REST api, "status": ..., "resolution": ..., ... # Calculated data "is_open": bool, "is_deselected": bool, "clones": [list], "dupe_data": {dict} }, "used_in" [ { "filepath": "tests/foreman/ui/test_sync.py", "lineno": 124, "testcase": "test_positive_sync_custom_ostree_repo", "component": "Repositories", "usage": "skip_if_open" }, ... ] }, ... } """ settings.configure() valid_markers = ["skip_if_open", "skip", "deselect"] collected_data = defaultdict(lambda: {"data": {}, "used_in": []}) bz_cache = config.getvalue('bz_cache') # use existing json cache? cached_data = None if bz_cache: with open(bz_cache) as bz_cache_file: cached_data = { k: _handle_version(k, v) for k, v in json.load(bz_cache_file).items() } deselect_data = {} # a local cache for deselected tests IS_OPEN = re.compile( # To match `if is_open('BZ:123456'):` r"\s*if\sis_open\(\S(?P<src>\D{2})\s*:\s*(?P<num>\d*)\S\)\d*") NOT_IS_OPEN = re.compile( # To match `if not is_open('BZ:123456'):` r"\s*if\snot\sis_open\(\S(?P<src>\D{2})\s*:\s*(?P<num>\d*)\S\)\d*") COMPONENT = re.compile( # To match :CaseComponent: FooBar r"\s*:CaseComponent:\s*(?P<component>\S*)", re.IGNORECASE, ) IMPORTANCE = re.compile( # To match :CaseImportance: Critical r"\s*:CaseImportance:\s*(?P<importance>\S*)", re.IGNORECASE, ) BZ = re.compile( # To match :BZ: 123456, 456789 r"\s*:BZ:\s*(?P<bz>.*\S*)", re.IGNORECASE, ) test_modules = set() # --- Build the issue marked usage collection --- for item in items: component = None bzs = None importance = None # register test module as processed test_modules.add(item.module) # Find matches from docstrings top-down from: module, class, function. mod_cls_fun = (item.module, getattr(item, 'cls', None), item.function) for docstring in map(inspect.getdoc, mod_cls_fun): if not docstring: continue component_matches = COMPONENT.findall(docstring) if component_matches: component = component_matches[-1] bz_matches = BZ.findall(docstring) if bz_matches: bzs = bz_matches[-1] importance_matches = IMPORTANCE.findall(docstring) if importance_matches: importance = importance_matches[-1] filepath, lineno, testcase = item.location component_mark = slugify_component(component, False) for marker in item.iter_markers(): if marker.name in valid_markers: issue = marker.kwargs.get('reason') or marker.args[0] issue_key = issue.strip() collected_data[issue_key]['used_in'].append({ 'filepath': filepath, 'lineno': lineno, 'testcase': testcase, 'component': component, 'importance': importance, 'component_mark': component_mark, 'usage': marker.name, }) # Store issue key to lookup in the deselection process deselect_data[item.location] = issue_key # Add issue as a marker to enable filtering e.g: "-m BZ_123456" item.add_marker( getattr(pytest.mark, issue_key.replace(':', '_'))) # Then take the workarounds using `is_open` helper. source = inspect.getsource(item.function) if 'is_open(' in source: kwargs = { 'filepath': filepath, 'lineno': lineno, 'testcase': testcase, 'component': component, 'importance': importance, 'component_mark': component_mark, } _add_workaround(collected_data, IS_OPEN.findall(source), 'is_open', **kwargs) _add_workaround(collected_data, NOT_IS_OPEN.findall(source), 'not is_open', **kwargs) # Add component as a marker to anable filtering e.g: "-m contentviews" item.add_marker(getattr(pytest.mark, component_mark)) # Add BZs from tokens as a marker to enable filter e.g: "-m BZ_123456" if bzs: for bz in bzs.split(','): item.add_marker(getattr(pytest.mark, f'BZ_{bz.strip()}')) # Add importance as a token if importance: item.add_marker(getattr(pytest.mark, importance.lower().strip())) # Take uses of `is_open` from outside of test cases e.g: SetUp methods for test_module in test_modules: module_source = inspect.getsource(test_module) component_matches = COMPONENT.findall(module_source) module_component = None if component_matches: module_component = component_matches[0] if 'is_open(' in module_source: kwargs = { 'filepath': test_module.__file__, 'lineno': 1, 'testcase': test_module.__name__, 'component': module_component, } def validation(data, issue, usage, **kwargs): return issue not in data _add_workaround( collected_data, IS_OPEN.findall(module_source), 'is_open', validation=validation, **kwargs, ) _add_workaround( collected_data, NOT_IS_OPEN.findall(module_source), 'not is_open', validation=validation, **kwargs, ) # --- Collect BUGZILLA data --- bugzilla.collect_data_bz(collected_data, cached_data) # --- add deselect markers dinamically --- for item in items: issue = deselect_data.get(item.location) if issue and _should_deselect(issue, collected_data[issue]['data']): collected_data[issue]['data']['is_deselected'] = True item.add_marker(pytest.mark.deselect(reason=issue)) # --- if not using pre-existing cache write it --- if not bz_cache: collected_data['_meta'] = { "version": settings.server.version, "hostname": settings.server.hostname, "created": datetime.now().isoformat(), "pytest": { "args": config.args, "pwd": str(config.invocation_dir) }, } with open('bz_cache.json', 'w') as collect_file: json.dump(collected_data, collect_file, indent=4, cls=VersionEncoder) LOGGER.debug( f"Generated file {bz_cache or 'bz_cache.json'} with BZ collect data" ) return collected_data
def generate_issue_collection(items, config): # pragma: no cover """Generates a dictionary with the usage of Issue blockers For use in pytest_collection_modifyitems hook Arguments: items {list} - List of pytest test case objects. config {dict} - Pytest config object. Returns: [List of dicts] - Dicts indexed by "<handler>:<issue>" Example of return data:: { "XX:1625783" { "data": { # data taken from REST api, "status": ..., "resolution": ..., ... # Calculated data "is_open": bool, "is_deselected": bool, "clones": [list], "dupe_data": {dict} }, "used_in" [ { "filepath": "tests/foreman/ui/test_sync.py", "lineno": 124, "testcase": "test_positive_sync_custom_ostree_repo", "component": "Repositories", "usage": "skip_if_open" }, ... ] }, ... } """ if not settings.configured: settings.configure() valid_markers = ["skip_if_open", "skip", "deselect"] collected_data = defaultdict(lambda: {"data": {}, "used_in": []}) use_bz_cache = config.getoption('bz_cache', None) # use existing json cache? cached_data = None if use_bz_cache: try: with open(DEFAULT_BZ_CACHE_FILE) as bz_cache_file: logger.info(f'Using BZ cache file for issue collection: {DEFAULT_BZ_CACHE_FILE}') cached_data = { k: search_version_key(k, v) for k, v in json.load(bz_cache_file).items() } except FileNotFoundError: # no bz cache file exists logger.warning( f'--bz-cache option used, cache file [{DEFAULT_BZ_CACHE_FILE}] not found' ) deselect_data = {} # a local cache for deselected tests test_modules = set() # --- Build the issue marked usage collection --- for item in items: if item.nodeid.startswith('tests/robottelo/'): # Unit test, no bz processing # TODO: We very likely don't need to include component and importance in collected_data # Removing these would unravel issue_handlers dependence on testimony # Allowing for issue_handler use in unit tests continue bz_marks_to_add = [] # register test module as processed test_modules.add(item.module) # Find matches from docstrings top-down from: module, class, function. mod_cls_fun = (item.module, getattr(item, 'cls', None), item.function) for docstring in [d for d in map(inspect.getdoc, mod_cls_fun) if d is not None]: bz_matches = BZ.findall(docstring) if bz_matches: bz_marks_to_add.extend(b.strip() for b in bz_matches[-1].split(',')) filepath, lineno, testcase = item.location # Component and importance marks are determined by testimony tokens # Testimony.yaml as of writing has both as required, so any component_mark = item.get_closest_marker('component').args[0] component_slug = slugify_component(component_mark, False) importance_mark = item.get_closest_marker('importance').args[0] for marker in item.iter_markers(): if marker.name in valid_markers: issue = marker.kwargs.get('reason') or marker.args[0] issue_key = issue.strip() collected_data[issue_key]['used_in'].append( { 'filepath': filepath, 'lineno': lineno, 'testcase': testcase, 'component': component_mark, 'importance': importance_mark, 'component_mark': component_slug, 'usage': marker.name, } ) # Store issue key to lookup in the deselection process deselect_data[item.location] = issue_key # Add issue as a marker to enable filtering e.g: "--BZ 123456" bz_marks_to_add.append(issue_key.split(':')[-1]) # Then take the workarounds using `is_open` helper. source = inspect.getsource(item.function) if 'is_open(' in source: kwargs = { 'filepath': filepath, 'lineno': lineno, 'testcase': testcase, 'component': component_mark, 'importance': importance_mark, 'component_mark': component_slug, } add_workaround(collected_data, IS_OPEN.findall(source), 'is_open', **kwargs) add_workaround(collected_data, NOT_IS_OPEN.findall(source), 'not is_open', **kwargs) # Add BZs from tokens as a marker to enable filter e.g: "--BZ 123456" if bz_marks_to_add: item.add_marker(pytest.mark.BZ(*bz_marks_to_add)) # Take uses of `is_open` from outside of test cases e.g: SetUp methods for test_module in test_modules: module_source = inspect.getsource(test_module) component_matches = COMPONENT.findall(module_source) module_component = None if component_matches: module_component = component_matches[0] if 'is_open(' in module_source: kwargs = { 'filepath': test_module.__file__, 'lineno': 1, 'testcase': test_module.__name__, 'component': module_component, } def validation(data, issue, usage, **kwargs): return issue not in data add_workaround( collected_data, IS_OPEN.findall(module_source), 'is_open', validation=validation, **kwargs, ) add_workaround( collected_data, NOT_IS_OPEN.findall(module_source), 'not is_open', validation=validation, **kwargs, ) # --- Collect BUGZILLA data --- bugzilla.collect_data_bz(collected_data, cached_data) # --- add deselect markers dynamically --- for item in items: issue = deselect_data.get(item.location) if issue and should_deselect(issue, collected_data[issue]['data']): collected_data[issue]['data']['is_deselected'] = True item.add_marker(pytest.mark.deselect(reason=issue)) # --- if no cache file existed write a new cache file --- if cached_data is None and use_bz_cache: collected_data['_meta'] = { "version": settings.server.version, "hostname": settings.server.hostname, "created": datetime.now().isoformat(), "pytest": {"args": config.args, "pwd": str(config.invocation_dir)}, } # bz_cache_filename could be None from the option not being passed, write the file anyway with open(DEFAULT_BZ_CACHE_FILE, 'w') as collect_file: json.dump(collected_data, collect_file, indent=4, cls=VersionEncoder) logger.info(f"Generated BZ cache file {DEFAULT_BZ_CACHE_FILE}") return collected_data
def init_settings(): """Explicitly init the robottelo conf settings""" if not settings.configured: settings.configure()