From 6ad78dd0622488a0acadd2fac0b114b8289e6f0c Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Sep 10 2019 14:48:13 +0000 Subject: [PATCH 1/4] python: fix run_tests.sh wrapper --- diff --git a/python/run_tests.sh b/python/run_tests.sh index 8295422..e8e42fe 100755 --- a/python/run_tests.sh +++ b/python/run_tests.sh @@ -1,4 +1,6 @@ #!/bin/bash -#python -B -m pytest --cov-report term-missing --cov ./copr_cli/ tests $@ -PYTHONPATH=.:$PYTHONPATH python -B -m pytest --cov-report term-missing --cov ./copr/client_v2/ -s $@ +absdir="$(dirname "$(readlink -f "$0")")" +export PYTHONPATH="$absdir" + +python3 -B -m pytest --cov-report term-missing copr/test "$@" From 580a24308c8450581b55efec5e2305888a021a07 Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Sep 10 2019 14:48:13 +0000 Subject: [PATCH 2/4] frontend: add new template macro owner_url This simplifies and de-duplicates the repo_file_href and copr_url macros. Note that for /repo/ routes we can not use copr_url, because that is adding unwanted `coprname=` argument, which is not accepted by that particular route and leads to additional query string. --- diff --git a/frontend/coprs_frontend/coprs/templates/_helpers.html b/frontend/coprs_frontend/coprs/templates/_helpers.html index 965dc48..37f3ae8 100644 --- a/frontend/coprs_frontend/coprs/templates/_helpers.html +++ b/frontend/coprs_frontend/coprs/templates/_helpers.html @@ -242,6 +242,24 @@ {% endmacro %} +{%- macro owner_url(view, owner) %} + {#- Given the owner object (user or group) generate proper URL for view + + Note that if you wan't to use this method for routes which _accept_ "coprname" + argument, you wan't to use `copr_url` below. + + Usage: + owner_url('coprs_ns.foo', groupX) + owner_url('coprs_ns.foo', user1, arg1='bar', arg2='baz') + #} + {%- if owner.at_name %} + {{- url_for(view, group_name=owner.name, **kwargs)|fix_url_https_frontend }} + {%- else %} + {{- url_for(view, username=owner.name, **kwargs)|fix_url_https_frontend }} + {%- endif %} +{% endmacro %} + + {%- macro copr_url(view, copr) %} {#- Examine given copr and generate proper URL for the `view` @@ -250,17 +268,13 @@ Usage: copr_url('coprs_ns.foo', copr) - copr_url('coprs_ns.foo', copr, arg1='bar', arg2='baz) + copr_url('coprs_ns.foo', copr, arg1='bar', arg2='baz') #} - {%- if not copr.is_a_group_project %} - {{- url_for(view, username=copr.user.name, coprname=copr.name, **kwargs) }} - {%- else %} - {{- url_for(view, group_name=copr.group.name, coprname=copr.name, **kwargs) }} - {%- endif %} + {{- owner_url(view, copr.owner, coprname=copr.name, **kwargs) }} {%- endmacro %} -{%- macro owner_url(copr) %} +{%- macro copr_owner_url(copr) %} {% if copr.is_a_group_project %} {{- url_for('groups_ns.list_projects_by_group', group_name=copr.group.name) }} {% else %} @@ -432,23 +446,12 @@ https://admin.fedoraproject.org/accounts/group/view/{{name}} {% macro repo_file_href(copr, repo) %} - {% if copr.is_a_group_project: %} - {{- url_for('coprs_ns.generate_repo_file', - group_name=copr.group.name, - copr_dirname=copr.main_dir.name, - name_release=repo.name_release, - repofile=repo.repo_file, - _external=True - )|fix_url_https_frontend -}} - {% else %} - {{- url_for('coprs_ns.generate_repo_file', - username=copr.user.name, - copr_dirname=copr.main_dir.name, - name_release=repo.name_release, - repofile=repo.repo_file, - _external=True - )|fix_url_https_frontend -}} - {% endif %} + {{- owner_url('coprs_ns.generate_repo_file', + copr.owner, + copr_dirname=copr.main_dir.name, + name_release=repo.name_release, + repofile=repo.repo_file, + _external=True) -}} {% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/forks.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/forks.html index 6c6139c..6bc2de1 100644 --- a/frontend/coprs_frontend/coprs/templates/coprs/detail/forks.html +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/forks.html @@ -1,5 +1,5 @@ {% extends "coprs/detail.html" %} -{% from "_helpers.html" import copr_name, owner_url, initialize_datatables %} +{% from "_helpers.html" import copr_name, copr_owner_url, initialize_datatables %} {% block title %}Forks of {{ copr_name(copr) }}{% endblock %} {% set selected_tab = "forks" %} @@ -27,7 +27,7 @@ {% for fork in copr.forks %} - {{ fork.owner_name }} + {{ fork.owner_name }} {{ fork.name }} From 74ac5e758d5ef75dcf929aa4c5bf5124cc1bdeb4 Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Sep 10 2019 14:48:13 +0000 Subject: [PATCH 3/4] frontend: add new decorator @req_with_copr_dir This simplifies `generate_repo_file` route, and will help us in future with adding more routes. --- diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py index 774b058..1cb92ed 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py @@ -36,7 +36,8 @@ from coprs.mail import send_mail, LegalFlagMessage, PermissionRequestMessage, Pe from coprs.logic.complex_logic import ComplexLogic -from coprs.views.misc import login_required, page_not_found, req_with_copr, req_with_copr, generic_error +from coprs.views.misc import (login_required, page_not_found, req_with_copr, + generic_error, req_with_copr_dir) from coprs.views.coprs_ns import coprs_ns @@ -702,12 +703,10 @@ def process_legal_flag(copr): @coprs_ns.route("///repo//") @coprs_ns.route("/g///repo//", defaults={"repofile": None}) @coprs_ns.route("/g///repo//") -def generate_repo_file(copr_dirname, name_release, repofile, username=None, group_name=None): +@req_with_copr_dir +def generate_repo_file(copr_dir, name_release, repofile): """ Generate repo file for a given repo name. Reponame = username-coprname """ - - ownername = username if username else ('@'+group_name) - copr_dir = ComplexLogic.get_copr_dir_safe(ownername, copr_dirname) return render_generate_repo_file(copr_dir, name_release) diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py index 7de8441..3e08c81 100644 --- a/frontend/coprs_frontend/coprs/views/misc.py +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -353,6 +353,19 @@ def req_with_copr(f): return wrapper +def req_with_copr_dir(f): + @wraps(f) + def wrapper(**kwargs): + if "group_name" in kwargs: + ownername = '@' + kwargs.pop("group_name") + else: + ownername = kwargs.pop("username") + copr_dirname = kwargs.pop("copr_dirname") + copr_dir = ComplexLogic.get_copr_dir_safe(ownername, copr_dirname) + return f(copr_dir, **kwargs) + return wrapper + + def send_build_icon(build): if not build: return send_file("static/status_images/unknown.png", From 44bda6410c45c8596c8fb8969b253327086b68c3 Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Sep 10 2019 14:48:46 +0000 Subject: [PATCH 4/4] frontend, cli, python: support multilib projects We create a new flask route for generating arch-specific repofiles. Using this, and using the new per-project knob Project.multilib, frontend can easily determine whether multilib repofile should be generated or not. For now, if the project is configured for multilib, we don't do any magic on backend. We simply provide repofile with two (32bit and 64bit) repositories (the 32bit one contains `:ml` suffix in repoid). And then - if user does 'dnf copr enable' against multilib project - both repositories are available on the system. This trivial approach can be pretty easily enhanced in future, especially if we wanted to support similar approach for per-package multilib detection (as e.g. pungi has), we'd only have to drop the second - multilib - variant of repo from repofile. The fact that we are adding a new routenow, and that we have the `:ml` suffix in repoid - we need to update the dnf-plugins-core package as well [1]. But for compat with previous versions of dnf-plugins-core we keep the old routes as well. [1] https://github.com/rpm-software-management/dnf-plugins-core/pull/350 Fixes: #1 Merges: #938 --- diff --git a/cli/copr_cli/main.py b/cli/copr_cli/main.py index 79878b3..d08b4ee 100644 --- a/cli/copr_cli/main.py +++ b/cli/copr_cli/main.py @@ -358,6 +358,7 @@ class Commands(object): auto_prune=ON_OFF_MAP[args.auto_prune], use_bootstrap_container=ON_OFF_MAP[args.use_bootstrap_container], delete_after_days=args.delete_after_days, + multilib=ON_OFF_MAP[args.multilib], ) print("New project was successfully created.") @@ -379,6 +380,7 @@ class Commands(object): use_bootstrap_container=ON_OFF_MAP[args.use_bootstrap_container], chroots=args.chroots, delete_after_days=args.delete_after_days, + multilib=ON_OFF_MAP[args.multilib], ) @requires_api_auth @@ -817,6 +819,13 @@ def setup_parser(): help="If mock bootstrap container is used to initialize the buildroot.") parser_create.add_argument("--delete-after-days", default=None, metavar='DAYS', help="Delete the project after the specfied period of time") + parser_create.add_argument( + "--multilib", choices=["on", "off"], default="off", + help=("When users enable this copr repository on 64bit variant of " + "multilib capable architecture (e.g. x86_64), they will also be " + "able to install 32bit variants of the packages (e.g. i386 for " + "x86_64 arch), default is 'off'")) + parser_create.set_defaults(func="action_create") # create the parser for the "modify_project" command @@ -846,6 +855,12 @@ def setup_parser(): help=("Delete the project after the specfied " "period of time, empty or -1 disables, " "(default is \"don't change\")")) + parser_modify.add_argument( + "--multilib", choices=["on", "off"], + help=("When users enable this copr repository on 64bit variant of " + "multilib capable architecture (e.g. x86_64), they will also be " + "able to install 32bit variants of the packages (e.g. i386 for " + "x86_64 arch), default is \"don't change\"")) parser_modify.set_defaults(func="action_modify_project") # create the parser for the "delete" command diff --git a/cli/tests/test_cli.py b/cli/tests/test_cli.py index 5eb7b0d..2a3204f 100644 --- a/cli/tests/test_cli.py +++ b/cli/tests/test_cli.py @@ -375,6 +375,37 @@ def test_create_project(config_from_file, project_proxy_add, capsys): "unlisted_on_hp": None, "devel_mode": None, "enable_net": False, "use_bootstrap_container": None, "delete_after_days": None, + "multilib": False, + } + assert stdout == "New project was successfully created.\n" + +@mock.patch('copr.v3.proxies.project.ProjectProxy.add') +@mock.patch('copr_cli.main.config_from_file', return_value=mock_config) +def test_create_multilib_project(config_from_file, project_proxy_add, capsys): + main.main(argv=[ + "create", "foo", + '--multilib', 'on', + "--chroot", "fedora-rawhide-x86_64", + "--chroot", "fedora-rawhide-i386", + "--instructions", "instruction string", + "--repo", "repo1", "--repo", "repo2", + "--initial-pkgs", "pkg1", + ]) + stdout, stderr = capsys.readouterr() + + project_proxy_add.assert_called_once() + args, kwargs = project_proxy_add.call_args + assert kwargs == { + "auto_prune": True, + "ownername": None, "persistent": False, "projectname": "foo", + "description": None, + "instructions": "instruction string", + "chroots": ["fedora-rawhide-x86_64", "fedora-rawhide-i386"], + "additional_repos": ["repo1", "repo2"], + "unlisted_on_hp": None, "devel_mode": None, "enable_net": False, + "use_bootstrap_container": None, + "delete_after_days": None, + "multilib": True, } assert stdout == "New project was successfully created.\n" diff --git a/frontend/coprs_frontend/alembic/schema/versions/0dbdd06fb850_multilib_knob_on_copr_public.py b/frontend/coprs_frontend/alembic/schema/versions/0dbdd06fb850_multilib_knob_on_copr_public.py new file mode 100644 index 0000000..cbdd57d --- /dev/null +++ b/frontend/coprs_frontend/alembic/schema/versions/0dbdd06fb850_multilib_knob_on_copr_public.py @@ -0,0 +1,20 @@ +""" +multilib knob on copr_public + +Revision ID: 0dbdd06fb850 +Revises: 12abab545d7a +Create Date: 2019-08-20 22:41:50.747899 +""" + +import sqlalchemy as sa +from alembic import op + + +revision = '0dbdd06fb850' +down_revision = '12abab545d7a' + +def upgrade(): + op.add_column('copr', sa.Column('multilib', sa.Boolean(), server_default='0', nullable=False)) + +def downgrade(): + op.drop_column('copr', 'multilib') diff --git a/frontend/coprs_frontend/coprs/forms.py b/frontend/coprs_frontend/coprs/forms.py index 9e8f8d9..3c03c15 100644 --- a/frontend/coprs_frontend/coprs/forms.py +++ b/frontend/coprs_frontend/coprs/forms.py @@ -330,6 +330,15 @@ class CoprFormFactory(object): default=True, false_values=FALSE_VALUES) + multilib = wtforms.BooleanField( + "Multilib support", + description="""When users enable this copr repository on + 64bit variant of multilib capable architecture (e.g. + x86_64), they will be able to install 32bit variants of the + packages (e.g. i386 for x86_64 arch)""", + default=False, + false_values=FALSE_VALUES) + # Deprecated, use `enable_net` instead build_enable_net = wtforms.BooleanField( "Enable internet access during builds", @@ -1061,6 +1070,7 @@ class CoprModifyForm(FlaskForm): # Deprecated, use `enable_net` instead build_enable_net = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) enable_net = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) + multilib = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) class CoprForkFormFactory(object): diff --git a/frontend/coprs_frontend/coprs/helpers.py b/frontend/coprs_frontend/coprs/helpers.py index 49bd9f9..083ad8f 100644 --- a/frontend/coprs_frontend/coprs/helpers.py +++ b/frontend/coprs_frontend/coprs/helpers.py @@ -189,7 +189,7 @@ def parse_package_name(pkg): return pkg -def generate_repo_url(mock_chroot, url): +def generate_repo_url(mock_chroot, url, arch=None): """ Generates url with build results for .repo file. No checks if copr or mock_chroot exists. """ @@ -208,7 +208,7 @@ def generate_repo_url(mock_chroot, url): url = posixpath.join( url, "{0}-{1}-{2}/".format(mock_chroot.os_release, - os_version, "$basearch")) + os_version, arch or '$basearch')) return url diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py index 42d7542..c68a61e 100644 --- a/frontend/coprs_frontend/coprs/models.py +++ b/frontend/coprs_frontend/coprs/models.py @@ -274,6 +274,8 @@ class _CoprPublic(db.Model, helpers.Serializer, CoprSearchRelatedData): # temporary project if non-null delete_after = db.Column(db.DateTime, index=True, nullable=True) + multilib = db.Column(db.Boolean, default=False, nullable=False, server_default="0") + class _CoprPrivate(db.Model, helpers.Serializer): """ @@ -372,6 +374,28 @@ class Copr(db.Model, helpers.Serializer): return filter(lambda x: x.is_active, self.mock_chroots) @property + def active_multilib_chroots(self): + """ + Return list of active mock_chroots which have the 32bit multilib + counterpart. + """ + chroot_names = [chroot.name for chroot in self.active_chroots] + + found_chroots = [] + for chroot in self.active_chroots: + if chroot.arch not in MockChroot.multilib_pairs: + continue + + counterpart = "{}-{}-{}".format(chroot.os_release, + chroot.os_version, + MockChroot.multilib_pairs[chroot.arch]) + if counterpart in chroot_names: + found_chroots.append(chroot) + + return found_chroots + + + @property def active_copr_chroots(self): """ :rtype: list of CoprChroot @@ -1185,6 +1209,10 @@ class MockChroot(db.Model, helpers.Serializer): comment = db.Column(db.Text, nullable=True) + multilib_pairs = { + 'x86_64': 'i386', + } + @classmethod def latest_fedora_branched_chroot(cls, arch='x86_64'): return (cls.query diff --git a/frontend/coprs_frontend/coprs/templates/_helpers.html b/frontend/coprs_frontend/coprs/templates/_helpers.html index 37f3ae8..97f0c4e 100644 --- a/frontend/coprs_frontend/coprs/templates/_helpers.html +++ b/frontend/coprs_frontend/coprs/templates/_helpers.html @@ -445,13 +445,23 @@ https://admin.fedoraproject.org/accounts/group/view/{{name}} {%- endmacro -%} -{% macro repo_file_href(copr, repo) %} +{% macro repo_file_href(copr, repo, arch=None) %} +{%- if not arch %} {{- owner_url('coprs_ns.generate_repo_file', copr.owner, copr_dirname=copr.main_dir.name, name_release=repo.name_release, repofile=repo.repo_file, _external=True) -}} +{%- else %} + {{- owner_url('coprs_ns.generate_repo_file', + copr.owner, + copr_dirname=copr.main_dir.name, + name_release=repo.name_release, + repofile=repo.repo_file, + arch=arch, + _external=True) -}} +{%- endif %} {% endmacro %} diff --git a/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html b/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html index 9ce1c83..2ecedc4 100644 --- a/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html +++ b/frontend/coprs_frontend/coprs/templates/coprs/_coprs_forms.html @@ -144,6 +144,7 @@ [form.persistent, g.user.admin], [form.use_bootstrap_container], [form.follow_fedora_branching], + [form.multilib], ])}} {{ render_field(form.delete_after_days, diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html index 82fe603..311cfff 100644 --- a/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html +++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/overview.html @@ -64,6 +64,13 @@ {{ friendly_os_name(repo.os_release, repo.os_version) }} + {% if repo.arch_repos %} + {% for arch in repo.arch_repos %} + + multilib {{ arch }}+{{ repo.arch_repos[arch] }} + + {% endfor %} + {% endif %} ({{ repo.dl_stat }} downloads) diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py index e1b037c..520e3ad 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py @@ -127,6 +127,7 @@ def add_project(ownername): contact=form.contact.data, disable_createrepo=form.disable_createrepo.data, delete_after_days=form.delete_after_days.data, + multilib=form.multilib.data, ) db.session.commit() except (DuplicateException, diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py index 1cb92ed..7f6f06d 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py @@ -189,6 +189,7 @@ def copr_new(username=None, group_name=None): use_bootstrap_container=form.use_bootstrap_container.data, follow_fedora_branching=form.follow_fedora_branching.data, delete_after_days=form.delete_after_days.data, + multilib=form.multilib.data, ) db.session.commit() @@ -297,6 +298,18 @@ def render_copr_detail(copr): else: repos_info[chroot.name_release]["arch_list"].append(chroot.arch) repos_info[chroot.name_release]["rpm_dl_stat"][chroot.arch] = chroot_rpms_dl_stat + + if copr.multilib: + for name_release in repos_info: + arches = repos_info[name_release]['arch_list'] + arch_repos = {} + for ch64, ch32 in models.MockChroot.multilib_pairs.items(): + if set([ch64, ch32]).issubset(set(arches)): + arch_repos[ch64] = ch32 + + repos_info[name_release]['arch_repos'] = arch_repos + + repos_info_list = sorted(repos_info.values(), key=lambda rec: rec["name_release"]) builds = builds_logic.BuildsLogic.get_multiple_by_copr(copr=copr).limit(1).all() @@ -457,6 +470,7 @@ def process_copr_update(copr, form): copr.use_bootstrap_container = form.use_bootstrap_container.data copr.follow_fedora_branching = form.follow_fedora_branching.data copr.delete_after_days = form.delete_after_days.data + copr.multilib = form.multilib.data if flask.g.user.admin: copr.auto_prune = form.auto_prune.data else: @@ -707,25 +721,54 @@ def process_legal_flag(copr): def generate_repo_file(copr_dir, name_release, repofile): """ Generate repo file for a given repo name. Reponame = username-coprname """ - return render_generate_repo_file(copr_dir, name_release) + + arch = flask.request.args.get('arch') + return render_generate_repo_file(copr_dir, name_release, arch) + + +def render_repo_template(copr_dir, mock_chroot, arch=None): + repo_id = "copr:{0}:{1}:{2}{3}".format( + app.config["PUBLIC_COPR_HOSTNAME"].split(":")[0], + copr_dir.copr.owner_name.replace("@", "group_"), + copr_dir.name, + ":ml" if arch else "" + ) + url = os.path.join(copr_dir.repo_url, '') # adds trailing slash + repo_url = generate_repo_url(mock_chroot, url, arch) + pubkey_url = urljoin(url, "pubkey.gpg") + return flask.render_template("coprs/copr_dir.repo", copr_dir=copr_dir, + url=repo_url, pubkey_url=pubkey_url, + repo_id=repo_id) + "\n" -def render_generate_repo_file(copr_dir, name_release): +def render_generate_repo_file(copr_dir, name_release, arch=None): name_release = app.config["CHROOT_NAME_RELEASE_ALIAS"].get(name_release, name_release) - mock_chroot = coprs_logic.MockChrootsLogic.get_from_name(name_release, noarch=True).first() + copr = copr_dir.copr + + # if the arch isn't specified, find the fist one starting with name_release + searched_chroot = name_release if not arch else name_release + "-" + arch + + mock_chroot = None + for mc in copr.active_chroots: + if not mc.name.startswith(searched_chroot): + continue + mock_chroot = mc if not mock_chroot: - raise ObjectNotFound("Chroot {} does not exist".format(name_release)) + raise ObjectNotFound("Chroot {} does not exist in {}".format( + searched_chroot, copr.full_name)) + + # normal, arch agnostic repofile + response_content = render_repo_template(copr_dir, mock_chroot) + + # append multilib counterpart repo only upon explicit request (ach != None), + # and only if the chroot actually is multilib capable + copr = copr_dir.copr + if arch and copr.multilib and mock_chroot in copr.active_multilib_chroots: + response_content += "\n" + render_repo_template(copr_dir, mock_chroot, 'i386') + + response = flask.make_response(response_content) - repo_id = "copr:{0}:{1}:{2}".format(app.config["PUBLIC_COPR_HOSTNAME"].split(":")[0], - copr_dir.copr.owner_name.replace("@", "group_"), - copr_dir.name) - url = os.path.join(copr_dir.repo_url, '') # adds trailing slash - repo_url = generate_repo_url(mock_chroot, url) - pubkey_url = urljoin(url, "pubkey.gpg") - response = flask.make_response( - flask.render_template("coprs/copr_dir.repo", copr_dir=copr_dir, url=repo_url, pubkey_url=pubkey_url, - repo_id=repo_id)) response.mimetype = "text/plain" response.headers["Content-Disposition"] = \ "filename={0}.repo".format(copr_dir.repo_name) diff --git a/frontend/coprs_frontend/tests/coprs_test_case.py b/frontend/coprs_frontend/tests/coprs_test_case.py index 4d820eb..b1425da 100644 --- a/frontend/coprs_frontend/tests/coprs_test_case.py +++ b/frontend/coprs_frontend/tests/coprs_test_case.py @@ -560,3 +560,22 @@ class TransactionDecorator(object): session["openid"] = username return fn(fn_self, *args) return decorator.decorator(wrapper, fn) + + +def new_app_context(fn): + """ + This is decorator function. Use this anytime you need to run more than one + 'self.tc.{get,post,..}()' requests in one test, or when you see something + like this in your test error output: + E sqlalchemy.orm.exc.DetachedInstanceError: Instance <..> + is not bound to a Session; attribute refresh operation cannot + proceed (Background on this error at: http://sqlalche.me/e/bhk3) + For more info see + https://stackoverflow.com/questions/19395697/sqlalchemy-session-not-getting-removed-properly-in-flask-testing + """ + @wraps(fn) + def wrapper(fn, fn_self, *args): + with coprs.app.app_context(): + return fn(fn_self, *args) + + return decorator.decorator(wrapper, fn) diff --git a/frontend/coprs_frontend/tests/test_helpers.py b/frontend/coprs_frontend/tests/test_helpers.py index c978d67..400c905 100644 --- a/frontend/coprs_frontend/tests/test_helpers.py +++ b/frontend/coprs_frontend/tests/test_helpers.py @@ -69,6 +69,12 @@ class TestHelpers(CoprsTestCase): dict(args=(m3, https_url), expected="https://example.com/path/rhel7-7.1-$basearch/")]) + test_sets.extend([ + dict(args=(m3, http_url, 'i386'), + expected="http://example.com/path/rhel7-7.1-i386/"), + dict(args=(m3, https_url, 'ppc64le'), + expected="https://example.com/path/rhel7-7.1-ppc64le/")]) + app.config["USE_HTTPS_FOR_RESULTS"] = True for test_set in test_sets: result = generate_repo_url(*test_set["args"]) diff --git a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py index 3468024..1f8307c 100644 --- a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py +++ b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py @@ -1,6 +1,7 @@ import json import flask import pytest +import re from unittest import mock @@ -12,7 +13,8 @@ from coprs import app, models from coprs.logic.coprs_logic import CoprsLogic, CoprDirsLogic from coprs.logic.actions_logic import ActionsLogic -from tests.coprs_test_case import CoprsTestCase, TransactionDecorator +from tests.coprs_test_case import (CoprsTestCase, TransactionDecorator, + new_app_context) class TestMonitor(CoprsTestCase): @@ -696,6 +698,72 @@ class TestCoprRepoGeneration(CoprsTestCase): assert b"baseurl=https://" in r.data app.config["ENFORCE_PROTOCOL_FOR_BACKEND_URL"] = orig + @new_app_context + def test_repofile_multilib(self, f_users, f_coprs, f_mock_chroots, + f_mock_chroots_many, f_custom_builds, f_db): + + r_non_ml_chroot = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-18/some.repo&arch=x86_64".format( + self.u1.name, self.c1.name)) + + for f_version in range(19, 24): + for arch in ['x86_64', 'i386']: + # with disabled multilib there's no change between fedora repos, + # no matter what the version or architecture is + r_ml_chroot = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-{2}/some.repo&arch={3}".format( + self.u1.name, self.c1.name, f_version, arch)) + assert r_ml_chroot.data == r_non_ml_chroot.data + + self.c1.multilib = True + self.db.session.commit() + + # The project is now multilib, but f18 chroot doesn't have i386 + # countepart in c1 + + r_non_ml_chroot = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-18/some.repo?arch=x86_64".format( + self.u1.name, self.c1.name)) + + r_ml_first_chroot = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-19/some.repo?arch=x86_64".format( + self.u1.name, self.c1.name)) + + for f_version in range(19, 24): + # All the Fedora 19..23 chroots have both i386 and x86_64 enabled in + # c1, so all the repofiles need to be the same. + r_ml_chroot = self.tc.get( + "/coprs/{0}/{1}/repo/fedora-{2}/some.repo?arch=x86_64".format( + self.u1.name, self.c1.name, f_version)) + assert r_ml_chroot.data == r_ml_first_chroot.data + assert r_ml_chroot.data != r_non_ml_chroot.data + + def parse_repofile(string): + lines = string.split('\n') + repoids = [x.strip('[]') for x in lines if re.match(r'^\[.*\]$', x)] + baseurls = [x.split('=')[1] for x in lines if re.match(r'^baseurl=.*', x)] + gpgkeys = [x.split('=')[1] for x in lines if re.match(r'^gpgkey=.*', x)] + return repoids, baseurls, gpgkeys + + non_ml_repofile = r_non_ml_chroot.data.decode('utf-8') + ml_repofile = r_ml_first_chroot.data.decode('utf-8') + + repoids, baseurls, gpgkeys = parse_repofile(non_ml_repofile) + assert len(repoids) == len(baseurls) == len(gpgkeys) == 1 + + normal_gpgkey = gpgkeys[0] + normal_repoid = repoids[0] + normal_baseurl = baseurls[0] + + repoids, baseurls, gpgkeys = parse_repofile(ml_repofile) + assert len(repoids) == len(baseurls) == len(gpgkeys) == 2 + + assert normal_repoid == repoids[0] + assert normal_repoid + ':ml' == repoids[1] + assert gpgkeys[0] == gpgkeys[1] == normal_gpgkey + assert normal_baseurl == baseurls[0] + assert normal_baseurl.rsplit('-', 1)[0] == baseurls[1].rsplit('-', 1)[0] + class TestSearch(CoprsTestCase): diff --git a/python/copr/v3/proxies/project.py b/python/copr/v3/proxies/project.py index add5d86..ae22ed1 100644 --- a/python/copr/v3/proxies/project.py +++ b/python/copr/v3/proxies/project.py @@ -60,7 +60,7 @@ class ProjectProxy(BaseProxy): def add(self, ownername, projectname, chroots, description=None, instructions=None, homepage=None, contact=None, additional_repos=None, unlisted_on_hp=False, enable_net=True, persistent=False, auto_prune=True, use_bootstrap_container=False, devel_mode=False, - delete_after_days=None): + delete_after_days=None, multilib=False): """ Create a project @@ -100,6 +100,7 @@ class ProjectProxy(BaseProxy): "use_bootstrap_container": use_bootstrap_container, "devel_mode": devel_mode, "delete_after_days": delete_after_days, + "multilib": multilib, } request = Request(endpoint, api_base_url=self.api_base_url, method=POST, params=params, data=data, auth=self.auth) @@ -109,7 +110,7 @@ class ProjectProxy(BaseProxy): def edit(self, ownername, projectname, chroots=None, description=None, instructions=None, homepage=None, contact=None, additional_repos=None, unlisted_on_hp=None, enable_net=None, auto_prune=None, use_bootstrap_container=None, devel_mode=None, - delete_after_days=None): + delete_after_days=None, multilib=None): """ Edit a project @@ -147,6 +148,7 @@ class ProjectProxy(BaseProxy): "use_bootstrap_container": use_bootstrap_container, "devel_mode": devel_mode, "delete_after_days": delete_after_days, + "multilib": multilib, } request = Request(endpoint, api_base_url=self.api_base_url, method=POST, params=params, data=data, auth=self.auth)