def init_available_relationships(app): """ Update directive option_spec with custom attributes defined in configuration file ``traceability_attributes`` variable. Update directive option_spec with custom relationships defined in configuration file ``traceability_relationships`` variable. Both keys (relationships) and values (reverse relationships) are added. This handler should be called upon builder initialization, before processing any directive. Function also passes relationships to traceability collection. """ env = app.builder.env for attr in app.config.traceability_attributes.keys(): ItemDirective.option_spec[attr] = directives.unchanged ItemListDirective.option_spec[attr] = directives.unchanged TraceableItem.define_attribute( attr, app.config.traceability_attributes[attr]) for rel in list(app.config.traceability_relationships.keys()): revrel = app.config.traceability_relationships[rel] env.traceability_collection.add_relation_pair(rel, revrel) ItemDirective.option_spec[rel] = directives.unchanged if revrel: ItemDirective.option_spec[revrel] = directives.unchanged
def add_relation(self, sourceid, relation, targetid): ''' Add relation between two items The function adds the forward and the automatic reverse relation. Args: sourceid (str): ID of the source item relation (str): Relation between source and target item targetid (str): ID of the target item ''' # Add placeholder if source item is unknown if sourceid not in self.items: src = TraceableItem(sourceid, True) self.add_item(src) source = self.items[sourceid] # Error if relation is unknown if relation not in self.relations: raise TraceabilityException( 'Relation {name} not known'.format(name=relation), source.get_document()) # Add forward relation source.add_target(relation, targetid) # When reverse relation exists, continue to create/adapt target-item reverse_relation = self.get_reverse_relation(relation) if reverse_relation: # Add placeholder if target item is unknown if targetid not in self.items: tgt = TraceableItem(targetid, True) self.add_item(tgt) # Add reverse relation to target-item self.items[targetid].add_target(reverse_relation, sourceid, implicit=True)
def test_get_info_from_relationship_str(self, _): """ Tests dut.get_info_from_relationship with a config_for_parent parameter as str """ relationship_to_parent = 'depends_on' alternative_parent = TraceableItem('ZZZ-TO_BE_IGNORED') # not to be prioritized over MEETING-12345_2 (natural sorting) self.coll.add_relation('ACTION-12345_ACTION_1', 'depends_on', alternative_parent.id) action1 = self.coll.get_item('ACTION-12345_ACTION_1') attendees, jira_field = dut.get_info_from_relationship(action1, relationship_to_parent, self.coll) self.assertEqual(attendees, ['ABC', ' ZZZ']) self.assertEqual(jira_field, 'MEETING-12345_2: Action 1\'s caption?')
def test_get_info_from_relationship_tuple(self, _): """ Tests dut.get_info_from_relationship with a config_for_parent parameter as tuple """ relationship_to_parent = ('depends_on', r'ZZZ-[\w_]+') alternative_parent = TraceableItem('ZZZ-TO_BE_PRIORITIZED') # to be prioritized over MEETING-12345_2 self.coll.add_relation('ACTION-12345_ACTION_1', 'depends_on', alternative_parent.id) action1 = self.coll.get_item('ACTION-12345_ACTION_1') attendees, jira_field = dut.get_info_from_relationship(action1, relationship_to_parent, self.coll) self.assertEqual(attendees, []) self.assertEqual(jira_field, 'ZZZ-TO_BE_PRIORITIZED: Action 1\'s caption?')
def test_tuple_for_relationship_to_parent(self, jira): """ Tests that the linked item, added in this test case, is selected by configured tuple for ``relationship_to_parent`` """ self.settings['relationship_to_parent'] = ('depends_on', r'ZZZ-[\w_]+') alternative_parent = TraceableItem('ZZZ-TO_BE_PRIORITIZED') # to be prioritized over MEETING-12345_2 self.coll.add_relation('ACTION-12345_ACTION_1', 'depends_on', alternative_parent.id) jira_mock = jira.return_value jira_mock.search_issues.return_value = [] with self.assertLogs(level=WARNING) as cm: warning('Dummy log') dut.create_jira_issues(self.settings, self.coll) self.assertEqual( cm.output, ['WARNING:root:Dummy log'] ) self.assertEqual(jira_mock.search_issues.call_args_list, [ mock.call("project=MLX12345 and summary ~ " '"ZZZ\\\\-TO_BE_PRIORITIZED\\\\: Action 1\'s caption\\\\?"'), mock.call("project=MLX12345 and summary ~ 'Caption for action 2'"), ]) self.assertEqual( jira_mock.create_issue.call_args_list, [ mock.call( summary='ZZZ-TO_BE_PRIORITIZED: Action 1\'s caption?', description='Description for action 1', assignee={'name': 'ABC'}, **self.general_fields ), mock.call( summary='Caption for action 2', description='Caption for action 2', assignee={'name': 'ZZZ'}, **self.general_fields ), ])
def run(self): env = self.state.document.settings.env app = env.app caption = '' targetid = self.arguments[0] targetnode = nodes.target('', '', ids=[targetid]) itemnode = Item('') itemnode['id'] = targetid # Item caption is the text following the mandatory id argument. # Caption should be considered a line of text. Remove line breaks. if len(self.arguments) > 1: caption = self.arguments[1].replace('\n', ' ') # Store item info item = TraceableItem(targetid) item.set_document(env.docname, self.lineno) item.bind_node(targetnode) item.set_caption(caption) item.set_content('\n'.join(self.content)) try: env.traceability_collection.add_item(item) except TraceabilityException as err: report_warning(env, err, env.docname, self.lineno) # Add found attributes to item. Attribute data is a single string. for attribute in app.config.traceability_attributes.keys(): if attribute in self.options: try: item.add_attribute(attribute, self.options[attribute]) except TraceabilityException as err: report_warning(env, err, env.docname, self.lineno) # Add found relationships to item. All relationship data is a string of # item ids separated by space. It is splitted in a list of item ids for rel in env.traceability_collection.iter_relations(): if rel in self.options: related_ids = self.options[rel].split() for related_id in related_ids: try: env.traceability_collection.add_relation( targetid, rel, related_id) except TraceabilityException as err: report_warning(env, err, env.docname, self.lineno) # Custom callback for modifying items if app.config.traceability_callback_per_item: app.config.traceability_callback_per_item( targetid, env.traceability_collection) # Output content of item to document template = [] for line in self.content: template.append(' ' + line) self.state_machine.insert_input( template, self.state_machine.document.attributes['source']) # Check nocaptions flag if 'nocaptions' in self.options: itemnode['nocaptions'] = True elif app.config.traceability_item_no_captions: itemnode['nocaptions'] = True else: itemnode['nocaptions'] = False return [targetnode, itemnode]
def setUp(self): self.general_fields = { 'components': [ {'name': '[SW]'}, {'name': '[HW]'}, ], 'issuetype': {'name': 'Task'}, 'project': 'MLX12345', } self.settings = { 'api_endpoint': 'https://jira.atlassian.com/rest/api/latest/', 'username': '******', 'password': '******', 'jira_field_id': 'summary', 'issue_type': 'Task', 'item_to_ticket_regex': r'ACTION-12345_ACTION_\d+', 'project_key_regex': r'ACTION-(?P<project>\d{5})_', 'project_key_prefix': 'MLX', 'default_project': 'SWCC', 'warn_if_exists': True, 'relationship_to_parent': 'depends_on', 'components': '[SW],[HW]', 'catch_errors': False, 'notify_watchers': False, } self.coll = TraceableCollection() parent = TraceableItem('MEETING-12345_2') action1 = TraceableItem('ACTION-12345_ACTION_1') action1.caption = 'Action 1\'s caption?' action1.set_content('Description for action 1') action2 = TraceableItem('ACTION-12345_ACTION_2') action2.caption = 'Caption for action 2' action2.set_content('') action3 = TraceableItem('ACTION-98765_ACTION_55') item1 = TraceableItem('ITEM-12345_1') effort_attr = TraceableAttribute('effort', r'^([\d\.]+(mo|[wdhm]) ?)+$') assignee_attr = TraceableAttribute('assignee', '^.*$') attendees_attr = TraceableAttribute('attendees', '^([A-Z]{3}[, ]*)+$') TraceableItem.define_attribute(effort_attr) TraceableItem.define_attribute(assignee_attr) TraceableItem.define_attribute(attendees_attr) parent.add_attribute('attendees', 'ABC, ZZZ') action1.add_attribute('effort', '2w 3d 4h 55m') action1.add_attribute('assignee', 'ABC') action2.add_attribute('assignee', 'ZZZ') action3.add_attribute('assignee', 'ABC') for item in (parent, action1, action2, action3, item1): self.coll.add_item(item) self.coll.add_relation_pair('depends_on', 'impacts_on') self.coll.add_relation(action1.id, 'impacts_on', item1.id) # to be ignored self.coll.add_relation(action1.id, 'depends_on', parent.id) # to be taken into account self.coll.add_relation(action2.id, 'impacts_on', parent.id) # to be ignored