| |
@@ -3,13 +3,109 @@
|
| |
import argparse
|
| |
import os
|
| |
import subprocess as sp
|
| |
+ import time
|
| |
|
| |
|
| |
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
| |
+ TIMESTAMP = int(time.time())
|
| |
+
|
| |
+
|
| |
+ def _container_image_exist(container_name, container_type):
|
| |
+ cmd = [
|
| |
+ "podman",
|
| |
+ "image",
|
| |
+ "exists",
|
| |
+ containers[container_name][container_type],
|
| |
+ ]
|
| |
+ return _call_command(cmd)
|
| |
+
|
| |
+
|
| |
+ # fmt: off
|
| |
+ def _build_container(container_name, container_type, result_path,
|
| |
+ container_volume=None, **kwargs):
|
| |
+ # fmt: on
|
| |
+ # kwargs can be used to pass '--build-arg'
|
| |
+ build_args = []
|
| |
+ for arg in kwargs.values():
|
| |
+ build_args.append("--build-arg")
|
| |
+ build_args.append(arg)
|
| |
+
|
| |
+ volume = []
|
| |
+ if container_volume:
|
| |
+ volume.append("-v")
|
| |
+ volume.append(volume)
|
| |
+
|
| |
+ container_file = ""
|
| |
+ if container_type == "base":
|
| |
+ container_file = containers[container_name]["base"]
|
| |
+ container_name = container_file
|
| |
+ if container_type == "code":
|
| |
+ container_file = containers[container_name]["code"]
|
| |
+ container_name = container_file
|
| |
+
|
| |
+ cmd = [
|
| |
+ "podman",
|
| |
+ "build",
|
| |
+ "--no-cache",
|
| |
+ "--rm",
|
| |
+ "-t",
|
| |
+ container_name,
|
| |
+ "-f",
|
| |
+ ROOT + "/dev/containers/%s" % container_file,
|
| |
+ ROOT + "/dev/containers",
|
| |
+ ]
|
| |
+
|
| |
+ cmd += build_args
|
| |
+ cmd += volume
|
| |
+
|
| |
+ logfile = "{}/{}_{}-build.log".format(
|
| |
+ result_path, TIMESTAMP, container_type
|
| |
+ )
|
| |
+ return _call_command(cmd, logfile)
|
| |
+
|
| |
+
|
| |
+ def _call_command(cmd, logfile=None):
|
| |
+ print("Command: " + " ".join(cmd))
|
| |
+
|
| |
+ if logfile is None:
|
| |
+ rc = sp.call(cmd)
|
| |
+ else:
|
| |
+ # 'tee' like behavior, Kudos: falsetru
|
| |
+ # https://stackoverflow.com/a/31583238
|
| |
+ tee = sp.Popen(["tee", logfile], stdin=sp.PIPE)
|
| |
+ rc = sp.call(cmd, stdout=tee.stdin, stderr=sp.STDOUT)
|
| |
+ tee.stdin.close()
|
| |
+
|
| |
+ if rc != 0:
|
| |
+ return False
|
| |
+ else:
|
| |
+ return True
|
| |
+
|
| |
+
|
| |
+ def _check_pre_reqs():
|
| |
+ programs = [
|
| |
+ {"name": "podman", "cmd": ["podman", "version"]},
|
| |
+ {"name": "git", "cmd": ["git", "version"]},
|
| |
+ ]
|
| |
+
|
| |
+ # 'os.devnull' used for backward compatibility with Python2.
|
| |
+ # for Py3 only, 'sp.DEVNULL' can be used and this workaround removed.
|
| |
+ FNULL = open(os.devnull, "w")
|
| |
+
|
| |
+ missing = []
|
| |
+ for program in programs:
|
| |
+ try:
|
| |
+ sp.call(program["cmd"], stdout=FNULL, stderr=sp.STDOUT)
|
| |
+ except OSError:
|
| |
+ missing.append(program["name"])
|
| |
+
|
| |
+ if len(missing) > 0:
|
| |
+ print("Error! Required programs not found: " + ", ".join(missing))
|
| |
+ os._exit(1)
|
| |
|
| |
|
| |
def setup_parser():
|
| |
- """ Setup the cli arguments """
|
| |
+ """Setup the cli arguments"""
|
| |
parser = argparse.ArgumentParser(prog="pagure-test")
|
| |
parser.add_argument(
|
| |
"test_case", nargs="?", default="", help="Run the given test case"
|
| |
@@ -30,10 +126,16 @@
|
| |
help="Run the tests in a venv on a Fedora host",
|
| |
)
|
| |
parser.add_argument(
|
| |
- "--skip-build",
|
| |
- dest="skip_build",
|
| |
- action="store_false",
|
| |
- help="Skip building the container image",
|
| |
+ "--rebuild",
|
| |
+ dest="rebuild",
|
| |
+ action="store_true",
|
| |
+ help="Enforce rebuild of container images",
|
| |
+ )
|
| |
+ parser.add_argument(
|
| |
+ "--rebuild-code",
|
| |
+ dest="rebuild_code",
|
| |
+ action="store_true",
|
| |
+ help="Enforce rebuild of code container images only",
|
| |
)
|
| |
parser.add_argument(
|
| |
"--shell",
|
| |
@@ -46,120 +148,190 @@
|
| |
parser.add_argument(
|
| |
"--repo",
|
| |
dest="repo",
|
| |
- default="https://pagure.io/pagure.git",
|
| |
- help="URL of the public repo to use as source, can be overridden using "
|
| |
- "the REPO environment variable",
|
| |
+ default="/wrkdir",
|
| |
+ help="URL or local path to git repository as source of the "
|
| |
+ "public repo to use as source, defaults to git repo in "
|
| |
+ "current directory, can also be overridden using the "
|
| |
+ "REPO environment variable",
|
| |
)
|
| |
parser.add_argument(
|
| |
"--branch",
|
| |
dest="branch",
|
| |
- default="master",
|
| |
- help="Branch of the repo to use as source, can be overridden using "
|
| |
- "the BRANCH environment variable",
|
| |
+ default="wrkdirbranch",
|
| |
+ help="Branch name to use as source, defaults to the active "
|
| |
+ "branch in current directory, can also be overridden by "
|
| |
+ "using the BRANCH environment variable",
|
| |
)
|
| |
|
| |
return parser
|
| |
|
| |
|
| |
if __name__ == "__main__":
|
| |
+ _check_pre_reqs()
|
| |
+
|
| |
parser = setup_parser()
|
| |
args = parser.parse_args()
|
| |
|
| |
- if args.centos is True:
|
| |
- container_names = ["pagure-c8s-rpms-py3"]
|
| |
- container_files = ["centos8-rpms-py3"]
|
| |
- elif args.fedora is True:
|
| |
- container_names = ["pagure-fedora-rpms-py3"]
|
| |
- container_files = ["fedora-rpms-py3"]
|
| |
- elif args.pip is True:
|
| |
- container_names = ["pagure-fedora-pip-py3"]
|
| |
- container_files = ["fedora-pip-py3"]
|
| |
+ containers = {
|
| |
+ "centos": {
|
| |
+ "name": "pagure-tests-centos-stream8-rpms-py3",
|
| |
+ "base": "base-centos-stream8-rpms-py3",
|
| |
+ "code": "code-centos-stream8-rpms-py3",
|
| |
+ },
|
| |
+ "fedora": {
|
| |
+ "name": "pagure-tests-fedora-rpms-py3",
|
| |
+ "base": "base-fedora-rpms-py3",
|
| |
+ "code": "code-fedora-rpms-py3",
|
| |
+ },
|
| |
+ "pip": {
|
| |
+ "name": "pagure-tests-fedora-pip-py3",
|
| |
+ "base": "base-fedora-pip-py3",
|
| |
+ "code": "code-fedora-pip-py3",
|
| |
+ },
|
| |
+ }
|
| |
+
|
| |
+ if args.centos:
|
| |
+ container_names = ["centos"]
|
| |
+ elif args.fedora:
|
| |
+ container_names = ["fedora"]
|
| |
+ elif args.pip:
|
| |
+ container_names = ["pip"]
|
| |
else:
|
| |
- container_names = [
|
| |
- "pagure-fedora-rpms-py3",
|
| |
- "pagure-c8s-rpms-py3",
|
| |
- "pagure-fedora-pip-py3",
|
| |
- ]
|
| |
- container_files = [
|
| |
- "fedora-rpms-py3",
|
| |
- "centos8-rpms-py3",
|
| |
- "fedora-pip-py3",
|
| |
- ]
|
| |
+ container_names = ["centos", "fedora", "pip"]
|
| |
+
|
| |
+ # get full path of git repo in current directory
|
| |
+ # and set var to mount it into the container
|
| |
+ if args.repo == "/wrkdir":
|
| |
+ # 'git rev-parse --show-toplevel' via python, Kudos: Ryne Everett
|
| |
+ # https://stackoverflow.com/questions/22081209#comment44778829_22081487
|
| |
+ wrkdir_path = (
|
| |
+ sp.Popen(["git", "rev-parse", "--show-toplevel"], stdout=sp.PIPE)
|
| |
+ .communicate()[0]
|
| |
+ .rstrip()
|
| |
+ .decode("ascii")
|
| |
+ )
|
| |
+ mount_wrkdir = True
|
| |
+ # 'args.repo' will be set as path to mount it into the container and then
|
| |
+ # overridden with '/wrkdir' to leverage existing logic to use a local path
|
| |
+ elif "http://" not in args.repo and "https://" not in args.repo:
|
| |
+ wrkdir_path = args.repo
|
| |
+ args.repo = "/wrkdir"
|
| |
+ mount_wrkdir = True
|
| |
+
|
| |
+ if args.branch == "wrkdirbranch":
|
| |
+ args.branch = (
|
| |
+ sp.Popen(["git", "branch", "--show-current"], stdout=sp.PIPE)
|
| |
+ .communicate()[0]
|
| |
+ .rstrip()
|
| |
+ .decode("ascii")
|
| |
+ )
|
| |
|
| |
failed = []
|
| |
- print("Running for {} containers:".format(len(container_names)))
|
| |
+ print("Running for %d containers:" % len(container_names))
|
| |
print(" - " + "\n - ".join(container_names))
|
| |
- for idx, container_name in enumerate(container_names):
|
| |
- if args.skip_build is not False:
|
| |
- print("------ Building Container Image -----")
|
| |
- cmd = [
|
| |
- "podman",
|
| |
- "build",
|
| |
- "--build-arg",
|
| |
- "branch={}".format(os.environ.get("BRANCH") or args.branch),
|
| |
- "--build-arg",
|
| |
- "repo={}".format(os.environ.get("REPO") or args.repo),
|
| |
- "--rm",
|
| |
- "-t",
|
| |
+ for container_name in container_names:
|
| |
+ result_path = "{}/results_{}".format(
|
| |
+ os.getcwd(), containers[container_name]["name"]
|
| |
+ )
|
| |
+ if not os.path.exists(result_path):
|
| |
+ os.mkdir(result_path)
|
| |
+
|
| |
+ print("\n------ Building Container Image -----")
|
| |
+
|
| |
+ if not _container_image_exist(container_name, "base") or args.rebuild:
|
| |
+ print(
|
| |
+ "Container does not exist, building: %s"
|
| |
+ % containers[container_name]["base"]
|
| |
+ )
|
| |
+ if _build_container(
|
| |
container_name,
|
| |
- "-f",
|
| |
- ROOT + "/dev/containers/%s" % container_files[idx],
|
| |
- ROOT + "/dev/containers",
|
| |
- ]
|
| |
- print(" ".join(cmd))
|
| |
- output_code = sp.call(cmd)
|
| |
- if output_code:
|
| |
- print("Failed building: %s", container_name)
|
| |
+ "base",
|
| |
+ result_path,
|
| |
+ branch="{}".format(os.environ.get("BRANCH") or args.branch),
|
| |
+ repo="{}".format(os.environ.get("REPO") or args.repo),
|
| |
+ ):
|
| |
+ base_build = True
|
| |
+ else:
|
| |
+ print(
|
| |
+ "Failed building: %s" % containers[container_name]["base"]
|
| |
+ )
|
| |
break
|
| |
+ else:
|
| |
+ base_build = False
|
| |
+ print(
|
| |
+ "Container already exist, skipped building: %s"
|
| |
+ % containers[container_name]["base"]
|
| |
+ )
|
| |
|
| |
- result_path = "{}/results_{}".format(os.getcwd(), container_files[idx])
|
| |
- if not os.path.exists(result_path):
|
| |
- os.mkdir(result_path)
|
| |
+ if (
|
| |
+ not _container_image_exist(container_name, "code")
|
| |
+ or base_build
|
| |
+ or args.rebuild
|
| |
+ or args.rebuild_code
|
| |
+ ):
|
| |
+ print(
|
| |
+ "Container does not exist, building: %s"
|
| |
+ % containers[container_name]["code"]
|
| |
+ )
|
| |
+ if not _build_container(container_name, "code", result_path):
|
| |
+ print(
|
| |
+ "Failed building: %s" % containers[container_name]["code"]
|
| |
+ )
|
| |
+ break
|
| |
+ else:
|
| |
+ print(
|
| |
+ "Container already exist, skipped building: %s"
|
| |
+ % containers[container_name]["code"]
|
| |
+ )
|
| |
+
|
| |
+ volumes = ["-v", "{}:/results:z".format(result_path)]
|
| |
+ if mount_wrkdir:
|
| |
+ volumes += ["-v", "{}:/wrkdir:z,ro".format(wrkdir_path)]
|
| |
+
|
| |
+ env_vars = [
|
| |
+ "-e",
|
| |
+ "BRANCH={}".format(os.environ.get("BRANCH") or args.branch),
|
| |
+ "-e",
|
| |
+ "REPO={}".format(os.environ.get("REPO") or args.repo),
|
| |
+ "-e",
|
| |
+ "TESTCASE={}".format(args.test_case or ""),
|
| |
+ ]
|
| |
|
| |
if args.shell:
|
| |
print("--------- Shelling in the container --------------")
|
| |
- command = [
|
| |
+ cmd = [
|
| |
"podman",
|
| |
"run",
|
| |
"-it",
|
| |
"--rm",
|
| |
"--name",
|
| |
- container_name,
|
| |
- "-v",
|
| |
- "{}/results_{}:/pagure/results:z".format(
|
| |
- os.getcwd(), container_files[idx]
|
| |
- ),
|
| |
- "-e",
|
| |
- "BRANCH={}".format(os.environ.get("BRANCH") or args.branch),
|
| |
- "-e",
|
| |
- "REPO={}".format(os.environ.get("REPO") or args.repo),
|
| |
+ containers[container_name]["name"],
|
| |
+ ]
|
| |
+ cmd += volumes
|
| |
+ cmd += env_vars
|
| |
+ cmd += [
|
| |
"--entrypoint=/bin/bash",
|
| |
- container_name,
|
| |
+ containers[container_name]["code"],
|
| |
]
|
| |
- sp.call(command)
|
| |
+ logfile = "{}/{}_shell.log".format(result_path, TIMESTAMP)
|
| |
+ _call_command(cmd, logfile)
|
| |
else:
|
| |
print("--------- Running Test --------------")
|
| |
- command = [
|
| |
+ cmd = [
|
| |
"podman",
|
| |
"run",
|
| |
"-it",
|
| |
"--rm",
|
| |
"--name",
|
| |
- container_name,
|
| |
- "-v",
|
| |
- "{}/results_{}:/pagure/results:z".format(
|
| |
- os.getcwd(), container_files[idx]
|
| |
- ),
|
| |
- "-e",
|
| |
- "BRANCH={}".format(os.environ.get("BRANCH") or args.branch),
|
| |
- "-e",
|
| |
- "REPO={}".format(os.environ.get("REPO") or args.repo),
|
| |
- "-e",
|
| |
- "TESTCASE={}".format(args.test_case or ""),
|
| |
- container_name,
|
| |
+ containers[container_name]["name"],
|
| |
+ ]
|
| |
+ cmd += volumes
|
| |
+ cmd += env_vars
|
| |
+ cmd += [
|
| |
+ containers[container_name]["code"],
|
| |
]
|
| |
- output_code = sp.call(command)
|
| |
- if output_code:
|
| |
+ logfile = "{}/{}_tests.log".format(result_path, TIMESTAMP)
|
| |
+ if not _call_command(cmd, logfile):
|
| |
failed.append(container_name)
|
| |
|
| |
if not args.shell:
|
| |
TLDR; This PR split the container images into base and code Dockerfile as well as a separate entrypoint script. Testing of the local git repo and active branch by mounting the folder into the container as new default.
REPO
andBRANCH
env vars can be set which allow testing of remote repositories as before if wanted. Image build only if they not exist in the local podman cache or when manually triggered, every test run use the existing container otherwise. This allows local testing of current changes within a few minutes or even just seconds.To reduce the resources and time to test changes, I created some scripts a few months ago which I used a lot when I was working on fixing the unit tests. It was hacky and mainly for my personal use, so I brushed it up a and moved all the logic into the
run-tests-container.py
script.General design changes:
The Dockerfiles of the old container image contained everything and also included the pagure source that should be tested. I found this very inflexible, every small change required a rebuild of the whole container, which is very time consuming. Therefore I split them in three parts: A base and code Dockerfile and a separate entrypoint script.
The base image contains all packages, rpm also everything required by
pagure.spec
, pip all tox environments pre-populated with the packages based on all requirements.txt files. The code image uses the base image and adds the entrypoint script.The actual pagure code isn't part of any of the images and will either be downloaded from a external repo or (default), mounted from the current directory, when the code container get started.
./run-tests-container.py --centos "tests/test_pagure_flask_api_issue_change_status.py"
> start c8s code container, local git top-folder mounted as/wrkdir
, entrypoint script performs git clone from/wrkdir
and given branch, default is the currently active, to/pagure
inside the container. The rest is mostly unchanged to the original code.Available container images:
I added additional tests to
run-tests-container.py
, if git or podman are not installed, it will exit. If the base or code container image not exist, they will be build. If the new parameter--rebuild
or--rebuild-code
is used, a new image build is forced by ignoring the local cache. As long no rebuild is triggered manually, the existing images will be used for every test run.pagure/dev/results_<test-image-name>
will still be mounted into the container as/results
, but the logging is done myrun-tests-container.py
, I don't see any pagure test code that still writes into/results
.Log files are automatically created for build, test and also shell activities and saved in the related
pagure/dev/results_<test-image-name>
folder, with the current unix timestamp as prefix. This allows to go back and check logs for a previous test run. Example:Specifying a test case was possible from a CLI perspective in the past but then only passed to the tests inside the rpm container, not pip. This is now possible with all images by adding the path to the test file as last argument. Example to run one test case and just a single test on c8s:
Testing changes quickly locally, without pushing to remote, was something I needed very urgent but was also requested for example here: https://pagure.io/pagure/issue/5186
As outlined in the above issue, if a PR is already open, pushing changes to perform a local test also triggered the Jenkins CI in the related PR, which is a waste of resources.
@ngompa @pingou sorry for the wall of text, I tried to explain this quite large change as good as possible. Feel free to give it a try and please let me know what you think. I'm happy to further improve it but for sure, you can also just merge it if you like the proposed approach.
Important:
This PR doesn't touch anything related to the CI pipeline, it only affects local testing. If parts of this approach would also be valuable to reduce the CI runtime, I can work an that at a later point.