class StagingWorkflow(object): def __init__(self, project=PROJECT): """ Initialize the configuration """ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) oscrc = os.path.join(THIS_DIR, 'test.oscrc') # set to None so we return the destructor early in case of exceptions self.api = None self.apiurl = APIURL self.project = project self.projects = {} self.requests = [] self.groups = [] self.users = [] logging.basicConfig() # clear cache from other tests - otherwise the VCR is replayed depending # on test order, which can be harmful memoize_session_reset() osc.core.conf.get_config(override_conffile=oscrc, override_no_keyring=True, override_no_gnome_keyring=True) os.environ['OSC_CONFIG'] = oscrc if os.environ.get('OSC_DEBUG'): osc.core.conf.config['debug'] = 1 CacheManager.test = True # disable caching, the TTLs break any reproduciblity Cache.CACHE_DIR = None Cache.PATTERNS = {} Cache.init() self.setup_remote_config() self.load_config() self.api = StagingAPI(APIURL, project) def load_config(self, project=None): if project is None: project = self.project self.config = Config(APIURL, project) def create_attribute_type(self, namespace, name, values=None): meta = """ <namespace name='{}'> <modifiable_by user='******'/> </namespace>""".format(namespace) url = osc.core.makeurl(APIURL, ['attribute', namespace, '_meta']) osc.core.http_PUT(url, data=meta) meta = "<definition name='{}' namespace='{}'><description/>".format( name, namespace) if values: meta += "<count>{}</count>".format(values) meta += "<modifiable_by role='maintainer'/></definition>" url = osc.core.makeurl(APIURL, ['attribute', namespace, name, '_meta']) osc.core.http_PUT(url, data=meta) def setup_remote_config(self): self.create_target() self.create_attribute_type('OSRT', 'Config', 1) config = { 'overridden-by-local': 'remote-nope', 'staging-group': 'factory-staging', 'remote-only': 'remote-indeed', } self.remote_config_set(config, replace_all=True) def remote_config_set(self, config, replace_all=False): if not replace_all: config_existing = Config.get(self.apiurl, self.project) config_existing.update(config) config = config_existing config_lines = [] for key, value in config.items(): config_lines.append(f'{key} = {value}') attribute_value_save(APIURL, self.project, 'Config', '\n'.join(config_lines)) def create_group(self, name, users=[]): meta = """ <group> <title>{}</title> </group> """.format(name) if len(users): root = ET.fromstring(meta) persons = ET.SubElement(root, 'person') for user in users: ET.SubElement(persons, 'person', {'userid': user}) meta = ET.tostring(root) if not name in self.groups: self.groups.append(name) url = osc.core.makeurl(APIURL, ['group', name]) osc.core.http_PUT(url, data=meta) def create_user(self, name): if name in self.users: return meta = """ <person> <login>{}</login> <email>{}@example.com</email> <state>confirmed</state> </person> """.format(name, name) self.users.append(name) url = osc.core.makeurl(APIURL, ['person', name]) osc.core.http_PUT(url, data=meta) url = osc.core.makeurl(APIURL, ['person', name], {'cmd': 'change_password'}) osc.core.http_POST(url, data='opensuse') home_project = 'home:' + name self.projects[home_project] = Project(home_project, create=False) def create_target(self): if self.projects.get('target'): return self.create_user('staging-bot') self.create_group('factory-staging', users=['staging-bot']) p = Project(name=self.project, reviewer={'groups': ['factory-staging']}) self.projects['target'] = p self.projects[self.project] = p url = osc.core.makeurl(APIURL, ['staging', self.project, 'workflow']) data = "<workflow managers='factory-staging'/>" osc.core.http_POST(url, data=data) # creates A and B as well self.projects['staging:A'] = Project(self.project + ':Staging:A', create=False) self.projects['staging:B'] = Project(self.project + ':Staging:B', create=False) def setup_rings(self): self.create_target() self.projects['ring0'] = Project(name=self.project + ':Rings:0-Bootstrap') self.projects['ring1'] = Project(name=self.project + ':Rings:1-MinimalX') target_wine = Package(name='wine', project=self.projects['target']) target_wine.create_commit() self.create_link(target_wine, self.projects['ring1']) def create_package(self, project, package): project = self.create_project(project) return Package(name=package, project=project) def create_link(self, source_package, target_project, target_package=None): if not target_package: target_package = source_package.name target_package = Package(name=target_package, project=target_project) url = self.api.makeurl( ['source', target_project.name, target_package.name, '_link']) osc.core.http_PUT(url, data='<link project="{}" package="{}"/>'.format( source_package.project.name, source_package.name)) return target_package def create_project(self, name, reviewer={}, maintainer={}, project_links=[]): if isinstance(name, Project): return name if name in self.projects: return self.projects[name] self.projects[name] = Project(name, reviewer=reviewer, maintainer=maintainer, project_links=project_links) return self.projects[name] def submit_package(self, package=None, project=None): if not project: project = self.project request = Request(source_package=package, target_project=project) self.requests.append(request) return request def request_package_delete(self, package, project=None): if not project: project = package.project request = Request(target_package=package, target_project=project, type='delete') self.requests.append(request) return request def create_submit_request(self, project, package, text=None): project = self.create_project(project) package = Package(name=package, project=project) package.create_commit(text=text) return self.submit_package(package) def create_staging(self, suffix, freeze=False, rings=None, with_repo=False): staging_key = 'staging:{}'.format(suffix) # do not reattach if already present if not staging_key in self.projects: staging_name = self.project + ':Staging:' + suffix staging = Project(staging_name, create=False, with_repo=with_repo) url = osc.core.makeurl( APIURL, ['staging', self.project, 'staging_projects']) data = '<workflow><staging_project>{}</staging_project></workflow>' osc.core.http_POST(url, data=data.format(staging_name)) self.projects[staging_key] = staging else: staging = self.projects[staging_key] project_links = [] if rings == 0: project_links.append(self.project + ":Rings:0-Bootstrap") if rings == 1 or rings == 0: project_links.append(self.project + ":Rings:1-MinimalX") staging.update_meta(project_links=project_links, maintainer={'groups': ['factory-staging']}, with_repo=with_repo) if freeze: FreezeCommand(self.api).perform(staging.name) return staging def __del__(self): if not self.api: return try: self.remove() except: # normally exceptions in destructors are ignored but a info # message is displayed. Make this a little more useful by # printing it into the capture log traceback.print_exc(None, sys.stdout) def remove(self): print('deleting staging workflow') for project in self.projects.values(): project.remove() for request in self.requests: request.revoke() for group in self.groups: url = osc.core.makeurl(APIURL, ['group', group]) try: osc.core.http_DELETE(url) except HTTPError: pass print('done') if hasattr(self.api, '_invalidate_all'): self.api._invalidate_all()
class TestApiCalls(unittest.TestCase): """ Tests for various api calls to ensure we return expected content """ def setUp(self): """ Initialize the configuration """ self.obs = OBS() Config('openSUSE:Factory') self.api = StagingAPI(APIURL, 'openSUSE:Factory') def tearDown(self): """Clean internal cache""" if hasattr(self.api, '_invalidate_all'): self.api._invalidate_all() def test_ring_packages(self): """ Validate the creation of the rings. """ # our content in the XML files # test content for listonly ie. list command ring_packages = { 'elem-ring-0': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-1': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-mini': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-2': 'openSUSE:Factory:Rings:2-TestDVD', 'git': 'openSUSE:Factory:Rings:2-TestDVD', 'wine': 'openSUSE:Factory:Rings:1-MinimalX', } self.assertEqual(ring_packages, self.api.ring_packages_for_links) # test content for real usage ring_packages = { 'elem-ring-0': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-1': 'openSUSE:Factory:Rings:1-MinimalX', 'elem-ring-mini': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-2': 'openSUSE:Factory:Rings:2-TestDVD', 'git': 'openSUSE:Factory:Rings:2-TestDVD', 'wine': 'openSUSE:Factory:Rings:1-MinimalX', } self.assertEqual(ring_packages, self.api.ring_packages) @unittest.skip("no longer approving non-ring packages") def test_dispatch_open_requests(self): """ Test dispatching and closure of non-ring packages """ # Get rid of open requests self.api.dispatch_open_requests() # Check that we tried to close it self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'changereviewstate']) # Try it again self.api.dispatch_open_requests() # This time there should be nothing to close self.assertEqual(httpretty.last_request().method, 'GET') def test_pseudometa_get_prj(self): """ Test getting project metadata from YAML in project description """ # Try to get data from project that has no metadata data = self.api.get_prj_pseudometa('openSUSE:Factory:Staging:A') # Should be empty, but contain structure to work with self.assertEqual(data, {'requests': []}) # Add some sample data rq = {'id': '123', 'package': 'test-package'} data['requests'].append(rq) # Save them and read them back self.api.set_prj_pseudometa('openSUSE:Factory:Staging:A', data) test_data = self.api.get_prj_pseudometa('openSUSE:Factory:Staging:A') # Verify that we got back the same data self.assertEqual(data, test_data) def test_list_projects(self): """ List projects and their content """ # Prepare expected results data = [] for prj in self.obs.staging_project: data.append('openSUSE:Factory:Staging:' + prj) # Compare the results self.assertEqual(data, self.api.get_staging_projects()) def test_open_requests(self): """ Test searching for open requests """ requests = [] # get the open requests requests = self.api.get_open_requests() # Compare the results, we only care now that we got 1 of them not the content self.assertEqual(1, len(requests)) def test_get_package_information(self): """ Test if we get proper project, name and revision from the staging informations """ package_info = { 'dir_srcmd5': '751efeae52d6c99de48164088a33d855', 'project': 'home:Admin', 'rev': '7b98ac01b8071d63a402fa99dc79331c', 'srcmd5': '7b98ac01b8071d63a402fa99dc79331c', 'package': 'wine' } # Compare the results, we only care now that we got 2 of them not the content self.assertEqual( package_info, self.api.get_package_information('openSUSE:Factory:Staging:B', 'wine')) def test_request_id_package_mapping(self): """ Test whether we can get correct id for sr in staging project """ prj = 'openSUSE:Factory:Staging:B' # Get rq num = self.api.get_request_id_for_package(prj, 'wine') self.assertEqual(333, num) # Get package name self.assertEqual('wine', self.api.get_package_for_request_id(prj, num)) def test_rm_from_prj(self): prj = 'openSUSE:Factory:Staging:B' pkg = 'wine' full_name = prj + '/' + pkg # Verify package is there self.assertTrue(full_name in self.obs.links) # Get rq number num = self.api.get_request_id_for_package(prj, pkg) # Delete the package self.api.rm_from_prj(prj, package='wine') # Verify package is not there self.assertTrue(full_name not in self.obs.links) # RQ is gone self.assertEqual(None, self.api.get_request_id_for_package(prj, pkg)) self.assertEqual(None, self.api.get_package_for_request_id(prj, num)) # Verify that review is closed self.assertEqual('accepted', self.obs.requests[str(num)]['review']) self.assertEqual('new', self.obs.requests[str(num)]['request']) def test_rm_from_prj_2(self): # Try the same with request number prj = 'openSUSE:Factory:Staging:B' pkg = 'wine' full_name = prj + '/' + pkg # Get rq number num = self.api.get_request_id_for_package(prj, pkg) # Delete the package self.api.rm_from_prj(prj, request_id=num) # Verify package is not there self.assertTrue(full_name not in self.obs.links) # RQ is gone self.assertEqual(None, self.api.get_request_id_for_package(prj, pkg)) self.assertEqual(None, self.api.get_package_for_request_id(prj, num)) # Verify that review is closed self.assertEqual('accepted', self.obs.requests[str(num)]['review']) self.assertEqual('new', self.obs.requests[str(num)]['request']) def test_add_sr(self): prj = 'openSUSE:Factory:Staging:A' rq = '123' # Running it twice shouldn't change anything for i in range(2): # Add rq to the project self.api.rq_to_prj(rq, prj) # Verify that review is there self.assertEqual('new', self.obs.requests[rq]['review']) self.assertEqual('review', self.obs.requests[rq]['request']) self.assertEqual(self.api.get_prj_pseudometa('openSUSE:Factory:Staging:A'), {'requests': [{'id': 123, 'package': 'gcc', 'author': 'Admin'}]}) def test_create_package_container(self): """Test if the uploaded _meta is correct.""" self.api.create_package_container('openSUSE:Factory:Staging:B', 'wine') self.assertEqual(httpretty.last_request().method, 'PUT') self.assertEqual(httpretty.last_request().body, '<package name="wine"><title/><description/></package>') self.assertEqual(httpretty.last_request().path, '/source/openSUSE:Factory:Staging:B/wine/_meta') self.api.create_package_container('openSUSE:Factory:Staging:B', 'wine', disable_build=True) self.assertEqual(httpretty.last_request().method, 'PUT') self.assertEqual(httpretty.last_request().body, '<package name="wine"><title /><description /><build><disable /></build></package>') self.assertEqual(httpretty.last_request().path, '/source/openSUSE:Factory:Staging:B/wine/_meta') def test_review_handling(self): """Test whether accepting/creating reviews behaves correctly.""" # Add review self.api.add_review('123', by_project='openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'addreview']) # Try to readd, should do anything self.api.add_review('123', by_project='openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'GET') # Accept review self.api.set_review('123', 'openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'changereviewstate']) # Try to accept it again should do anything self.api.set_review('123', 'openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'GET') # But we should be able to reopen it self.api.add_review('123', by_project='openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'addreview']) def test_prj_from_letter(self): # Verify it works self.assertEqual(self.api.prj_from_letter('openSUSE:Factory'), 'openSUSE:Factory') self.assertEqual(self.api.prj_from_letter('A'), 'openSUSE:Factory:Staging:A') def test_frozen_mtime(self): """Test frozen mtime.""" # Testing frozen mtime self.assertTrue(self.api.days_since_last_freeze('openSUSE:Factory:Staging:A') > 8) self.assertTrue(self.api.days_since_last_freeze('openSUSE:Factory:Staging:B') < 1) # U == unfrozen self.assertTrue(self.api.days_since_last_freeze('openSUSE:Factory:Staging:U') > 1000) def test_frozen_enough(self): """Test frozen enough.""" # Testing frozen mtime self.assertEqual(self.api.prj_frozen_enough('openSUSE:Factory:Staging:B'), True) self.assertEqual(self.api.prj_frozen_enough('openSUSE:Factory:Staging:A'), False) # U == unfrozen self.assertEqual(self.api.prj_frozen_enough('openSUSE:Factory:Staging:U'), False) def test_move(self): """Test package movement.""" init_data = self.api.get_package_information('openSUSE:Factory:Staging:B', 'wine') self.api.move_between_project('openSUSE:Factory:Staging:B', 333, 'openSUSE:Factory:Staging:A') test_data = self.api.get_package_information('openSUSE:Factory:Staging:A', 'wine') self.assertEqual(init_data, test_data)
class TestApiCalls(unittest.TestCase): """ Tests for various api calls to ensure we return expected content """ def setUp(self): """ Initialize the configuration """ self.obs = OBS() Config('openSUSE:Factory') self.api = StagingAPI(APIURL, 'openSUSE:Factory') def tearDown(self): """Clean internal cache""" if hasattr(self.api, '_invalidate_all'): self.api._invalidate_all() def test_ring_packages(self): """ Validate the creation of the rings. """ # our content in the XML files # test content for listonly ie. list command ring_packages = { 'apparmor': 'openSUSE:Factory:Rings:1-MinimalX', 'elem-ring-0': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-1': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-mini': 'openSUSE:Factory:Rings:0-Bootstrap', 'wine': 'openSUSE:Factory:Rings:1-MinimalX', } self.assertEqual(ring_packages, self.api.ring_packages_for_links) # test content for real usage ring_packages = { 'apparmor': 'openSUSE:Factory:Rings:1-MinimalX', 'elem-ring-0': 'openSUSE:Factory:Rings:0-Bootstrap', 'elem-ring-1': 'openSUSE:Factory:Rings:1-MinimalX', 'elem-ring-mini': 'openSUSE:Factory:Rings:0-Bootstrap', 'wine': 'openSUSE:Factory:Rings:1-MinimalX', } self.assertEqual(ring_packages, self.api.ring_packages) @unittest.skip("no longer approving non-ring packages") def test_dispatch_open_requests(self): """ Test dispatching and closure of non-ring packages """ # Get rid of open requests self.api.dispatch_open_requests() # Check that we tried to close it self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'changereviewstate']) # Try it again self.api.dispatch_open_requests() # This time there should be nothing to close self.assertEqual(httpretty.last_request().method, 'GET') def test_pseudometa_get_prj(self): """ Test getting project metadata from YAML in project description """ # Try to get data from project that has no metadata data = self.api.get_prj_pseudometa('openSUSE:Factory:Staging:A') # Should be empty, but contain structure to work with self.assertEqual(data, {'requests': []}) # Add some sample data rq = {'id': '123', 'package': 'test-package'} data['requests'].append(rq) # Save them and read them back self.api.set_prj_pseudometa('openSUSE:Factory:Staging:A', data) test_data = self.api.get_prj_pseudometa('openSUSE:Factory:Staging:A') # Verify that we got back the same data self.assertEqual(data, test_data) def test_list_projects(self): """ List projects and their content """ # Prepare expected results data = [] for prj in self.obs.staging_project: data.append('openSUSE:Factory:Staging:' + prj) # Compare the results self.assertEqual(data, self.api.get_staging_projects()) def test_open_requests(self): """ Test searching for open requests """ requests = [] # get the open requests requests = self.api.get_open_requests() # Compare the results, we only care now that we got 1 of them not the content self.assertEqual(1, len(requests)) def test_get_package_information(self): """ Test if we get proper project, name and revision from the staging informations """ package_info = { 'dir_srcmd5': '751efeae52d6c99de48164088a33d855', 'project': 'home:Admin', 'rev': '7b98ac01b8071d63a402fa99dc79331c', 'srcmd5': '7b98ac01b8071d63a402fa99dc79331c', 'package': 'wine' } # Compare the results, we only care now that we got 2 of them not the content self.assertEqual( package_info, self.api.get_package_information('openSUSE:Factory:Staging:B', 'wine')) def test_request_id_package_mapping(self): """ Test whether we can get correct id for sr in staging project """ prj = 'openSUSE:Factory:Staging:B' # Get rq num = self.api.get_request_id_for_package(prj, 'wine') self.assertEqual(333, num) # Get package name self.assertEqual('wine', self.api.get_package_for_request_id(prj, num)) def test_rm_from_prj(self): prj = 'openSUSE:Factory:Staging:B' pkg = 'wine' full_name = prj + '/' + pkg # Verify package is there self.assertTrue(full_name in self.obs.links) # Get rq number num = self.api.get_request_id_for_package(prj, pkg) # Delete the package self.api.rm_from_prj(prj, package='wine') # Verify package is not there self.assertTrue(full_name not in self.obs.links) # RQ is gone self.assertEqual(None, self.api.get_request_id_for_package(prj, pkg)) self.assertEqual(None, self.api.get_package_for_request_id(prj, num)) # Verify that review is closed self.assertEqual('accepted', self.obs.requests[str(num)]['review']) self.assertEqual('new', self.obs.requests[str(num)]['request']) def test_rm_from_prj_2(self): # Try the same with request number prj = 'openSUSE:Factory:Staging:B' pkg = 'wine' full_name = prj + '/' + pkg # Get rq number num = self.api.get_request_id_for_package(prj, pkg) # Delete the package self.api.rm_from_prj(prj, request_id=num) # Verify package is not there self.assertTrue(full_name not in self.obs.links) # RQ is gone self.assertEqual(None, self.api.get_request_id_for_package(prj, pkg)) self.assertEqual(None, self.api.get_package_for_request_id(prj, num)) # Verify that review is closed self.assertEqual('accepted', self.obs.requests[str(num)]['review']) self.assertEqual('new', self.obs.requests[str(num)]['request']) def test_add_sr(self): prj = 'openSUSE:Factory:Staging:A' rq = '123' # Running it twice shouldn't change anything for i in range(2): # Add rq to the project self.api.rq_to_prj(rq, prj) # Verify that review is there self.assertEqual('new', self.obs.requests[rq]['review']) self.assertEqual('review', self.obs.requests[rq]['request']) self.assertEqual(self.api.get_prj_pseudometa('openSUSE:Factory:Staging:A'), {'requests': [{'id': 123, 'package': 'gcc', 'author': 'Admin', 'type': 'submit'}]}) def test_create_package_container(self): """Test if the uploaded _meta is correct.""" self.api.create_package_container('openSUSE:Factory:Staging:B', 'wine') self.assertEqual(httpretty.last_request().method, 'PUT') self.assertEqual(httpretty.last_request().body, '<package name="wine"><title/><description/></package>') self.assertEqual(httpretty.last_request().path, '/source/openSUSE:Factory:Staging:B/wine/_meta') self.api.create_package_container('openSUSE:Factory:Staging:B', 'wine', disable_build=True) self.assertEqual(httpretty.last_request().method, 'PUT') self.assertEqual(httpretty.last_request().body, '<package name="wine"><title/><description/><build><disable/></build></package>') self.assertEqual(httpretty.last_request().path, '/source/openSUSE:Factory:Staging:B/wine/_meta') def test_review_handling(self): """Test whether accepting/creating reviews behaves correctly.""" # Add review self.api.add_review('123', by_project='openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'addreview']) # Try to readd, should do anything self.api.add_review('123', by_project='openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'GET') # Accept review self.api.set_review('123', 'openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'changereviewstate']) # Try to accept it again should do anything self.api.set_review('123', 'openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'GET') # But we should be able to reopen it self.api.add_review('123', by_project='openSUSE:Factory:Staging:A') self.assertEqual(httpretty.last_request().method, 'POST') self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'addreview']) def test_prj_from_letter(self): # Verify it works self.assertEqual(self.api.prj_from_letter('openSUSE:Factory'), 'openSUSE:Factory') self.assertEqual(self.api.prj_from_letter('A'), 'openSUSE:Factory:Staging:A') def test_frozen_mtime(self): """Test frozen mtime.""" # Testing frozen mtime self.assertTrue(self.api.days_since_last_freeze('openSUSE:Factory:Staging:A') > 8) self.assertTrue(self.api.days_since_last_freeze('openSUSE:Factory:Staging:B') < 1) # U == unfrozen self.assertTrue(self.api.days_since_last_freeze('openSUSE:Factory:Staging:U') > 1000) def test_frozen_enough(self): """Test frozen enough.""" # Testing frozen mtime self.assertEqual(self.api.prj_frozen_enough('openSUSE:Factory:Staging:B'), True) self.assertEqual(self.api.prj_frozen_enough('openSUSE:Factory:Staging:A'), False) # U == unfrozen self.assertEqual(self.api.prj_frozen_enough('openSUSE:Factory:Staging:U'), False) def test_move(self): """Test package movement.""" init_data = self.api.get_package_information('openSUSE:Factory:Staging:B', 'wine') self.api.move_between_project('openSUSE:Factory:Staging:B', 333, 'openSUSE:Factory:Staging:A') test_data = self.api.get_package_information('openSUSE:Factory:Staging:A', 'wine') self.assertEqual(init_data, test_data)
class StagingWorkflow(ABC): """This abstract base class is intended to setup and manipulate the environment (projects, users, etc.) in the local OBS instance used to tests the release tools. Thus, the derivative classes make easy to setup scenarios similar to the ones used during the real (open)SUSE development. """ def __init__(self, project=PROJECT): """Initializes the configuration Note this constructor calls :func:`create_target`, which implies several projects and users are created right away. :param project: default target project :type project: str """ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) oscrc = os.path.join(THIS_DIR, 'test.oscrc') # set to None so we return the destructor early in case of exceptions self.api = None self.apiurl = APIURL self.project = project self.projects = {} self.requests = [] self.groups = [] self.users = [] self.attr_types = {} logging.basicConfig() # clear cache from other tests - otherwise the VCR is replayed depending # on test order, which can be harmful memoize_session_reset() osc.core.conf.get_config(override_conffile=oscrc, override_no_keyring=True, override_no_gnome_keyring=True) os.environ['OSC_CONFIG'] = oscrc if os.environ.get('OSC_DEBUG'): osc.core.conf.config['debug'] = 1 CacheManager.test = True # disable caching, the TTLs break any reproduciblity Cache.CACHE_DIR = None Cache.PATTERNS = {} Cache.init() # Note this implicitly calls create_target() self.setup_remote_config() self.load_config() self.api = StagingAPI(APIURL, project) @abstractmethod def initial_config(self): """Values to use to initialize the 'Config' attribute at :func:`setup_remote_config`""" pass @abstractmethod def staging_group_name(self): """Name of the group in charge of the staging workflow""" pass def load_config(self, project=None): """Loads the corresponding :class:`osclib.Config` object into the attribute ``config`` Such an object represents the set of values stored on the attribute 'Config' of the target project. See :func:`remote_config_set`. :param project: target project name :type project: str """ if project is None: project = self.project self.config = Config(APIURL, project) def create_attribute_type(self, namespace, name, values=None): """Creates a new attribute type in the OBS instance.""" if namespace not in self.attr_types: self.attr_types[namespace] = [] if name not in self.attr_types[namespace]: self.attr_types[namespace].append(name) meta = """ <namespace name='{}'> <modifiable_by user='******'/> </namespace>""".format(namespace) url = osc.core.makeurl(APIURL, ['attribute', namespace, '_meta']) osc.core.http_PUT(url, data=meta) meta = "<definition name='{}' namespace='{}'><description/>".format( name, namespace) if values: meta += "<count>{}</count>".format(values) meta += "<modifiable_by role='maintainer'/></definition>" url = osc.core.makeurl(APIURL, ['attribute', namespace, name, '_meta']) osc.core.http_PUT(url, data=meta) def setup_remote_config(self): """Creates the attribute 'Config' for the target project, with proper initial content. See :func:`remote_config_set` for more information about that attribute. Note this calls :func:`create_target` to ensure the target project exists. """ # First ensure the existence of both the target project and the 'Config' attribute type self.create_target() self.create_attribute_type('OSRT', 'Config', 1) self.remote_config_set(self.initial_config(), replace_all=True) def remote_config_set(self, config, replace_all=False): """Sets the values of the 'Config' attribute for the target project. That attribute stores a set of values that are useful to influence the behavior of several tools and bots in the context of the given project. For convenience, such a collection of values is usually accessed using a :class:`osclib.Config` object. See :func:`load_config`. :param config: values to write into the attribute :type config: dict[str, str] :param replace_all: whether the previous content of 'Config' should be cleared up :type replace_all: bool """ if not replace_all: config_existing = Config.get(self.apiurl, self.project) config_existing.update(config) config = config_existing config_lines = [] for key, value in config.items(): config_lines.append(f'{key} = {value}') attribute_value_save(APIURL, self.project, 'Config', '\n'.join(config_lines)) def create_group(self, name, users=[]): """Creates a group and assigns users to it. If the group already exists then it just updates it users. :param name: name of group :type name: str :param users: list of users to be in group :type users: list(str) """ meta = """ <group> <title>{}</title> </group> """.format(name) if len(users): root = ET.fromstring(meta) persons = ET.SubElement(root, 'person') for user in users: ET.SubElement(persons, 'person', {'userid': user}) meta = ET.tostring(root) if name not in self.groups: self.groups.append(name) url = osc.core.makeurl(APIURL, ['group', name]) osc.core.http_PUT(url, data=meta) def create_user(self, name): """Creates a user and their home project. Do nothing if the user already exists. Password is always "opensuse". The home project is not really created in the OBS instance, but :func:`Project.update_meta` can be used to create it. :param name: name of the user :type name: str """ if name in self.users: return meta = """ <person> <login>{}</login> <email>{}@example.com</email> <state>confirmed</state> </person> """.format(name, name) self.users.append(name) url = osc.core.makeurl(APIURL, ['person', name]) osc.core.http_PUT(url, data=meta) url = osc.core.makeurl(APIURL, ['person', name], {'cmd': 'change_password'}) osc.core.http_POST(url, data='opensuse') home_project = 'home:' + name self.projects[home_project] = Project(home_project, create=False) def create_target(self): """Creates the main project that represents the product being developed and, as such, is expected to be the target for requests. It also creates all the associated projects, users and groups involved in the development workflow. In the base implementation, that includes: - The target project (see :func:`create_target_project`) - A group of staging managers including the "staging-bot" user (see :func:`create_staging_users`) - A couple of staging projects for the target one - The ProductVersion attribute type, that is used by the staging tools After the execution, the target project is indexed in the projects dictionary twice, by its name and as 'target'. """ if self.projects.get('target'): return self.create_target_project() self.create_staging_users() self.projects['staging:A'] = Project(self.project + ':Staging:A', create=False) self.projects['staging:B'] = Project(self.project + ':Staging:B', create=False) # The ProductVersion is required for some actions, like accepting a staging project self.create_attribute_type('OSRT', 'ProductVersion', 1) def create_package(self, project, package): project = self.create_project(project) return Package(name=package, project=project) def create_link(self, source_package, target_project, target_package=None): if not target_package: target_package = source_package.name target_package = Package(name=target_package, project=target_project) url = self.api.makeurl( ['source', target_project.name, target_package.name, '_link']) osc.core.http_PUT(url, data='<link project="{}" package="{}"/>'.format( source_package.project.name, source_package.name)) return target_package def create_project(self, name, reviewer={}, maintainer={}, project_links=[]): """Creates project if it does not already exist. For params see the constructor of :class:`Project` :return: the project instance representing the given project :rtype: Project """ if isinstance(name, Project): return name if name in self.projects: return self.projects[name] self.projects[name] = Project(name, reviewer=reviewer, maintainer=maintainer, project_links=project_links) return self.projects[name] def submit_package(self, package, project=None): """Creates submit request from package to target project. Both have to exist (Use :func:`create_submit_request` otherwise). :param package: package to submit :type package: Package :param project: project where to send submit request, None means use the default. :type project: Project or str or None :return: created request. :rtype: Request """ if not project: project = self.project request = Request(source_package=package, target_project=project) self.requests.append(request) return request def request_package_delete(self, package, project=None): if not project: project = package.project request = Request(target_package=package, target_project=project, type='delete') self.requests.append(request) return request def create_submit_request(self, project, package, text=None, add_commit=True): """Creates submit request from package in specified project to default project. It creates project if not exist and also package. Package is commited with optional text. Note different parameters than submit_package. :param project: project where package will live :type project: Project or str :param package: package name to create :type package: str :param text: commit message for initial package creation :type text: str :param add_commit: whether add initial package commit. Useful to disable if package already exists :type add_commit: bool :return: created request. :rtype: Request """ project = self.create_project(project) package = Package(name=package, project=project) if add_commit: package.create_commit(text=text) return self.submit_package(package) def __del__(self): if not self.api: return try: self.remove() except: # normally exceptions in destructors are ignored but a info # message is displayed. Make this a little more useful by # printing it into the capture log traceback.print_exc(None, sys.stdout) def remove(self): print('deleting staging workflow') for project in self.projects.values(): project.remove() for request in self.requests: request.revoke() for group in self.groups: self.remove_group(group) for namespace in self.attr_types: self.remove_attribute_types(namespace) print('done') if hasattr(self.api, '_invalidate_all'): self.api._invalidate_all() def remove_group(self, group): """Removes a group from the OBS instance :param group: name of the group to remove :type group: str """ print('deleting group', group) url = osc.core.makeurl(APIURL, ['group', group]) self._safe_delete(url) def remove_attribute_types(self, namespace): """Removes an attributes namespace and all the attribute types it contains :param namespace: attributes namespace to remove :type namespace: str """ for name in self.attr_types[namespace]: print('deleting attribute type {}:{}'.format(namespace, name)) url = osc.core.makeurl(APIURL, ['attribute', namespace, name, '_meta']) self._safe_delete(url) print('deleting namespace', namespace) url = osc.core.makeurl(APIURL, ['attribute', namespace, '_meta']) self._safe_delete(url) def _safe_delete(self, url): """Performs a delete request to the OBS instance, ignoring possible http errors :param url: url to use for the http delete request :type url: str """ try: osc.core.http_DELETE(url) except HTTPError: pass def create_target_project(self): """Creates the main target project (see :func:`create_target`)""" p = Project(name=self.project) self.projects['target'] = p self.projects[self.project] = p def create_staging_users(self): """Creates users and groups for the staging workflow for the target project (see :func:`create_target`) """ group = self.staging_group_name() self.create_user('staging-bot') self.create_group(group, users=['staging-bot']) self.projects['target'].add_reviewers(groups=[group]) url = osc.core.makeurl(APIURL, ['staging', self.project, 'workflow']) data = f"<workflow managers='{group}'/>" osc.core.http_POST(url, data=data)
class TestApiCalls(unittest.TestCase): """ Tests for various api calls to ensure we return expected content """ def setUp(self): """ Initialize the configuration """ self.obs = OBS() Config("openSUSE:Factory") self.api = StagingAPI(APIURL, "openSUSE:Factory") def tearDown(self): """Clean internal cache""" self.api._invalidate_all() def test_ring_packages(self): """ Validate the creation of the rings. """ # our content in the XML files ring_packages = { "elem-ring-0": "openSUSE:Factory:Rings:0-Bootstrap", "elem-ring-1": "openSUSE:Factory:Rings:1-MinimalX", "elem-ring-2": "openSUSE:Factory:Rings:2-TestDVD", "git": "openSUSE:Factory:Rings:2-TestDVD", "wine": "openSUSE:Factory:Rings:1-MinimalX", } self.assertEqual(ring_packages, self.api.ring_packages) @unittest.skip("no longer approving non-ring packages") def test_dispatch_open_requests(self): """ Test dispatching and closure of non-ring packages """ # Get rid of open requests self.api.dispatch_open_requests() # Check that we tried to close it self.assertEqual(httpretty.last_request().method, "POST") self.assertEqual(httpretty.last_request().querystring[u"cmd"], [u"changereviewstate"]) # Try it again self.api.dispatch_open_requests() # This time there should be nothing to close self.assertEqual(httpretty.last_request().method, "GET") def test_pseudometa_get_prj(self): """ Test getting project metadata from YAML in project description """ # Try to get data from project that has no metadata data = self.api.get_prj_pseudometa("openSUSE:Factory:Staging:A") # Should be empty, but contain structure to work with self.assertEqual(data, {"requests": []}) # Add some sample data rq = {"id": "123", "package": "test-package"} data["requests"].append(rq) # Save them and read them back self.api.set_prj_pseudometa("openSUSE:Factory:Staging:A", data) test_data = self.api.get_prj_pseudometa("openSUSE:Factory:Staging:A") # Verify that we got back the same data self.assertEqual(data, test_data) def test_list_projects(self): """ List projects and their content """ # Prepare expected results data = [] for prj in self.obs.staging_project: data.append("openSUSE:Factory:Staging:" + prj) # Compare the results self.assertEqual(data, self.api.get_staging_projects()) def test_open_requests(self): """ Test searching for open requests """ requests = [] # get the open requests requests = self.api.get_open_requests() # Compare the results, we only care now that we got 1 of them not the content self.assertEqual(1, len(requests)) def test_get_package_information(self): """ Test if we get proper project, name and revision from the staging informations """ package_info = { "dir_srcmd5": "751efeae52d6c99de48164088a33d855", "project": "home:Admin", "rev": "7b98ac01b8071d63a402fa99dc79331c", "srcmd5": "7b98ac01b8071d63a402fa99dc79331c", "package": "wine", } # Compare the results, we only care now that we got 2 of them not the content self.assertEqual(package_info, self.api.get_package_information("openSUSE:Factory:Staging:B", "wine")) def test_request_id_package_mapping(self): """ Test whether we can get correct id for sr in staging project """ prj = "openSUSE:Factory:Staging:B" # Get rq num = self.api.get_request_id_for_package(prj, "wine") self.assertEqual(333, num) # Get package name self.assertEqual("wine", self.api.get_package_for_request_id(prj, num)) def test_rm_from_prj(self): prj = "openSUSE:Factory:Staging:B" pkg = "wine" full_name = prj + "/" + pkg # Verify package is there self.assertTrue(full_name in self.obs.links) # Get rq number num = self.api.get_request_id_for_package(prj, pkg) # Delete the package self.api.rm_from_prj(prj, package="wine") # Verify package is not there self.assertTrue(full_name not in self.obs.links) # RQ is gone self.assertEqual(None, self.api.get_request_id_for_package(prj, pkg)) self.assertEqual(None, self.api.get_package_for_request_id(prj, num)) # Verify that review is closed self.assertEqual("accepted", self.obs.requests[str(num)]["review"]) self.assertEqual("new", self.obs.requests[str(num)]["request"]) def test_rm_from_prj_2(self): # Try the same with request number prj = "openSUSE:Factory:Staging:B" pkg = "wine" full_name = prj + "/" + pkg # Get rq number num = self.api.get_request_id_for_package(prj, pkg) # Delete the package self.api.rm_from_prj(prj, request_id=num) # Verify package is not there self.assertTrue(full_name not in self.obs.links) # RQ is gone self.assertEqual(None, self.api.get_request_id_for_package(prj, pkg)) self.assertEqual(None, self.api.get_package_for_request_id(prj, num)) # Verify that review is closed self.assertEqual("accepted", self.obs.requests[str(num)]["review"]) self.assertEqual("new", self.obs.requests[str(num)]["request"]) def test_add_sr(self): prj = "openSUSE:Factory:Staging:A" rq = "123" # Running it twice shouldn't change anything for i in range(2): # Add rq to the project self.api.rq_to_prj(rq, prj) # Verify that review is there self.assertEqual("new", self.obs.requests[rq]["review"]) self.assertEqual("review", self.obs.requests[rq]["request"]) self.assertEqual( self.api.get_prj_pseudometa("openSUSE:Factory:Staging:A"), {"requests": [{"id": 123, "package": "gcc", "author": "Admin"}]}, ) def test_create_package_container(self): """Test if the uploaded _meta is correct.""" self.api.create_package_container("openSUSE:Factory:Staging:B", "wine") self.assertEqual(httpretty.last_request().method, "PUT") self.assertEqual(httpretty.last_request().body, '<package name="wine"><title/><description/></package>') self.assertEqual(httpretty.last_request().path, "/source/openSUSE:Factory:Staging:B/wine/_meta") self.api.create_package_container("openSUSE:Factory:Staging:B", "wine", disable_build=True) self.assertEqual(httpretty.last_request().method, "PUT") self.assertEqual( httpretty.last_request().body, '<package name="wine"><title /><description /><build><disable /></build></package>', ) self.assertEqual(httpretty.last_request().path, "/source/openSUSE:Factory:Staging:B/wine/_meta") def test_review_handling(self): """Test whether accepting/creating reviews behaves correctly.""" # Add review self.api.add_review("123", by_project="openSUSE:Factory:Staging:A") self.assertEqual(httpretty.last_request().method, "POST") self.assertEqual(httpretty.last_request().querystring[u"cmd"], [u"addreview"]) # Try to readd, should do anything self.api.add_review("123", by_project="openSUSE:Factory:Staging:A") self.assertEqual(httpretty.last_request().method, "GET") # Accept review self.api.set_review("123", "openSUSE:Factory:Staging:A") self.assertEqual(httpretty.last_request().method, "POST") self.assertEqual(httpretty.last_request().querystring[u"cmd"], [u"changereviewstate"]) # Try to accept it again should do anything self.api.set_review("123", "openSUSE:Factory:Staging:A") self.assertEqual(httpretty.last_request().method, "GET") # But we should be able to reopen it self.api.add_review("123", by_project="openSUSE:Factory:Staging:A") self.assertEqual(httpretty.last_request().method, "POST") self.assertEqual(httpretty.last_request().querystring[u"cmd"], [u"addreview"]) def test_prj_from_letter(self): # Verify it works self.assertEqual(self.api.prj_from_letter("openSUSE:Factory"), "openSUSE:Factory") self.assertEqual(self.api.prj_from_letter("A"), "openSUSE:Factory:Staging:A") def test_frozen_mtime(self): """Test frozen mtime.""" # Testing frozen mtime self.assertTrue(self.api.days_since_last_freeze("openSUSE:Factory:Staging:A") > 8) self.assertTrue(self.api.days_since_last_freeze("openSUSE:Factory:Staging:B") < 1) # U == unfrozen self.assertTrue(self.api.days_since_last_freeze("openSUSE:Factory:Staging:U") > 1000) def test_frozen_enough(self): """Test frozen enough.""" # Testing frozen mtime self.assertEqual(self.api.prj_frozen_enough("openSUSE:Factory:Staging:B"), True) self.assertEqual(self.api.prj_frozen_enough("openSUSE:Factory:Staging:A"), False) # U == unfrozen self.assertEqual(self.api.prj_frozen_enough("openSUSE:Factory:Staging:U"), False) def test_move(self): """Test package movement.""" init_data = self.api.get_package_information("openSUSE:Factory:Staging:B", "wine") self.api.move_between_project("openSUSE:Factory:Staging:B", 333, "openSUSE:Factory:Staging:A") test_data = self.api.get_package_information("openSUSE:Factory:Staging:A", "wine") self.assertEqual(init_data, test_data)
class StagingWorkflow(object): def __init__(self, project=PROJECT): """ Initialize the configuration """ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) oscrc = os.path.join(THIS_DIR, 'test.oscrc') self.apiurl = APIURL logging.basicConfig() # clear cache from other tests - otherwise the VCR is replayed depending # on test order, which can be harmful memoize_session_reset() osc.core.conf.get_config(override_conffile=oscrc, override_no_keyring=True, override_no_gnome_keyring=True) if os.environ.get('OSC_DEBUG'): osc.core.conf.config['debug'] = 1 self.project = project self.projects = {} self.requests = [] self.groups = [] self.users = [] CacheManager.test = True # disable caching, the TTLs break any reproduciblity Cache.CACHE_DIR = None Cache.PATTERNS = {} Cache.init() self.setup_remote_config() self.load_config() self.api = StagingAPI(APIURL, project) def load_config(self, project=None): if project is None: project = self.project self.config = Config(APIURL, project) def create_attribute_type(self, namespace, name, values=None): meta=""" <namespace name='{}'> <modifiable_by user='******'/> </namespace>""".format(namespace) url = osc.core.makeurl(APIURL, ['attribute', namespace, '_meta']) osc.core.http_PUT(url, data=meta) meta="<definition name='{}' namespace='{}'><description/>".format(name, namespace) if values: meta += "<count>{}</count>".format(values) meta += "<modifiable_by role='maintainer'/></definition>" url = osc.core.makeurl(APIURL, ['attribute', namespace, name, '_meta']) osc.core.http_PUT(url, data=meta) def setup_remote_config(self): self.create_target() self.create_attribute_type('OSRT', 'Config', 1) attribute_value_save(APIURL, self.project, 'Config', 'overridden-by-local = remote-nope\n' 'remote-only = remote-indeed\n') def create_group(self, name, users=[]): meta = """ <group> <title>{}</title> </group> """.format(name) if len(users): root = ET.fromstring(meta) persons = ET.SubElement(root, 'person') for user in users: ET.SubElement(persons, 'person', { 'userid': user } ) meta = ET.tostring(root) if not name in self.groups: self.groups.append(name) url = osc.core.makeurl(APIURL, ['group', name]) osc.core.http_PUT(url, data=meta) def create_user(self, name): if name in self.users: return meta = """ <person> <login>{}</login> <email>{}@example.com</email> <state>confirmed</state> </person> """.format(name, name) self.users.append(name) url = osc.core.makeurl(APIURL, ['person', name]) osc.core.http_PUT(url, data=meta) url = osc.core.makeurl(APIURL, ['person', name], {'cmd': 'change_password'}) osc.core.http_POST(url, data='opensuse') home_project = 'home:' + name self.projects[home_project] = Project(home_project, create=False) def create_target(self): if self.projects.get('target'): return self.create_group('factory-staging') self.projects['target'] = Project(name=self.project, reviewer={'groups': ['factory-staging']}) def setup_rings(self): self.create_target() self.projects['ring0'] = Project(name=self.project + ':Rings:0-Bootstrap') self.projects['ring1'] = Project(name=self.project + ':Rings:1-MinimalX') target_wine = Package(name='wine', project=self.projects['target']) target_wine.create_commit() self.create_link(target_wine, self.projects['ring1']) def create_package(self, project, package): project = self.create_project(project) return Package(name=package, project=project) def create_link(self, source_package, target_project, target_package=None): if not target_package: target_package = source_package.name target_package = Package(name=target_package, project=target_project) url = self.api.makeurl(['source', target_project.name, target_package.name, '_link']) osc.core.http_PUT(url, data='<link project="{}" package="{}"/>'.format(source_package.project.name, source_package.name)) return target_package def create_project(self, name, reviewer={}, maintainer={}, project_links=[]): if isinstance(name, Project): return name if name in self.projects: return self.projects[name] self.projects[name] = Project(name, reviewer=reviewer, maintainer=maintainer, project_links=project_links) return self.projects[name] def submit_package(self, package=None): request = Request(source_package=package, target_project=self.project) self.requests.append(request) return request def create_submit_request(self, project, package, text=None): project = self.create_project(project) package = Package(name=package, project=project) package.create_commit(text=text) return self.submit_package(package) def create_staging(self, suffix, freeze=False, rings=None): project_links = [] if rings == 0: project_links.append(self.project + ":Rings:0-Bootstrap") if rings == 1 or rings == 0: project_links.append(self.project + ":Rings:1-MinimalX") staging = Project(self.project + ':Staging:' + suffix, project_links=project_links) if freeze: FreezeCommand(self.api).perform(staging.name) self.projects['staging:{}'.format(suffix)] = staging return staging def __del__(self): try: self.remove() except: # normally exceptions in destructors are ignored but a info # message is displayed. Make this a little more useful by # printing it into the capture log traceback.print_exc(None, sys.stdout) def remove(self): print('deleting staging workflow') for project in self.projects.values(): project.remove() for request in self.requests: request.revoke() for group in self.groups: url = osc.core.makeurl(APIURL, ['group', group]) try: osc.core.http_DELETE(url) except HTTPError: pass print('done') if hasattr(self.api, '_invalidate_all'): self.api._invalidate_all()