def import_nitrate(): """ Conditionally import the nitrate module """ # Need to import nitrate only when really needed. Otherwise we get # traceback when nitrate not installed or config file not available. # And we want to keep the core tmt package with minimal dependencies. try: global nitrate, DEFAULT_PRODUCT import nitrate DEFAULT_PRODUCT = nitrate.Product(name='RHEL Tests') except ImportError: raise ConvertError("Install nitrate to export tests there.") except nitrate.NitrateError as error: raise ConvertError(error)
def plugin_install(self, name): """ Install a vagrant plugin if it's not installed yet. """ plugin = f'{self.executable}-{name}' command = ['plugin', 'install'] try: # is it already present? return self.run( f"bash -c \"{self.executable} {command[0]} list | grep '^{plugin}'\"" ) except GeneralError: pass try: # try to install it return self.run_vagrant(command[0], command[1], plugin) except GeneralError as error: # Let's work-around the error handling limitation for now # by getting the output manually command = ' '.join([self.executable] + command + [plugin]) out, err = self.run(f"bash -c \"{command}; :\"") if re.search(r"Conflicting dependency chains:", err) is None: raise error raise ConvertError( 'Dependency conflict detected.\n' 'Please install vagrant plugins from one source only(hint: `dnf remove vagrant-libvirt`).' )
def import_bugzilla(): """ Conditionally import the bugzilla module """ try: global bugzilla import bugzilla except ImportError: raise ConvertError( "Install 'tmt-test-convert' to link test to the bugzilla.")
def write(path, data): """ Write gathered metadata in the fmf format """ # Make sure there is a metadata tree initialized try: tree = fmf.Tree(path) except fmf.utils.RootError: raise ConvertError("Initialize metadata tree using 'fmf init'.") # Store all metadata into a new main.fmf file fmf_path = os.path.join(path, 'main.fmf') try: with open(fmf_path, 'w', encoding='utf-8') as fmf_file: yaml.safe_dump( data, fmf_file, encoding='utf-8', allow_unicode=True, indent=4, default_flow_style=False) except IOError: raise ConvertError("Unable to write '{0}'".format(fmf_path)) echo(style( "Metadata successfully stored into '{0}'.".format(fmf_path), fg='red'))
def bz_set_coverage(bz_instance, bug_ids, case_id): """ Set coverage in Bugzilla """ overall_pass = True no_email = 1 # Do not send emails about the change get_bz_dict = { 'ids': bug_ids, 'include_fields': ['id', 'external_bugs', 'flags'] } bugs_data = bz_instance._proxy.Bug.get(get_bz_dict) for bug in bugs_data['bugs']: # Process flag (might fail for some types) bug_id = bug['id'] if 'qe_test_coverage+' not in set( [x['name'] + x['status'] for x in bug['flags']]): try: bz_instance._proxy.Flag.update({ 'ids': [bug_id], 'nomail': no_email, 'updates': [{ 'name': 'qe_test_coverage', 'status': '+' }] }) except xmlrpc.client.Fault as err: log.debug(f"Update flag failed: {err}") echo( style( f"Failed to set qe_test_coverage+ flag for BZ#{bug_id}", fg='red')) # Process external tracker - should succeed current = set([ int(b['ext_bz_bug_id']) for b in bug['external_bugs'] if b['ext_bz_id'] == EXTERNAL_TRACKER_ID ]) if case_id not in current: query = { 'bug_ids': [bug_id], 'nomail': no_email, 'external_bugs': [{ 'ext_type_id': EXTERNAL_TRACKER_ID, 'ext_bz_bug_id': case_id, 'ext_description': '', }] } try: bz_instance._proxy.ExternalBugs.add_external_bug(query) except Exception as err: log.debug(f"Link case failed: {err}") echo(style(f"Failed to link to BZ#{bug_id}", fg='red')) overall_pass = False if not overall_pass: raise ConvertError("Failed to link the case to bugs.")
def html_to_markdown(html): """ Convert html to markdown """ try: import html2text md_handler = html2text.HTML2Text() except ImportError: raise ConvertError("Install tmt-test-convert to import tests.") if html is None: markdown = "" else: markdown = md_handler.handle(html).strip() return markdown
def run_vagrant_success(self, *args): """ Run vagrant command and raise an error if it fails see run_vagrant() for args """ self.debug() cps = self.run_vagrant(*args) if cps.returncode != 0: raise ConvertError(f'Failed to run vagrant:{self.eol}\ command: {self.hr(args)}{self.eol}\ stdout: {self.hr(cps.stdout)}{self.eol}\ stderr: {self.hr(cps.stderr)}{self.eol}\ ') return cps
def write(path, data): """ Write gathered metadata in the fmf format """ # Put keys into a reasonable order extra_keys = ['extra-summary', 'extra-task', 'extra-nitrate'] sorted_data = dict() for key in tmt.base.Test._keys + extra_keys: try: sorted_data[key] = data[key] except KeyError: pass # Store metadata into a fmf file try: with open(path, 'w', encoding='utf-8') as fmf_file: fmf_file.write(tmt.utils.dict_to_yaml(sorted_data)) except IOError: raise ConvertError("Unable to write '{0}'".format(path)) echo(style( "Metadata successfully stored into '{0}'.".format(path), fg='magenta'))
def adjust_runtest(path): """ Adjust runtest.sh content and permission """ # Nothing to do if there is no runtest.sh if not os.path.exists(path): return # Remove sourcing of rhts-environment.sh and update beakerlib path rhts_line = '. /usr/bin/rhts-environment.sh' old_beakerlib_path1 = '. /usr/lib/beakerlib/beakerlib.sh' old_beakerlib_path2 = '. /usr/share/rhts-library/rhtslib.sh' new_beakerlib_path = '. /usr/share/beakerlib/beakerlib.sh || exit 1\n' try: with open(path, 'r+') as runtest: lines = runtest.readlines() runtest.seek(0) for line in lines: if rhts_line in line: echo( style( "Removing sourcing of 'rhts-environment.sh' " "from 'runtest.sh'.", fg='magenta')) elif (old_beakerlib_path1 in line or old_beakerlib_path2 in line): runtest.write(new_beakerlib_path) echo( style( "Replacing old beakerlib path with new one " "in 'runtest.sh'.", fg='magenta')) else: runtest.write(line) runtest.truncate() except IOError: raise ConvertError("Unable to read/write '{0}'.".format(path)) # Make sure the script has correct execute permissions try: os.chmod(path, 0o755) except IOError: raise tmt.convert.ConvertError( "Could not make '{0}' executable.".format(path))
def write_markdown(path, content): """ Write gathered metadata in the markdown format """ to_print = "" if content['setup']: to_print += "# Setup\n" + content['setup'] + '\n\n' if content['action'] or content['expected']: to_print += "# Test\n\n" if content['action']: to_print += "## Step\n" + content['action'] + '\n\n' if content['expected']: to_print += "## Expect\n" + content['expected'] + '\n\n' if content['cleanup']: to_print += "# Cleanup\n" + content['cleanup'] + '\n' try: with open(path, 'w', encoding='utf-8') as md_file: md_file.write(to_print) echo(style( f"Test case successfully stored into '{path}'.", fg='magenta')) except IOError: raise ConvertError(f"Unable to write '{path}'.")
def create_nitrate_case(test): """ Create new nitrate case """ import_nitrate() # Get category from Makefile try: with open('Makefile', encoding='utf-8') as makefile_file: makefile = makefile_file.read() category = re.search(r'echo\s+"Type:\s*(.*)"', makefile, re.M).group(1) # Default to 'Sanity' if Makefile or Type not found except (IOError, AttributeError): category = 'Sanity' # Create the new test case remote_dirname = re.sub('.git$', '', os.path.basename(test.fmf_id['url'])) if not remote_dirname: raise ConvertError("Unable to find git remote url.") summary = test.node.get('extra-summary', (remote_dirname or "") + (test.name or "") + ' - ' + (test.summary or "")) category = nitrate.Category(name=category, product=DEFAULT_PRODUCT) testcase = nitrate.TestCase(summary=summary, category=category) echo(style(f"Test case '{testcase.identifier}' created.", fg='blue')) return testcase
def read_manual(plan_id, case_id, disabled, with_script): """ Reads metadata of manual test cases from Nitrate """ nitrate = tmt.export.import_nitrate() # Turns off nitrate caching nitrate.set_cache_level(0) try: tree = fmf.Tree(os.getcwd()) except fmf.utils.RootError: raise ConvertError("Initialize metadata tree using 'tmt init'.") try: if plan_id: all_cases = nitrate.TestPlan(int(plan_id)).testcases case_ids = [case.id for case in all_cases if not case.automated] else: case_ids = [int(case_id)] except ValueError: raise ConvertError('Test plan/case identifier must be an integer.') # Create directory to store manual tests in old_cwd = os.getcwd() os.chdir(tree.root) try: os.mkdir('Manual') except FileExistsError: pass os.chdir('Manual') for case_id in case_ids: testcase = nitrate.TestCase(case_id) if testcase.status.name != 'CONFIRMED' and not disabled: log.debug( testcase.identifier + ' skipped (testcase is not CONFIRMED).') continue if testcase.script is not None and not with_script: log.debug(testcase.identifier + ' skipped (script is not empty).') continue # Filename sanitization dir_name = testcase.summary.replace(' ', '_') dir_name = dir_name.replace('/', '_') try: os.mkdir(dir_name) except FileExistsError: pass os.chdir(dir_name) echo("Importing the '{0}' test case.".format(dir_name)) # Test case data md_content = read_manual_data(testcase) # Test case metadata data = read_nitrate_case(testcase) data['manual'] = True data['test'] = 'test.md' write_markdown(os.getcwd() + '/test.md', md_content) write(os.getcwd() + '/main.fmf', data) os.chdir('..') os.chdir(old_cwd)
def read(path, makefile, nitrate, purpose): """ Read old metadata from various sources """ echo(style("Checking the '{0}' directory.".format(path), fg='red')) data = dict() # Makefile (extract summary, component and duration) if makefile: echo(style('Makefile ', fg='blue'), nl=False) makefile_path = os.path.join(path, 'Makefile') try: with open(makefile_path, encoding='utf-8') as makefile: content = makefile.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(makefile_path)) echo("found in '{0}'.".format(makefile_path)) # Test test = re.search('export TEST=(.*)\n', content).group(1) echo(style('test: ', fg='green') + test) # Summary data['summary'] = re.search( r'echo "Description:\s*(.*)"', content).group(1) echo(style('description: ', fg='green') + data['summary']) # Component data['component'] = re.search( r'echo "RunFor:\s*(.*)"', content).group(1) echo(style('component: ', fg='green') + data['component']) # Duration data['duration'] = re.search( r'echo "TestTime:\s*(.*)"', content).group(1) echo(style('duration: ', fg='green') + data['duration']) # Purpose (extract everything after the header as a description) if purpose: echo(style('Purpose ', fg='blue'), nl=False) purpose_path = os.path.join(path, 'PURPOSE') try: with open(purpose_path, encoding='utf-8') as purpose: content = purpose.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(purpose_path)) echo("found in '{0}'.".format(purpose_path)) for header in ['PURPOSE', 'Description', 'Author']: content = re.sub('^{0}.*\n'.format(header), '', content) data['description'] = content.lstrip('\n') echo(style('description:', fg='green')) echo(data['description'].rstrip('\n')) # Nitrate (extract contact, environment and relevancy) if nitrate: echo(style('Nitrate ', fg='blue'), nl=False) if test is None: raise ConvertError('No test name detected for nitrate search') if TestCase is None: raise ConvertError('Need nitrate module to import metadata') testcases = list(TestCase.search(script=test)) if not testcases: raise ConvertError("No testcase found for '{0}'.".format(test)) elif len(testcases) > 1: log.warn("Multiple test cases found for '{0}', using {1}".format( test, testcases[0].identifier)) testcase = testcases[0] echo("test case found '{0}'.".format(testcase.identifier)) # Contact if testcase.tester: data['contact'] = '{} <{}>'.format( testcase.tester.name, testcase.tester.email) echo(style('contact: ', fg='green') + data['contact']) # Environment if testcase.arguments: data['environment'] = tmt.utils.variables_to_dictionary( testcase.arguments) echo(style('environment:', fg='green')) echo(pprint.pformat(data['environment'])) # Relevancy field = tmt.utils.StructuredField(testcase.notes) data['relevancy'] = field.get('relevancy') echo(style('relevancy:', fg='green')) echo(data['relevancy'].rstrip('\n')) log.debug('Gathered metadata:\n' + pprint.pformat(data)) return data
def export_to_nitrate(test): """ Export fmf metadata to nitrate test cases """ import_nitrate() new_test_created = False # Check command line options create = test.opt('create') general = test.opt('general') duplicate = test.opt('duplicate') link_bugzilla = test.opt('bugzilla') if link_bugzilla: import_bugzilla() try: bz_instance = bugzilla.Bugzilla(url=BUGZILLA_XMLRPC_URL) except Exception as exc: log.debug(traceback.format_exc()) raise ConvertError("Couldn't initialize the Bugzilla client.", original=exc) if not bz_instance.logged_in: raise ConvertError( "Not logged to Bugzilla, check `man bugzilla` section " "'AUTHENTICATION CACHE AND API KEYS'.") # Check nitrate test case try: nitrate_id = test.node.get('extra-nitrate')[3:] nitrate_case = nitrate.TestCase(int(nitrate_id)) nitrate_case.summary # Make sure we connect to the server now echo(style(f"Test case '{nitrate_case.identifier}' found.", fg='blue')) except TypeError: # Create a new nitrate test case if create: nitrate_case = None # Check for existing Nitrate tests with the same fmf id if not duplicate: testcases = _nitrate_find_fmf_testcases(test) try: # Select the first found testcase if any nitrate_case = next(testcases) except StopIteration: pass if not nitrate_case: nitrate_case = create_nitrate_case(test) new_test_created = True # Newly created tmt tests have special format summary test._metadata['extra-summary'] = nitrate_case.summary else: raise ConvertError(f"Nitrate test case id not found for {test}" " (You can use --create option to enforce" " creating testcases)") except (nitrate.NitrateError, gssapi.raw.misc.GSSError) as error: raise ConvertError(error) # Summary summary = (test._metadata.get('extra-summary') or test._metadata.get('extra-task') or test.summary or test.name) if summary: nitrate_case.summary = summary echo(style('summary: ', fg='green') + summary) else: raise ConvertError("Nitrate case summary could not be determined.") # Script if test.node.get('extra-task'): nitrate_case.script = test.node.get('extra-task') echo(style('script: ', fg='green') + test.node.get('extra-task')) # Components # First remove any components that are already there nitrate_case.components.clear() if general: # Remove also all general plans linked to testcase for nitrate_plan in [plan for plan in nitrate_case.testplans]: if nitrate_plan.type.name == "General": nitrate_case.testplans.remove(nitrate_plan) # Then add fmf ones if test.component: echo(style('components: ', fg='green') + ' '.join(test.component)) for component in test.component: try: nitrate_case.components.add( nitrate.Component(name=component, product=DEFAULT_PRODUCT.id)) except nitrate.xmlrpc_driver.NitrateError as error: log.debug(error) echo(style(f"Failed to add component '{component}'.", fg='red')) if general: try: general_plan = find_general_plan(component) nitrate_case.testplans.add(general_plan) except nitrate.NitrateError as error: log.debug(error) echo( style( f"Failed to link general test plan for '{component}'.", fg='red')) # Tags nitrate_case.tags.clear() # Convert 'tier' attribute into a Tier tag if test.tier is not None: test.tag.append(f"Tier{test.tier}") # Add special fmf-export tag test.tag.append('fmf-export') nitrate_case.tags.add([nitrate.Tag(tag) for tag in test.tag]) echo(style('tags: ', fg='green') + ' '.join(set(test.tag))) # Default tester if test.contact: # Need to pick one value, so picking the first contact email_address = email.utils.parseaddr(test.contact[0])[1] # TODO handle nitrate user not existing and other possible exceptions nitrate_case.tester = nitrate.User(email_address) echo(style('default tester: ', fg='green') + email_address) # Duration nitrate_case.time = test.duration echo(style('estimated time: ', fg='green') + test.duration) # Manual nitrate_case.automated = not test.manual echo(style('automated: ', fg='green') + ['auto', 'manual'][test.manual]) # Status current_status = nitrate_case.status # Enable enabled tests if test.enabled: nitrate_case.status = nitrate.CaseStatus('CONFIRMED') echo(style('status: ', fg='green') + 'CONFIRMED') # Disable disabled tests which are CONFIRMED elif current_status == nitrate.CaseStatus('CONFIRMED'): nitrate_case.status = nitrate.CaseStatus('DISABLED') echo(style('status: ', fg='green') + 'DISABLED') # Keep disabled tests in their states else: echo(style('status: ', fg='green') + str(current_status)) # Environment if test.environment: environment = ' '.join(tmt.utils.shell_variables(test.environment)) nitrate_case.arguments = environment echo(style('arguments: ', fg='green') + environment) else: # FIXME unable clear to set empty arguments # (possibly error in xmlrpc, BZ#1805687) nitrate_case.arguments = ' ' echo(style('arguments: ', fg='green') + "' '") # Structured Field struct_field = tmt.utils.StructuredField(nitrate_case.notes) echo(style('Structured Field: ', fg='green')) # Mapping of structured field sections to fmf case attributes section_to_attr = { 'description': test.summary, 'purpose-file': test.description, 'hardware': test.node.get('extra-hardware'), 'pepa': test.node.get('extra-pepa'), } for section, attribute in section_to_attr.items(): if attribute is None: try: struct_field.remove(section) except tmt.utils.StructuredFieldError: pass else: struct_field.set(section, attribute) echo(style(section + ': ', fg='green') + attribute.strip()) # fmf identifer fmf_id = tmt.utils.dict_to_yaml(test.fmf_id) struct_field.set('fmf', fmf_id) echo(style('fmf id:\n', fg='green') + fmf_id.strip()) # Warning if WARNING not in struct_field.header(): struct_field.header(WARNING + struct_field.header()) echo(style('Add migration warning to the test case notes.', fg='green')) # Saving case.notes with edited StructField nitrate_case.notes = struct_field.save() # Export manual test instructions from *.md file to nitrate as html md_path = return_markdown_file() if os.path.exists(md_path): step, expect, setup, cleanup = convert_manual_to_nitrate(md_path) nitrate.User()._server.TestCase.store_text(nitrate_case.id, step, expect, setup, cleanup) echo(style(f"manual steps:", fg='green') + f" found in {md_path}") # Append id of newly created nitrate case to its file if new_test_created: echo(style(f"Append the nitrate test case id.", fg='green')) try: with test.node as data: data["extra-nitrate"] = nitrate_case.identifier except AttributeError: # FIXME: Remove this deprecated code after fmf support # for storing modified data is released long enough file_path = test.node.sources[-1] try: with open(file_path, encoding='utf-8', mode='a+') as file: file.write(f"extra-nitrate: {nitrate_case.identifier}\n") except IOError: raise ConvertError("Unable to open '{0}'.".format(file_path)) # List of bugs test verifies verifies_bug_ids = [] for link in test.link: try: verifies_bug_ids.append( int(re.search(RE_BUGZILLA_URL, link['verifies']).group(1))) except Exception as err: log.debug(err) # Add bugs to the Nitrate case for bug_id in verifies_bug_ids: nitrate_case.bugs.add(nitrate.Bug(bug=int(bug_id))) # Update nitrate test case nitrate_case.update() echo( style("Test case '{0}' successfully exported to nitrate.".format( nitrate_case.identifier), fg='magenta')) # Optionally link Bugzilla to Nitrate case if link_bugzilla and verifies_bug_ids: try: bz_set_coverage(bz_instance, verifies_bug_ids, int(nitrate_case.id)) echo( style("Linked to Bugzilla: {}.".format(" ".join( [f"BZ#{bz_id}" for bz_id in verifies_bug_ids])), fg='magenta')) except Exception as err: raise ConvertError("Couldn't update bugs", original=err)
def read(path, makefile, nitrate, purpose): """ Read old metadata from various sources Returns tuple (common_data, individual_data) where 'common_data' are metadata which belong to main.fmf and 'individual_data' contains data for individual testcases (if multiple nitrate testcases found). """ data = dict() echo("Checking the '{0}' directory.".format(path)) # Makefile (extract summary, test, duration and requires) if makefile: echo(style('Makefile ', fg='blue'), nl=False) makefile_path = os.path.join(path, 'Makefile') try: with open(makefile_path, encoding='utf-8') as makefile: content = makefile.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(makefile_path)) echo("found in '{0}'.".format(makefile_path)) # Beaker task name try: beaker_task = re.search('export TEST=(.*)\n', content).group(1) echo(style('task: ', fg='green') + beaker_task) data['extra-task'] = beaker_task except AttributeError: raise ConvertError("Unable to parse 'TEST' from the Makefile.") # Summary data['summary'] = re.search(r'echo "Description:\s*(.*)"', content).group(1) echo(style('summary: ', fg='green') + data['summary']) # Test script data['test'] = re.search('^run:.*\n\t(.*)$', content, re.M).group(1) echo(style('test: ', fg='green') + data['test']) # Component data['component'] = re.search(r'echo "RunFor:\s*(.*)"', content).group(1).split() echo(style('component: ', fg='green') + ' '.join(data['component'])) # Duration try: data['duration'] = re.search(r'echo "TestTime:\s*(.*)"', content).group(1) echo(style('duration: ', fg='green') + data['duration']) except AttributeError: pass # Requires and RhtsRequires (optional) requires = re.findall(r'echo "(?:Rhts)?Requires:\s*(.*)"', content) if requires: data['require'] = [ require for line in requires for require in line.split() ] echo(style('require: ', fg='green') + ' '.join(data['require'])) # Purpose (extract everything after the header as a description) if purpose: echo(style('Purpose ', fg='blue'), nl=False) purpose_path = os.path.join(path, 'PURPOSE') try: with open(purpose_path, encoding='utf-8') as purpose: content = purpose.read() echo("found in '{0}'.".format(purpose_path)) for header in ['PURPOSE', 'Description', 'Author']: content = re.sub('^{0}.*\n'.format(header), '', content) data['description'] = content.lstrip('\n') echo(style('description:', fg='green')) echo(data['description'].rstrip('\n')) except IOError: echo("not found.") # Nitrate (extract contact, environment and relevancy) if nitrate: common_data, individual_data = read_nitrate(beaker_task, data) else: common_data = data individual_data = [] log.debug('Common metadata:\n' + pprint.pformat(common_data)) log.debug('Individual metadata:\n' + pprint.pformat(individual_data)) return common_data, individual_data
def read_nitrate(beaker_task, common_data): """ Read old metadata from nitrate test cases """ # Check test case, make sure nitrate is available echo(style('Nitrate ', fg='blue'), nl=False) if beaker_task is None: raise ConvertError('No test name detected for nitrate search') if TestCase is None: raise ConvertError('Need nitrate module to import metadata') # Find testcases that have CONFIRMED status testcases = list(TestCase.search(script=beaker_task, case_status=2)) if not testcases: echo("No testcase found for '{0}'.".format(beaker_task)) return common_data, [] elif len(testcases) > 1: echo("Multiple test cases found for '{0}'.".format(beaker_task)) # Process individual test cases individual_data = list() for testcase in testcases: data = dict() echo("test case found '{0}'.".format(testcase.identifier)) # Test identifier data['extra-nitrate'] = testcase.identifier # Beaker task name (taken from summary) if testcase.summary: data['extra-summary'] = testcase.summary echo(style('extra-summary: ', fg='green') + data['extra-summary']) # Contact if testcase.tester: data['contact'] = '{} <{}>'.format(testcase.tester.name, testcase.tester.email) echo(style('contact: ', fg='green') + data['contact']) # Environment if testcase.arguments: data['environment'] = tmt.utils.variables_to_dictionary( testcase.arguments) echo(style('environment:', fg='green')) echo(pprint.pformat(data['environment'])) # Tags if testcase.tags: data['tag'] = sorted([ tag.name for tag in testcase.tags if tag.name != 'fmf-export' ]) echo(style('tag: ', fg='green') + str(data['tag'])) # Component data['component'] = [comp.name for comp in testcase.components] echo(style('component: ', fg='green') + ' '.join(data['component'])) # Status data['enabled'] = testcase.status.name == "CONFIRMED" echo(style('enabled: ', fg='green') + str(data['enabled'])) # Relevancy field = tmt.utils.StructuredField(testcase.notes) try: relevancy = field.get('relevancy') if relevancy: data['relevancy'] = relevancy echo(style('relevancy:', fg='green')) echo(data['relevancy'].rstrip('\n')) except tmt.utils.StructuredFieldError: pass # Extras: [pepa] and [hardware] try: extra_pepa = field.get('pepa') if extra_pepa: data['extra-pepa'] = extra_pepa echo(style('extra-pepa:', fg='green')) echo(data['extra-pepa'].rstrip('\n')) except tmt.utils.StructuredFieldError: pass try: extra_hardware = field.get('hardware') if extra_hardware: data['extra-hardware'] = extra_hardware echo(style('extra-hardware:', fg='green')) echo(data['extra-hardware'].rstrip('\n')) except tmt.utils.StructuredFieldError: pass individual_data.append(data) # Find common data from individual test cases common_candidates = dict() histogram = dict() for testcase in individual_data: if individual_data.index(testcase) == 0: common_candidates = copy.copy(testcase) for key in testcase: histogram[key] = 1 else: for key, value in testcase.items(): if key in common_candidates: if value != common_candidates[key]: common_candidates.pop(key) if key in histogram: histogram[key] += 1 for key in histogram: if key in common_candidates and histogram[key] < len(individual_data): common_candidates.pop(key) # Add common data to main.fmf for key, value in common_candidates.items(): common_data[key] = value # If there is only single testcase found there is no need to continue if len(individual_data) <= 1: return common_data, [] # Remove common data from individual fmfs for common_key in common_candidates: for testcase in individual_data: if common_key in testcase: testcase.pop(common_key) return common_data, individual_data
def read(path, makefile, restraint, nitrate, purpose, disabled, types): """ Read old metadata from various sources Returns tuple (common_data, individual_data) where 'common_data' are metadata which belong to main.fmf and 'individual_data' contains data for individual testcases (if multiple nitrate testcases found). """ echo("Checking the '{0}' directory.".format(path)) # Make sure there is a metadata tree initialized try: tree = fmf.Tree(path) except fmf.utils.RootError: raise ConvertError("Initialize metadata tree using 'tmt init'.") # Ascertain if datafile is of type Makefile or metadata makefile_file = None restraint_file = None filename = None files = \ [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] # Ascertain which file to use based on cmd arg. # If both are false raise an assertion. # If both are true then default to using # the restraint metadata file. # Raise an assertion if the file is not found. if not makefile and not restraint: raise ConvertError("Please specify either a " "Makefile or Restraint file.") elif makefile and restraint: if 'metadata' in files: filename = 'metadata' restraint_file = True echo(style('Restraint file ', fg='blue'), nl=False) elif 'Makefile' in files: filename = 'Makefile' makefile_file = True echo(style('Makefile ', fg='blue'), nl=False) else: raise ConvertError("Unable to find any metadata file.") elif makefile: if 'Makefile' not in files: raise ConvertError("Unable to find Makefile") else: filename = 'Makefile' makefile_file = True echo(style('Makefile ', fg='blue'), nl=False) elif restraint: if 'metadata' not in files: raise ConvertError("Unable to find restraint metadata file") else: filename = 'metadata' restraint_file = True echo(style('Restraint ', fg='blue'), nl=False) # Open the datafile if restraint_file or makefile_file: datafile_path = os.path.join(path, filename) try: with open(datafile_path, encoding='utf-8') as datafile_file: datafile = datafile_file.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(datafile_path)) echo("found in '{0}'.".format(datafile_path)) # If testinfo.desc exists read it to preserve content and remove it testinfo_path = os.path.join(path, 'testinfo.desc') if os.path.isfile(testinfo_path): try: with open(testinfo_path, encoding='utf-8') as testinfo: old_testinfo = testinfo.read() os.remove(testinfo_path) except IOError: raise ConvertError("Unable to open '{0}'.".format(testinfo_path)) else: old_testinfo = None # Make Makefile 'makeable' without extra dependecies # (replace targets, make include optional and remove rhts-lint) if makefile_file: datafile = datafile.replace('$(METADATA)', 'testinfo.desc') datafile = re.sub(r'^include /usr/share/rhts/lib/rhts-make.include', '-include /usr/share/rhts/lib/rhts-make.include', datafile, flags=re.MULTILINE) datafile = re.sub('.*rhts-lint.*', '', datafile) # Create testinfo.desc file with resolved variables try: subprocess.run(["make", "testinfo.desc", "-C", path, "-f", "-"], input=datafile, check=True, encoding='utf-8', stdout=subprocess.DEVNULL) except FileNotFoundError: raise ConvertError("Install tmt-test-convert to " "convert metadata from {0}.".format(filename)) except subprocess.CalledProcessError: raise ConvertError( "Failed to convert metadata using 'make testinfo.desc'.") # Read testinfo.desc try: with open(testinfo_path, encoding='utf-8') as testinfo_file: testinfo = testinfo_file.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(testinfo_path)) # restraint if restraint_file: beaker_task, data = \ read_datafile(path, filename, datafile, types) # Makefile (extract summary, test, duration and requires) else: beaker_task, data = \ read_datafile(path, filename, datafile, types, testinfo) # Warn if makefile has extra lines in run and build targets def target_content(target): """ Extract lines from the target content """ regexp = rf"^{target}:.*\n((?:\t[^\n]*\n?)*)" target = re.search(regexp, datafile, re.M).group(1) return [line.strip('\t') for line in target.splitlines()] run_target_list = target_content("run") run_target_list.remove(data["test"]) if run_target_list: echo( style(f"warn: Extra lines detected in the 'run' target:", fg="yellow")) for line in run_target_list: echo(f" {line}") build_target_list = target_content("build") if len(build_target_list) > 1: echo( style(f"warn: Multiple lines detected in the 'build' target:", fg="yellow")) for line in build_target_list: echo(f" {line}") # Restore the original testinfo.desc content (if existed) if old_testinfo: try: with open(testinfo_path, 'w', encoding='utf-8') as testinfo: testinfo.write(old_testinfo) except IOError: raise ConvertError( "Unable to write '{0}'.".format(testinfo_path)) # Remove created testinfo.desc otherwise else: os.remove(testinfo_path) # Purpose (extract everything after the header as a description) if purpose: echo(style('Purpose ', fg='blue'), nl=False) purpose_path = os.path.join(path, 'PURPOSE') try: with open(purpose_path, encoding='utf-8') as purpose: content = purpose.read() echo("found in '{0}'.".format(purpose_path)) for header in ['PURPOSE', 'Description', 'Author']: content = re.sub('^{0}.*\n'.format(header), '', content) data['description'] = content.lstrip('\n') echo(style('description:', fg='green')) echo(data['description'].rstrip('\n')) except IOError: echo("not found.") # Nitrate (extract contact, environment and relevancy) if nitrate: common_data, individual_data = read_nitrate(beaker_task, data, disabled) else: common_data = data individual_data = [] # Remove keys which are inherited from parent parent_path = os.path.dirname(path.rstrip('/')) parent_name = '/' + os.path.relpath(parent_path, tree.root) parent = tree.find(parent_name) if parent: for test in [common_data] + individual_data: for key in list(test): if parent.get(key) == test[key]: test.pop(key) log.debug('Common metadata:\n' + pprint.pformat(common_data)) log.debug('Individual metadata:\n' + pprint.pformat(individual_data)) return common_data, individual_data
def export_to_nitrate(test, create, general): """ Export fmf metadata to nitrate test cases """ import_nitrate() new_test_created = False # Check nitrate test case try: nitrate_id = test.node.get('extra-nitrate')[3:] nitrate_case = nitrate.TestCase(int(nitrate_id)) echo(style(f"Test case '{nitrate_case.identifier}' found.", fg='blue')) except TypeError: # Create a new nitrate test case if create: nitrate_case = create_nitrate_case(test) new_test_created = True else: raise ConvertError("Nitrate test case id not found.") # Summary summary = test.node.get('extra-summary', test.node.get('extra-task', test.summary)) if summary: nitrate_case.summary = summary echo(style('summary: ', fg='green') + summary) else: raise ConvertError("Nitrate case summary could not be determined.") # Script if test.node.get('extra-task'): nitrate_case.script = test.node.get('extra-task') echo(style('script: ', fg='green') + test.node.get('extra-task')) # Components # First remove any components that are already there nitrate_case.components.clear() # Then add fmf ones if test.component: echo(style('components: ', fg='green') + ' '.join(test.component)) for component in test.component: try: nitrate_case.components.add( nitrate.Component(name=component, product=DEFAULT_PRODUCT.id)) except nitrate.xmlrpc_driver.NitrateError as error: log.debug(error) echo(style(f"Failed to add component '{component}'.", fg='red')) if general: try: general_plan = find_general_plan(component) nitrate_case.testplans.add(general_plan) except nitrate.NitrateError as error: log.debug(error) echo( style( f"Failed to link general test plan for '{component}'.", fg='red')) # Tags nitrate_case.tags.clear() # Convert 'tier' attribute into a Tier tag if test.tier is not None: test.tag.append(f"Tier{test.tier}") # Add special fmf-export tag test.tag.append('fmf-export') nitrate_case.tags.add([nitrate.Tag(tag) for tag in test.tag]) echo(style('tags: ', fg='green') + ' '.join(set(test.tag))) # Default tester if test.contact: # Need to pick one value, so picking the first contact email_address = email.utils.parseaddr(test.contact[0])[1] # TODO handle nitrate user not existing and other possible exceptions nitrate_case.tester = nitrate.User(email_address) echo(style('default tester: ', fg='green') + email_address) # Duration nitrate_case.time = test.duration echo(style('estimated time: ', fg='green') + test.duration) # Status current_status = nitrate_case.status # Enable enabled tests if test.enabled: nitrate_case.status = nitrate.CaseStatus('CONFIRMED') echo(style('status: ', fg='green') + 'CONFIRMED') # Disable disabled tests which are CONFIRMED elif current_status == nitrate.CaseStatus('CONFIRMED'): nitrate_case.status = nitrate.CaseStatus('DISABLED') echo(style('status: ', fg='green') + 'DISABLED') # Keep disabled tests in their states else: echo(style('status: ', fg='green') + str(current_status)) # Environment if test.environment: environment = ' '.join(tmt.utils.shell_variables(test.environment)) nitrate_case.arguments = environment echo(style('arguments: ', fg='green') + environment) else: # FIXME unable clear to set empty arguments # (possibly error in xmlrpc, BZ#1805687) nitrate_case.arguments = ' ' echo(style('arguments: ', fg='green') + "' '") # Structured Field struct_field = tmt.utils.StructuredField(nitrate_case.notes) echo(style('Structured Field: ', fg='green')) # Mapping of structured field sections to fmf case attributes section_to_attr = { 'relevancy': test.relevancy, 'description': test.summary, 'purpose-file': test.description, 'hardware': test.node.get('extra-hardware'), 'pepa': test.node.get('extra-pepa'), } for section, attribute in section_to_attr.items(): if attribute is None: try: struct_field.remove(section) except tmt.utils.StructuredFieldError: pass else: struct_field.set(section, attribute) echo(style(section + ': ', fg='green') + attribute.strip()) # fmf identifer fmf_id = test.fmf_id struct_field.set('fmf', yaml.dump(fmf_id)) echo(style('fmf id:\n', fg='green') + yaml.dump(fmf_id).strip()) # Warning if WARNING not in struct_field.header(): struct_field.header(WARNING + struct_field.header()) echo(style('Added warning about porting to case notes.', fg='green')) # Saving case.notes with edited StructField nitrate_case.notes = struct_field.save() # Update nitrate test case nitrate_case.update() echo( style("Test case '{0}' successfully exported to nitrate.".format( nitrate_case.identifier), fg='magenta')) # Write id of newly created nitrate case to its file if new_test_created: fmf_file_path = test.node.sources[-1] try: with open(fmf_file_path, encoding='utf-8') as fmf_file: content = yaml.safe_load(fmf_file) except IOError: raise ConvertError("Unable to open '{0}'.".format(fmf_file_path)) content['extra-nitrate'] = nitrate_case.identifier tmt.convert.write(fmf_file_path, content)
def read_nitrate(beaker_task, common_data, disabled): """ Read old metadata from nitrate test cases """ # Need to import nitrate only when really needed. Otherwise we get # traceback when nitrate is not installed or config file not available. try: import nitrate import gssapi except ImportError: raise ConvertError('Install tmt-test-convert to import metadata.') # Check test case echo(style('Nitrate ', fg='blue'), nl=False) if beaker_task is None: raise ConvertError('No test name detected for nitrate search') # Find all testcases try: if disabled: testcases = list(nitrate.TestCase.search(script=beaker_task)) # Find testcases that do not have 'DISABLED' status else: testcases = list(nitrate.TestCase.search( script=beaker_task, case_status__in=[1, 2, 4])) except (nitrate.NitrateError, gssapi.raw.misc.GSSError) as error: raise ConvertError(error) if not testcases: echo("No {0}testcase found for '{1}'.".format( '' if disabled else 'non-disabled ', beaker_task)) return common_data, [] elif len(testcases) > 1: echo("Multiple test cases found for '{0}'.".format(beaker_task)) # Process individual test cases individual_data = list() md_content = dict() for testcase in testcases: # Testcase data must be fetched due to # https://github.com/psss/python-nitrate/issues/24 testcase._fetch() data = read_nitrate_case(testcase, common_data) individual_data.append(data) # Check testcase for manual data md_content_tmp = read_manual_data(testcase) if any(md_content_tmp.values()): md_content = md_content_tmp # Write md file if there is something to write # or try to remove if there isn't. md_path = os.getcwd() + '/test.md' if md_content: write_markdown(md_path, md_content) else: try: os.remove(md_path) echo(style(f"Test case file '{md_path}' " "successfully removed.", fg='magenta')) except FileNotFoundError: pass except IOError: raise ConvertError( "Unable to remove '{0}'.".format(md_path)) # Merge environment from Makefile and Nitrate if 'environment' in common_data: for case in individual_data: if 'environment' in case: case_environment = case['environment'] case['environment'] = common_data['environment'].copy() case['environment'].update(case_environment) # Merge description from PURPOSE with header/footer from Nitrate notes for testcase in individual_data: if 'description' in common_data: testcase['description'] = common_data['description'] + \ testcase['description'] if 'description' in common_data: common_data.pop('description') # Find common data from individual test cases common_candidates = dict() histogram = dict() for testcase in individual_data: if individual_data.index(testcase) == 0: common_candidates = copy.copy(testcase) for key in testcase: histogram[key] = 1 else: for key, value in testcase.items(): if key in common_candidates: if value != common_candidates[key]: common_candidates.pop(key) if key in histogram: histogram[key] += 1 for key in histogram: if key in common_candidates and histogram[key] < len(individual_data): common_candidates.pop(key) # Add common data to main.fmf for key, value in common_candidates.items(): common_data[key] = value # If there is only single testcase found there is no need to continue if len(individual_data) <= 1: return common_data, [] # Remove common data from individual fmfs for common_key in common_candidates: for testcase in individual_data: if common_key in testcase: testcase.pop(common_key) return common_data, individual_data
def read(path, makefile, nitrate, purpose, disabled): """ Read old metadata from various sources Returns tuple (common_data, individual_data) where 'common_data' are metadata which belong to main.fmf and 'individual_data' contains data for individual testcases (if multiple nitrate testcases found). """ data = dict(framework='beakerlib') echo("Checking the '{0}' directory.".format(path)) # Make sure there is a metadata tree initialized try: tree = fmf.Tree(path) except fmf.utils.RootError: raise ConvertError("Initialize metadata tree using 'tmt init'.") # Makefile (extract summary, test, duration and requires) if makefile: echo(style('Makefile ', fg='blue'), nl=False) makefile_path = os.path.join(path, 'Makefile') try: with open(makefile_path, encoding='utf-8') as makefile_file: makefile = makefile_file.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(makefile_path)) echo("found in '{0}'.".format(makefile_path)) # If testinfo.desc exists read it to preserve content and remove it testinfo_path = os.path.join(path, 'testinfo.desc') if os.path.isfile(testinfo_path): try: with open(testinfo_path, encoding='utf-8') as testinfo: old_testinfo = testinfo.read() os.remove(testinfo_path) except IOError: raise ConvertError( "Unable to open '{0}'.".format(testinfo_path)) else: old_testinfo = None # Make Makefile 'makeable' without extra dependecies # (replace targets, make include optional and remove rhts-lint) makefile = makefile.replace('$(METADATA)', 'testinfo.desc') makefile = re.sub( r'^include /usr/share/rhts/lib/rhts-make.include', '-include /usr/share/rhts/lib/rhts-make.include', makefile, flags=re.MULTILINE) makefile = re.sub('.*rhts-lint.*', '', makefile) # Create testinfo.desc file with resolved variables try: process = subprocess.run( ["make", "testinfo.desc", "-C", path, "-f", "-"], input=makefile, check=True, encoding='utf-8', stdout=subprocess.DEVNULL) except FileNotFoundError: raise ConvertError( "Install tmt-test-convert to convert metadata from Makefile.") except subprocess.CalledProcessError: raise ConvertError( "Failed to convert metadata using 'make testinfo.desc'.") # Read testinfo.desc try: with open(testinfo_path, encoding='utf-8') as testinfo_file: testinfo = testinfo_file.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(testinfo_path)) # Beaker task name try: beaker_task = re.search(r'Name:\s*(.*)\n', testinfo).group(1) echo(style('task: ', fg='green') + beaker_task) data['extra-task'] = beaker_task data['extra-summary'] = beaker_task except AttributeError: raise ConvertError("Unable to parse 'Name' from testinfo.desc.") # Summary try: data['summary'] = re.search( r'^Description:\s*(.*)\n', testinfo, re.M).group(1) echo(style('summary: ', fg='green') + data['summary']) except AttributeError: pass # Test script try: data['test'] = re.search( r'^run:.*\n\t(.*)$', makefile, re.M).group(1) echo(style('test: ', fg='green') + data['test']) except AttributeError: raise ConvertError("Makefile is missing the 'run' target.") # Contact try: data['contact'] = re.search( r'^Owner:\s*(.*)', testinfo, re.M).group(1) echo(style('contact: ', fg='green') + data['contact']) except AttributeError: pass # Component try: data['component'] = re.search( r'^RunFor:\s*(.*)', testinfo, re.M).group(1).split() echo(style('component: ', fg='green') + ' '.join(data['component'])) except AttributeError: pass # Duration try: data['duration'] = re.search( r'^TestTime:\s*(.*)', testinfo, re.M).group(1) echo(style('duration: ', fg='green') + data['duration']) except AttributeError: pass # Environment variables = re.findall(r'^Environment:\s*(.*)', testinfo, re.M) if variables: data['environment'] = {} for variable in variables: key, value = variable.split('=', maxsplit=1) data['environment'][key] = value echo(style('environment:', fg='green')) echo(pprint.pformat(data['environment'])) # RhtsRequires (optional) goes to require requires = re.findall(r'^RhtsRequires:\s*(.*)', testinfo, re.M) if requires: data['require'] = [ require for line in requires for require in line.split()] echo(style('require: ', fg='green') + ' '.join(data['require'])) # Requires (optional) goes to recommend recommends = re.findall(r'^Requires:\s*(.*)', testinfo, re.M) if recommends: data['recommend'] = [ recommend for line in recommends for recommend in line.split()] echo( style('recommend: ', fg='green') + ' '.join(data['recommend'])) # Multihost (from Type) -> Add tag for now try: mkfile_type = re.search(r'^Type:\s*(.*)', testinfo, re.M).group(1) if "Multihost" in mkfile_type: data['tag'] = ['multihost'] echo( style('multihost: ', fg='green') + 'Marked with the "multihost" tag') except AttributeError: pass # Add relevant bugs to the 'link' attribute for bug in re.findall(r'^Bug:\s*([0-9]+)', testinfo, re.M): add_bug(bug, data) # Restore the original testinfo.desc content (if existed) if old_testinfo: try: with open(testinfo_path, 'w', encoding='utf-8') as testinfo: testinfo.write(old_testinfo) except IOError: raise ConvertError( "Unable to write '{0}'.".format(testinfo_path)) # Remove created testinfo.desc otherwise else: os.remove(testinfo_path) # Purpose (extract everything after the header as a description) if purpose: echo(style('Purpose ', fg='blue'), nl=False) purpose_path = os.path.join(path, 'PURPOSE') try: with open(purpose_path, encoding='utf-8') as purpose: content = purpose.read() echo("found in '{0}'.".format(purpose_path)) for header in ['PURPOSE', 'Description', 'Author']: content = re.sub('^{0}.*\n'.format(header), '', content) data['description'] = content.lstrip('\n') echo(style('description:', fg='green')) echo(data['description'].rstrip('\n')) except IOError: echo("not found.") # Nitrate (extract contact, environment and relevancy) if nitrate: common_data, individual_data = read_nitrate( beaker_task, data, disabled) else: common_data = data individual_data = [] # Remove keys which are inherited from parent parent_path = os.path.dirname(path.rstrip('/')) parent_name = '/' + os.path.relpath(parent_path, tree.root) parent = tree.find(parent_name) if parent: for test in [common_data] + individual_data: for key in list(test): if parent.get(key) == test[key]: test.pop(key) log.debug('Common metadata:\n' + pprint.pformat(common_data)) log.debug('Individual metadata:\n' + pprint.pformat(individual_data)) return common_data, individual_data
def read_nitrate(beaker_task, common_data, disabled): """ Read old metadata from nitrate test cases """ # Need to import nitrate only when really needed. Otherwise we get # traceback when nitrate not installed or config file not available. try: import nitrate except ImportError: raise ConvertError('Install nitrate module to import metadata.') # Check test case echo(style('Nitrate ', fg='blue'), nl=False) if beaker_task is None: raise ConvertError('No test name detected for nitrate search') # Find all testcases try: if disabled: testcases = list(nitrate.TestCase.search(script=beaker_task)) # Find testcases that do not have 'DISABLED' status else: testcases = list(nitrate.TestCase.search( script=beaker_task, case_status__in=[1, 2, 4])) except nitrate.NitrateError as error: raise ConvertError(error) if not testcases: echo("No {0}testcase found for '{1}'.".format( '' if disabled else 'non-disabled ', beaker_task)) return common_data, [] elif len(testcases) > 1: echo("Multiple test cases found for '{0}'.".format(beaker_task)) # Process individual test cases individual_data = list() for testcase in testcases: data = dict() echo("test case found '{0}'.".format(testcase.identifier)) # Test identifier data['extra-nitrate'] = testcase.identifier # Beaker task name (taken from summary) if testcase.summary: data['extra-summary'] = testcase.summary echo(style('extra-summary: ', fg='green') + data['extra-summary']) # Contact if testcase.tester: data['contact'] = '{} <{}>'.format( testcase.tester.name, testcase.tester.email) echo(style('contact: ', fg='green') + data['contact']) # Environment if testcase.arguments: data['environment'] = tmt.utils.variables_to_dictionary( testcase.arguments) if not data['environment']: data.pop('environment') else: echo(style('environment:', fg='green')) echo(pprint.pformat(data['environment'])) # Tags if testcase.tags: tags = [] for tag in testcase.tags: if tag.name == 'fmf-export': continue tags.append(tag.name) # Add the tier attribute, if there are multiple TierX tags, # pick the one with the lowest index. tier_match = re.match(r'^Tier ?(?P<num>\d+)$', tag.name, re.I) if tier_match: num = tier_match.group('num') if 'tier' in data: log.warning('Multiple Tier tags found, using the one ' 'with a lower index') if int(num) < int(data['tier']): data['tier'] = num else: data['tier'] = num data['tag'] = sorted(tags) echo(style('tag: ', fg='green') + str(data['tag'])) # Tier try: echo(style('tier: ', fg='green') + data['tier']) except KeyError: pass # Component data['component'] = [comp.name for comp in testcase.components] echo(style('component: ', fg='green') + ' '.join(data['component'])) # Status data['enabled'] = testcase.status.name == "CONFIRMED" echo(style('enabled: ', fg='green') + str(data['enabled'])) # Relevancy field = tmt.utils.StructuredField(testcase.notes) try: relevancy = field.get('relevancy') if relevancy: data['relevancy'] = relevancy echo(style('relevancy:', fg='green')) echo(data['relevancy'].rstrip('\n')) except tmt.utils.StructuredFieldError: pass # Extras: [pepa] and [hardware] try: extra_pepa = field.get('pepa') if extra_pepa: data['extra-pepa'] = extra_pepa echo(style('extra-pepa:', fg='green')) echo(data['extra-pepa'].rstrip('\n')) except tmt.utils.StructuredFieldError: pass try: extra_hardware = field.get('hardware') if extra_hardware: data['extra-hardware'] = extra_hardware echo(style('extra-hardware:', fg='green')) echo(data['extra-hardware'].rstrip('\n')) except tmt.utils.StructuredFieldError: pass individual_data.append(data) # Find common data from individual test cases common_candidates = dict() histogram = dict() for testcase in individual_data: if individual_data.index(testcase) == 0: common_candidates = copy.copy(testcase) for key in testcase: histogram[key] = 1 else: for key, value in testcase.items(): if key in common_candidates: if value != common_candidates[key]: common_candidates.pop(key) if key in histogram: histogram[key] += 1 for key in histogram: if key in common_candidates and histogram[key] < len(individual_data): common_candidates.pop(key) # Add common data to main.fmf for key, value in common_candidates.items(): common_data[key] = value # If there is only single testcase found there is no need to continue if len(individual_data) <= 1: return common_data, [] # Remove common data from individual fmfs for common_key in common_candidates: for testcase in individual_data: if common_key in testcase: testcase.pop(common_key) return common_data, individual_data
def vagrant_status(self): """ Get vagrant's status """ raise ConvertError('NYI: cannot currently return status.')
def read(path, makefile, nitrate, purpose, disabled): """ Read old metadata from various sources Returns tuple (common_data, individual_data) where 'common_data' are metadata which belong to main.fmf and 'individual_data' contains data for individual testcases (if multiple nitrate testcases found). """ data = dict() echo("Checking the '{0}' directory.".format(path)) # Make sure there is a metadata tree initialized try: tree = fmf.Tree(path) except fmf.utils.RootError: raise ConvertError("Initialize metadata tree using 'tmt init'.") # Makefile (extract summary, test, duration and requires) if makefile: echo(style('Makefile ', fg='blue'), nl=False) makefile_path = os.path.join(path, 'Makefile') try: with open(makefile_path, encoding='utf-8') as makefile_file: makefile = makefile_file.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(makefile_path)) echo("found in '{0}'.".format(makefile_path)) # If testinfo.desc exists read it to preserve content and remove it testinfo_path = os.path.join(path, 'testinfo.desc') if os.path.isfile(testinfo_path): try: with open(testinfo_path, encoding='utf-8') as testinfo: old_testinfo = testinfo.read() os.remove(testinfo_path) except IOError: raise ConvertError( "Unable to open '{0}'.".format(testinfo_path)) else: old_testinfo = None # Make Makefile 'makeable' without extra dependecies # (replace targets, make include optional and remove rhts-lint) makefile = makefile.replace('$(METADATA)', 'testinfo.desc') makefile = makefile.replace( 'include /usr/share/rhts/lib/rhts-make.include', '-include /usr/share/rhts/lib/rhts-make.include') makefile = makefile.replace('rhts-lint testinfo.desc', '') # Create testinfo.desc file with resolved variables try: process = subprocess.run( ["make", "testinfo.desc", "-C", path, "-f", "-"], input=makefile, check=True, encoding='utf-8', stdout=subprocess.DEVNULL) except FileNotFoundError: raise ConvertError( "Install 'make' to convert metadata from Makefile.") except subprocess.CalledProcessError: raise ConvertError( "Failed to convert metadata using 'make testinfo.desc'.") # Read testinfo.desc try: with open(testinfo_path, encoding='utf-8') as testinfo_file: testinfo = testinfo_file.read() except IOError: raise ConvertError("Unable to open '{0}'.".format(testinfo_path)) # Beaker task name try: beaker_task = re.search(r'Name:\s*(.*)\n', testinfo).group(1) echo(style('task: ', fg='green') + beaker_task) data['extra-task'] = beaker_task except AttributeError: raise ConvertError("Unable to parse 'Name' from testinfo.desc.") # Summary try: data['summary'] = re.search( r'^Description:\s*(.*)\n', testinfo, re.M).group(1) echo(style('summary: ', fg='green') + data['summary']) except AttributeError: pass # Test script try: data['test'] = re.search( r'^run:.*\n\t(.*)$', makefile, re.M).group(1) echo(style('test: ', fg='green') + data['test']) except AttributeError: raise ConvertError("Makefile is missing the 'run' target.") # Component try: data['component'] = re.search( r'^RunFor:\s*(.*)', testinfo, re.M).group(1).split() echo(style('component: ', fg='green') + ' '.join(data['component'])) except AttributeError: pass # Duration try: data['duration'] = re.search( r'^TestTime:\s*(.*)', testinfo, re.M).group(1) echo(style('duration: ', fg='green') + data['duration']) except AttributeError: pass # Requires and RhtsRequires (optional) requires = re.findall(r'^(?:Rhts)?Requires:\s*(.*)', testinfo, re.M) if requires: data['require'] = [ require for line in requires for require in line.split()] echo(style('require: ', fg='green') + ' '.join(data['require'])) # Restore the original testinfo.desc content (if existed) if old_testinfo: try: with open(testinfo_path, 'w', encoding='utf-8') as testinfo: testinfo.write(old_testinfo) except IOError: raise ConvertError( "Unable to write '{0}'.".format(testinfo_path)) # Remove created testinfo.desc otherwise else: os.remove(testinfo_path) # Purpose (extract everything after the header as a description) if purpose: echo(style('Purpose ', fg='blue'), nl=False) purpose_path = os.path.join(path, 'PURPOSE') try: with open(purpose_path, encoding='utf-8') as purpose: content = purpose.read() echo("found in '{0}'.".format(purpose_path)) for header in ['PURPOSE', 'Description', 'Author']: content = re.sub('^{0}.*\n'.format(header), '', content) data['description'] = content.lstrip('\n') echo(style('description:', fg='green')) echo(data['description'].rstrip('\n')) except IOError: echo("not found.") # Nitrate (extract contact, environment and relevancy) if nitrate: common_data, individual_data = read_nitrate( beaker_task, data, disabled) else: common_data = data individual_data = [] log.debug('Common metadata:\n' + pprint.pformat(common_data)) log.debug('Individual metadata:\n' + pprint.pformat(individual_data)) return common_data, individual_data
def read_datafile(path, filename, datafile, types, testinfo=None): """ Read data values from supplied Makefile or metadata file. Returns task name and a dictionary of the collected values. """ data = dict() if filename == 'Makefile': regex_task = r'Name:\s*(.*)\n' regex_summary = r'^Description:\s*(.*)\n' regex_test = r'^run:.*\n\t(.*)$' regex_contact = r'^Owner:\s*(.*)' regex_duration = r'^TestTime:\s*(.*)' regex_recommend = r'^Requires:\s*(.*)' regex_require = r'^RhtsRequires:\s*(.*)' rec_separator = None else: regex_task = r'name=\s*(.*)\n' regex_summary = r'description=\s*(.*)\n' regex_test = r'entry_point=\s*(.*)$' regex_contact = r'owner=\s*(.*)' regex_duration = r'max_time=\s*(.*)' regex_require = r'dependencies=\s*(.*)' regex_recommend = r'softDependencies=\s*(.*)' rec_separator = ';' if testinfo is None: testinfo = datafile # Beaker task name try: beaker_task = re.search(regex_task, testinfo).group(1) echo(style('task: ', fg='green') + beaker_task) data['extra-task'] = beaker_task data['extra-summary'] = beaker_task except AttributeError: raise ConvertError("Unable to parse 'Name' from testinfo.desc.") # Summary try: data['summary'] = re.search(regex_summary, testinfo, re.M).group(1) echo(style('summary: ', fg='green') + data['summary']) except AttributeError: pass # Test script try: data['test'] = re.search(regex_test, datafile, re.M).group(1) if filename == 'metadata': data['test'] = data['test'].split()[-1] echo(style('test: ', fg='green') + data['test']) except AttributeError: raise ConvertError("Makefile is missing the 'run' target.") # Detect framework try: test_path = os.path.join(path, data["test"]) with open(test_path, encoding="utf-8") as test_file: if re.search("beakerlib", test_file.read()): data["framework"] = "beakerlib" else: data["framework"] = "shell" echo(style("framework: ", fg="green") + data["framework"]) except IOError: raise ConvertError("Unable to open '{0}'.".format(test_path)) # Contact try: data['contact'] = re.search(regex_contact, testinfo, re.M).group(1) echo(style('contact: ', fg='green') + data['contact']) except AttributeError: pass if filename == 'Makefile': # Component try: data['component'] = re.search(r'^RunFor:\s*(.*)', testinfo, re.M).group(1).split() echo( style('component: ', fg='green') + ' '.join(data['component'])) except AttributeError: pass # Duration try: data['duration'] = re.search(regex_duration, testinfo, re.M).group(1) echo(style('duration: ', fg='green') + data['duration']) except AttributeError: pass if filename == 'Makefile': # Environment variables = re.findall(r'^Environment:\s*(.*)', testinfo, re.M) if variables: data['environment'] = {} for variable in variables: key, value = variable.split('=', maxsplit=1) data['environment'][key] = value echo(style('environment:', fg='green')) echo(pprint.pformat(data['environment'])) # RhtsRequires or repoRequires (optional) goes to require requires = re.findall(regex_require, testinfo, re.M) if requires: data['require'] = [ require for line in requires for require in line.split(rec_separator) ] echo(style('require: ', fg='green') + ' '.join(data['require'])) # Requires or softDependencies (optional) goes to recommend recommends = re.findall(regex_recommend, testinfo, re.M) if recommends: data['recommend'] = [ recommend for line in recommends for recommend in line.split(rec_separator) ] echo(style('recommend: ', fg='green') + ' '.join(data['recommend'])) if filename == 'Makefile': # Convert Type into tags try: makefile_type = re.search(r'^Type:\s*(.*)', testinfo, re.M).group(1) if 'all' in [type_.lower() for type_ in types]: tags = makefile_type.split() else: tags = [ type_ for type_ in types if type_.lower() in makefile_type.lower().split() ] if tags: echo(style("tag: ", fg="green") + " ".join(tags)) data["tag"] = tags except AttributeError: pass # Add relevant bugs to the 'link' attribute for bug_line in re.findall(r'^Bug:\s*([0-9\s]+)', testinfo, re.M): for bug in re.findall(r'(\d+)', bug_line): add_bug(bug, data) return beaker_task, data