From e6dd6d89c271f37332e0f472471e9e3cd2cc6c3c Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Thu, 25 Apr 2024 08:55:58 +0200 Subject: [PATCH] Add pre-commit (#250) JIRA: RHELWF-10948 --- .github/workflows/gating.yaml | 8 +- .pre-commit-config.yaml | 80 + README.md | 6 +- conf/policies/fedora.yaml | 2 +- conftest.py | 15 - docker/greenwave-settings.py | 58 +- docker/resultsdb-settings.py | 10 +- docker/scripts/consume.py | 9 +- docker/scripts/produce.py | 27 +- docker/waiverdb-settings.py | 20 +- docs/conf.py | 108 +- docs/dev-guide.rst | 15 +- docs/release-notes.rst | 2 +- functional-tests/conftest.py | 327 +-- functional-tests/consumers/handlers.py | 8 +- functional-tests/consumers/test_resultsdb.py | 804 +++---- functional-tests/consumers/test_waiverdb.py | 222 +- functional-tests/test_api_v1.py | 2083 +++++++++-------- functional-tests/test_cache.py | 6 +- functional-tests/test_healthcheck.py | 4 +- greenwave/__init__.py | 4 +- greenwave/api_v1.py | 100 +- greenwave/app_factory.py | 41 +- greenwave/cache.py | 5 +- greenwave/config.py | 92 +- greenwave/consumers/__init__.py | 1 - greenwave/consumers/consumer.py | 138 +- .../consumers/fedora_messaging_consumer.py | 44 +- greenwave/consumers/resultsdb.py | 48 +- greenwave/consumers/waiverdb.py | 19 +- greenwave/decision.py | 151 +- greenwave/listeners/base.py | 29 +- greenwave/listeners/resultsdb.py | 2 +- greenwave/logger.py | 6 +- greenwave/monitor.py | 39 +- greenwave/policies.py | 504 ++-- greenwave/product_versions.py | 50 +- greenwave/request_session.py | 30 +- greenwave/resources.py | 100 +- greenwave/safe_yaml.py | 69 +- greenwave/subjects/__init__.py | 1 - greenwave/subjects/factory.py | 2 +- greenwave/subjects/subject.py | 42 +- greenwave/subjects/subject_type.py | 46 +- greenwave/tests/__init__.py | 1 - greenwave/tests/conftest.py | 26 +- greenwave/tests/settings_override.py | 4 +- greenwave/tests/test_api_v1.py | 167 +- greenwave/tests/test_app_factory.py | 24 +- greenwave/tests/test_listeners.py | 26 +- greenwave/tests/test_monitor.py | 53 +- greenwave/tests/test_policies.py | 1303 ++++++----- greenwave/tests/test_product_versions.py | 40 +- greenwave/tests/test_request_session.py | 14 +- greenwave/tests/test_resultsdb_consumer.py | 586 ++--- greenwave/tests/test_retrieve_gating_yaml.py | 125 +- greenwave/tests/test_rules.py | 276 ++- greenwave/tests/test_subjects.py | 45 +- greenwave/tests/test_subjects_type.py | 24 +- greenwave/tests/test_summary.py | 66 +- greenwave/tests/test_tracing.py | 132 +- greenwave/tests/test_utils.py | 68 +- greenwave/tests/test_waive.py | 238 +- greenwave/tests/test_waivers_retriever.py | 32 +- greenwave/tests/test_xmlrpc_server_proxy.py | 15 +- greenwave/tracing.py | 8 +- greenwave/utils.py | 96 +- greenwave/wsgi.py | 3 +- greenwave/xmlrpc_server_proxy.py | 4 +- rpmlint-config.py | 5 +- run-dev-server.py | 10 +- tox.ini | 17 +- 72 files changed, 4726 insertions(+), 4059 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 conftest.py diff --git a/.github/workflows/gating.yaml b/.github/workflows/gating.yaml index 8cbf8b38..da7df733 100644 --- a/.github/workflows/gating.yaml +++ b/.github/workflows/gating.yaml @@ -56,8 +56,6 @@ jobs: strategy: matrix: tox_env: - - bandit - - lint - mypy - semgrep @@ -161,10 +159,8 @@ jobs: - name: Install docker-compose run: | - echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list && - curl -L "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04/Release.key" | sudo apt-key add - && - sudo apt-get -y update && - sudo apt-get -y -o Dpkg::Options::="--force-overwrite" install podman + python -m ensurepip --upgrade && + pip install podman-compose - name: Test Image env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..72dc3a0f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,80 @@ +--- +ci: + skip: + - hadolint-docker + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: check-yaml + args: + - --allow-multiple-documents + - --unsafe + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + + # Sort imports + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort + args: + - --line-length=79 + - --profile=black + + # Remove unused imports, variables, statements + - repo: https://github.com/PyCQA/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + + # Auto-update syntax + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.2 + hooks: + - id: pyupgrade + args: + - --py311-plus + + # Linter and formatter + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.5 + hooks: + - id: ruff + args: + # ignore: E501 Line too long + - --ignore=E501 + - id: ruff-format + + # Linter and formatter + - repo: https://github.com/Instagram/Fixit + rev: v2.1.0 + hooks: + - id: fixit-fix + + # Type linter + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.9.0 + hooks: + - id: mypy + args: + - --disable-error-code=import-untyped + - --ignore-missing-imports + + # Security linter + - repo: https://github.com/pycqa/bandit + rev: 1.7.8 + hooks: + - id: bandit + name: bandit + exclude: tests/ + + # Dockerfile linter + - repo: https://github.com/hadolint/hadolint + rev: v2.12.1-beta + hooks: + - id: hadolint-docker diff --git a/README.md b/README.md index 6a650245..846d07ef 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ![logo of Greenwave](https://github.com/release-engineering/greenwave/raw/master/logo.png) -Greenwave is a service to decide whether a software artifact can pass certain -gating points in a software delivery pipeline, based on test results stored in -[ResultsDB](https://github.com/release-engineering/resultsdb) and waivers stored in +Greenwave is a service to decide whether a software artifact can pass certain +gating points in a software delivery pipeline, based on test results stored in +[ResultsDB](https://github.com/release-engineering/resultsdb) and waivers stored in [WaiverDB](https://github.com/release-engineering/waiverdb). ## Documentation diff --git a/conf/policies/fedora.yaml b/conf/policies/fedora.yaml index 32dd3822..3a4651b5 100644 --- a/conf/policies/fedora.yaml +++ b/conf/policies/fedora.yaml @@ -11,7 +11,7 @@ product_versions: decision_context: bodhi_update_push_stable subject_type: koji_build excluded_packages: - # see the excluded list for dist.abicheck + # see the excluded list for dist.abicheck # https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/taskotron/taskotron-trigger/templates/trigger_rules.yml.j2#n17 - firefox - thunderbird diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 2a330600..00000000 --- a/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ -import mock -import pytest - - -@pytest.fixture(autouse=True) -def set_environment_variable(monkeypatch): - monkeypatch.setenv('TEST', 'true') - - -@pytest.fixture -def koji_proxy(): - mock_proxy = mock.Mock() - with mock.patch('greenwave.resources.get_server_proxy', return_value=mock_proxy): - yield mock_proxy diff --git a/docker/greenwave-settings.py b/docker/greenwave-settings.py index 7e3a1710..ac0e2b42 100644 --- a/docker/greenwave-settings.py +++ b/docker/greenwave-settings.py @@ -1,5 +1,5 @@ -SECRET_KEY = 'greenwave' -HOST = '127.0.0.1' +SECRET_KEY = "greenwave" # nosec +HOST = "127.0.0.1" PORT = 8080 DEBUG = True POLICIES_DIR = "/etc/greenwave/policies/" @@ -17,48 +17,44 @@ } CACHE = { # 'backend': 'dogpile.cache.null', - 'backend': 'dogpile.cache.pymemcache', - 'expiration_time': 1, # 1 is 1 second, keep to see that memcached - # service is working - 'arguments': { - 'url': 'memcached:11211', - 'distributed_lock': True - } + "backend": "dogpile.cache.pymemcache", + "expiration_time": 1, # 1 is 1 second, keep to see that memcached + # service is working + "arguments": {"url": "memcached:11211", "distributed_lock": True}, } LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'loggers': { - 'greenwave': { - 'level': 'DEBUG', + "version": 1, + "disable_existing_loggers": False, + "loggers": { + "greenwave": { + "level": "DEBUG", }, - 'dogpile.cache': { - 'level': 'DEBUG', + "dogpile.cache": { + "level": "DEBUG", }, "stomp.py": { "level": "DEBUG", }, }, - 'handlers': { - 'console': { - 'formatter': 'bare', - 'class': 'logging.StreamHandler', - 'stream': 'ext://sys.stdout', - 'level': 'DEBUG', + "handlers": { + "console": { + "formatter": "bare", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "level": "DEBUG", }, }, - 'formatters': { - 'bare': { - 'format': '[%(asctime)s] [%(process)d] [%(levelname)s] %(name)s: %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S', + "formatters": { + "bare": { + "format": "[%(asctime)s] [%(process)d] [%(levelname)s] %(name)s: %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", } }, - 'root': { - 'level': 'INFO', - 'handlers': ['console'], + "root": { + "level": "INFO", + "handlers": ["console"], }, } -OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://jaeger:4318/v1/traces' +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "http://jaeger:4318/v1/traces" OTEL_EXPORTER_SERVICE_NAME = "greenwave" - diff --git a/docker/resultsdb-settings.py b/docker/resultsdb-settings.py index c89c0918..fd0b12c3 100644 --- a/docker/resultsdb-settings.py +++ b/docker/resultsdb-settings.py @@ -1,12 +1,14 @@ import os -SECRET_KEY = 'resultsdb' -SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://resultsdb:resultsdb@resultsdb-db:5432/resultsdb' +SECRET_KEY = "resultsdb" # nosec +SQLALCHEMY_DATABASE_URI = ( + "postgresql+psycopg2://resultsdb:resultsdb@resultsdb-db:5432/resultsdb" # notsecret +) FILE_LOGGING = False -LOGFILE = '/var/log/resultsdb/resultsdb.log' +LOGFILE = "/var/log/resultsdb/resultsdb.log" SYSLOG_LOGGING = False STREAM_LOGGING = True -RUN_HOST = '0.0.0.0' +RUN_HOST = "127.0.0.1" RUN_PORT = 5001 ADDITIONAL_RESULT_OUTCOMES = ("RUNNING", "QUEUED", "ERROR") diff --git a/docker/scripts/consume.py b/docker/scripts/consume.py index 63e8b842..54f3d35c 100755 --- a/docker/scripts/consume.py +++ b/docker/scripts/consume.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 +import itertools import json import os -import itertools - from pprint import pprint from rhmsg.activemq.consumer import AMQConsumer @@ -29,7 +28,7 @@ def message_handler(message, data): if isinstance(body, str): body = body.encode("utf-8", "backslashreplace") if data["dump"]: - print("------------- ({0}) {1} --------------".format(num, message.id)) + print(f"------------- ({num}) {message.id} --------------") print("address:", message.address) print("subject:", message.subject) print("properties:", message.properties) @@ -55,7 +54,7 @@ def message_handler(message, data): def main(): - os.environ['PN_TRACE_FRM'] = '1' + os.environ["PN_TRACE_FRM"] = "1" consumer = InsecureAMQConsumer(urls=URLS) consumer.consume( ADDRESS, @@ -72,5 +71,5 @@ def main(): ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/docker/scripts/produce.py b/docker/scripts/produce.py index 2ef6bc3f..50517b87 100755 --- a/docker/scripts/produce.py +++ b/docker/scripts/produce.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 import json -import proton import os import sys +import proton from rhmsg.activemq.producer import AMQProducer TOPIC = "VirtualTopic.eng.resultsdb.result.new" @@ -12,13 +12,8 @@ SUBJECT = f"test_message_{sys.argv[1]}" MESSAGE = { "submit_time": "2019-08-27T13:57:53.490376", - "testcase": { - "name": "example_test" - }, - "data": { - "type": ["brew-build"], - "item": ["example-container"] - } + "testcase": {"name": "example_test"}, + "data": {"type": ["brew-build"], "item": ["example-container"]}, } MESSAGE = { "data": { @@ -33,11 +28,11 @@ "log": ["https://jenkins.example.com/job/x/build/y/console"], "publisher_id": ["msg-greenwave-segment-test"], "type": ["koji_build"], - "version": ["3.5.202110051331.w9756"] + "version": ["3.5.202110051331.w9756"], }, "groups": ["52c6b84b-b617-4b79-af47-8975d11bb635"], "href": "http://resultsdb/api/v2.0/results/123", - "id": 123, + "id": "123", "note": "", "outcome": "PASSED", "ref_url": "https://jenkins.example.com/job/x/build/y", @@ -45,13 +40,13 @@ "testcase": { "href": "http://resultsdb/api/v2.0/testcases/dist.abicheck", "name": "dist.abicheck", - "ref_url": "https://jenkins.example.com/job/x/build/y" - } + "ref_url": "https://jenkins.example.com/job/x/build/y", + }, } def main(): - os.environ['PN_TRACE_FRM'] = '1' + os.environ["PN_TRACE_FRM"] = "1" with AMQProducer(urls=URLS) as producer: # Disable SSL @@ -59,11 +54,9 @@ def main(): producer.through_topic(TOPIC) body = json.dumps(MESSAGE) - message = proton.Message( - subject=SUBJECT, - body=body) + message = proton.Message(subject=SUBJECT, body=body) producer.send(message) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/docker/waiverdb-settings.py b/docker/waiverdb-settings.py index 1f5c63b6..bce6a82a 100644 --- a/docker/waiverdb-settings.py +++ b/docker/waiverdb-settings.py @@ -1,17 +1,19 @@ import os -DATABASE_URI = 'postgresql+psycopg2://waiverdb:waiverdb@waiverdb-db:5433/waiverdb' +DATABASE_URI = ( + "postgresql+psycopg2://waiverdb:waiverdb@waiverdb-db:5433/waiverdb" # notsecret +) -if os.getenv('TEST') == 'true': - DATABASE_URI += '_test' +if os.getenv("TEST") == "true": + DATABASE_URI += "_test" -HOST = '127.0.0.1' +HOST = "127.0.0.1" PORT = 5004 -#AUTH_METHOD = 'OIDC' -AUTH_METHOD = 'dummy' -SUPERUSERS = ['dummy'] -#OIDC_CLIENT_SECRETS = '/etc/secret/client_secrets.json' -RESULTSDB_API_URL = 'http://resultsdb:5001/api/v2.0' +# AUTH_METHOD = 'OIDC' +AUTH_METHOD = "dummy" +SUPERUSERS = ["dummy"] +# OIDC_CLIENT_SECRETS = '/etc/secret/client_secrets.json' +RESULTSDB_API_URL = "http://resultsdb:5001/api/v2.0" MESSAGE_BUS_PUBLISH = os.environ.get("GREENWAVE_LISTENERS", "") not in ("", "0") MESSAGE_PUBLISHER = "stomp" diff --git a/docs/conf.py b/docs/conf.py index 4d696b48..c1197dac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Greenwave documentation build configuration file, created by # sphinx-quickstart on Wed May 10 13:18:15 2017. @@ -20,51 +19,50 @@ import sys # This will cause the Flask application to be created with development configs -os.environ['DOCS'] = 'true' +os.environ["DOCS"] = "true" -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) import greenwave # noqa: E402 - # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # -needs_sphinx = '1.3' +needs_sphinx = "1.3" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'sphinxcontrib.autohttp.flask', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinxcontrib.autohttp.flask", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Greenwave' -copyright = '2017, Red Hat, Inc. and others' -author = 'Red Hat, Inc. and others' +project = "Greenwave" +copyright = "2017, Red Hat, Inc. and others" +author = "Red Hat, Inc. and others" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '.'.join(greenwave.__version__.split('.')[:2]) +version = ".".join(greenwave.__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = greenwave.__version__ @@ -73,15 +71,15 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -92,7 +90,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -103,42 +101,43 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] -html_static_path = [] +# html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'Greenwavedoc' +htmlhelp_basename = "Greenwavedoc" # -- Options for LaTeX output --------------------------------------------- -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} +# latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +# +# 'papersize': 'letterpaper', +# The font size ('10pt', '11pt' or '12pt'). +# +# 'pointsize': '10pt', +# Additional stuff for the LaTeX preamble. +# +# 'preamble': '', +# Latex figure (float) alignment +# +# 'figure_align': 'htbp', +# } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'Greenwave.tex', 'Greenwave Documentation', - 'Red Hat, Inc. and others', 'manual'), + ( + master_doc, + "Greenwave.tex", + "Greenwave Documentation", + "Red Hat, Inc. and others", + "manual", + ), ] @@ -146,10 +145,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'greenwave', 'Greenwave Documentation', - [author], 1) -] +man_pages = [(master_doc, "greenwave", "Greenwave Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -158,9 +154,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Greenwave', 'Greenwave Documentation', - author, 'Greenwave', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "Greenwave", + "Greenwave Documentation", + author, + "Greenwave", + "One line description of project.", + "Miscellaneous", + ), ] @@ -182,8 +184,10 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3/', 'python-intersphinx.inv')} +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", "python-intersphinx.inv") +} diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index e220a475..e891a322 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -8,6 +8,12 @@ get started. Development setup ================= +Install pre-commit: + +.. code-block:: console + + $ pre-commit install + Install development dependencies: .. code-block:: console @@ -68,12 +74,15 @@ documentation, you can build it locally and view it in your browser: Code style ========== -We follow the `PEP 8`_ style guide for Python. You can check your code's style -using flake8: +We follow the `PEP 8`_ style guide for Python. Pre-commit (see installation +instructions above) runs ``ruff`` and other tools to automatically check and +reformats the code on commit. + +To run pre-commit on all files: .. code-block:: console - $ tox -e flake8 + $ pre-commit run --all-files Additionally, we follow the `"Google style" for docstrings `_. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 7ae963b5..569335d8 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -319,7 +319,7 @@ Released 15 July 2019 The following issue prevents adopting a newer version of Sphinx: `https://github.com/ignatenkobrain/sphinxcontrib-issuetracker/ issues/23`. * General code optimizations and documentation update. * Correct the waiverdb consumer to use the correct messaging setting. -* Bug fix - Add retry logic when fetching data from dist-git. +* Bug fix - Add retry logic when fetching data from dist-git. * Bug fix - Fix matching some wrong product versions. * Fun addition - Added life-decision endpoint. Ask a question to Greenwave checking the /life-decision endpoint, it will give you an advice for your life. Greenwave is just a service, it cannot give you every answers for your life decisions, but it can help you to find the answer inside your heart. diff --git a/functional-tests/conftest.py b/functional-tests/conftest.py index 15d70f87..6b069418 100644 --- a/functional-tests/conftest.py +++ b/functional-tests/conftest.py @@ -1,33 +1,33 @@ # SPDX-License-Identifier: GPL-2.0+ -import os -import sys -import time -import textwrap import itertools import json import logging -import subprocess +import os import socket +import subprocess +import sys +import textwrap +import time +from contextlib import contextmanager +from unittest.mock import patch + import pytest import requests - -from contextlib import contextmanager -from mock import patch from sqlalchemy import create_engine from greenwave.logger import init_logging - +from greenwave.tests.conftest import * # noqa: F403 log = logging.getLogger(__name__) # It's all local, and so should be fast enough. -TEST_HTTP_TIMEOUT = int(os.environ.get('TEST_HTTP_TIMEOUT', 2)) +TEST_HTTP_TIMEOUT = int(os.environ.get("TEST_HTTP_TIMEOUT", 2)) -@pytest.fixture(scope='session', autouse=True) -def logging(): +@pytest.fixture(scope="session", autouse=True) +def setup_logging(): init_logging() # We don't configure any log handlers, let pytest capture the log # messages and display them instead. @@ -37,11 +37,11 @@ def drop_and_create_database(dbname): """ Drops (if exists) and re-creates the given database on the local Postgres instance. """ - engine = create_engine('postgresql+psycopg2:///template1') + engine = create_engine("postgresql+psycopg2:///template1") with engine.connect() as connection: - connection.execution_options(isolation_level='AUTOCOMMIT') - connection.execute('DROP DATABASE IF EXISTS :db', db=dbname) - connection.execute('CREATE DATABASE :db', db=dbname) + connection.execution_options(isolation_level="AUTOCOMMIT") + connection.execute("DROP DATABASE IF EXISTS :db", db=dbname) + connection.execute("CREATE DATABASE :db", db=dbname) engine.dispose() @@ -51,21 +51,26 @@ def wait_for_listen(port): """ for attempt in range(50): try: - s = socket.create_connection(('127.0.0.1', port), timeout=1) + s = socket.create_connection(("127.0.0.1", port), timeout=1) s.close() return - except socket.error: + except OSError: time.sleep(0.1) - raise RuntimeError('Gave up waiting for port %s' % port) + raise RuntimeError(f"Gave up waiting for port {port}") @contextmanager def server_subprocess( - name, port, start_server_arguments, - api_base='/api/v1.0', - source_path=None, - settings_content=None, tmpdir_factory=None, - dbname=None, init_db_arguments=None): + name, + port, + start_server_arguments, + api_base="/api/v1.0", + source_path=None, + settings_content=None, + tmpdir_factory=None, + dbname=None, + init_db_arguments=None, +): """ Starts a server as subprocess and returns address. """ @@ -73,29 +78,29 @@ def server_subprocess( # _TEST_URL environment variable overrides address and avoids # creating test process. - test_url_env_var = env_var_prefix + '_TEST_URL' + test_url_env_var = env_var_prefix + "_TEST_URL" if test_url_env_var in os.environ: url = os.environ[test_url_env_var] - config = f'greenwave.config.TestingConfig.{env_var_prefix}_API_URL' + config = f"greenwave.config.TestingConfig.{env_var_prefix}_API_URL" with patch(config, url + api_base): yield url return if source_path is None: - default_source_path = os.path.join('..', name) + default_source_path = os.path.join("..", name) source_path = os.environ.get(env_var_prefix, default_source_path) if not os.path.isdir(source_path): - raise RuntimeError('{} source tree {} does not exist'.format(name, source_path)) + raise RuntimeError(f"{name} source tree {source_path} does not exist") env = os.environ.copy() - env['PYTHONPATH'] = os.path.pathsep.join([source_path] + sys.path) - env['TEST'] = 'true' + env["PYTHONPATH"] = os.path.pathsep.join([source_path] + sys.path) + env["TEST"] = "true" # Write out a config if settings_content is not None: - settings_file = tmpdir_factory.mktemp(name).join('settings.py') + settings_file = tmpdir_factory.mktemp(name).join("settings.py") settings_file.write(textwrap.dedent(settings_content)) - config_env_var = env_var_prefix + '_CONFIG' + config_env_var = env_var_prefix + "_CONFIG" env[config_env_var] = settings_file.strpath subprocess_arguments = dict(env=env, cwd=source_path) @@ -108,141 +113,149 @@ def server_subprocess( # Start server with subprocess.Popen(start_server_arguments, **subprocess_arguments) as p: - log.debug('Started %s server as pid %s', name, p.pid) + log.debug("Started %s server as pid %s", name, p.pid) wait_for_listen(port) - yield 'http://localhost:{}/'.format(port) + yield f"http://localhost:{port}/" - log.debug('Terminating %s server pid %s', name, p.pid) + log.debug("Terminating %s server pid %s", name, p.pid) p.terminate() p.wait() -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def resultsdb_server(tmpdir_factory): - dbname = 'resultsdb_for_greenwave_functest' - settings_content = """ + dbname = "resultsdb_for_greenwave_functest" + settings_content = f""" PORT = 5001 - SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///%s' + SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///{dbname}' DEBUG = False ADDITIONAL_RESULT_OUTCOMES = ( 'QUEUED', 'RUNNING', ) - """ % dbname + """ - init_db_arguments = ['python3', 'run_cli.py', 'init_db'] - start_server_arguments = ['python3', 'runapp.py'] + init_db_arguments = ["python3", "run_cli.py", "init_db"] + start_server_arguments = ["python3", "runapp.py"] with server_subprocess( - name='resultsdb', - port=5001, - api_base='/api/v2.0', - dbname=dbname, - settings_content=settings_content, - init_db_arguments=init_db_arguments, - start_server_arguments=start_server_arguments, - tmpdir_factory=tmpdir_factory) as url: + name="resultsdb", + port=5001, + api_base="/api/v2.0", + dbname=dbname, + settings_content=settings_content, + init_db_arguments=init_db_arguments, + start_server_arguments=start_server_arguments, + tmpdir_factory=tmpdir_factory, + ) as url: yield url -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def waiverdb_server(tmpdir_factory): - dbname = 'waiverdb_for_greenwave_functest' - settings_content = """ + dbname = "waiverdb_for_greenwave_functest" + settings_content = f""" AUTH_METHOD = 'dummy' - DATABASE_URI = 'postgresql+psycopg2:///%s' + DATABASE_URI = 'postgresql+psycopg2:///{dbname}' MESSAGE_BUS_PUBLISH = False - """ % dbname - - init_db_arguments = ['python3', os.path.join('waiverdb', 'manage.py'), 'db', 'upgrade'] + """ + + init_db_arguments = [ + "python3", + os.path.join("waiverdb", "manage.py"), + "db", + "upgrade", + ] start_server_arguments = [ - 'gunicorn-3', - '--bind=127.0.0.1:5004', - '--access-logfile=-', - 'waiverdb.wsgi:app'] + "gunicorn-3", + "--bind=127.0.0.1:5004", + "--access-logfile=-", + "waiverdb.wsgi:app", + ] with server_subprocess( - name='waiverdb', - port=5004, - dbname=dbname, - settings_content=settings_content, - init_db_arguments=init_db_arguments, - start_server_arguments=start_server_arguments, - tmpdir_factory=tmpdir_factory) as url: + name="waiverdb", + port=5004, + dbname=dbname, + settings_content=settings_content, + init_db_arguments=init_db_arguments, + start_server_arguments=start_server_arguments, + tmpdir_factory=tmpdir_factory, + ) as url: yield url -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def distgit_server(tmpdir_factory): - """ Creating a fake dist-git process. It is just a serving some files in a tmp dir """ - tmp_dir = tmpdir_factory.mktemp('distgit') + """Creating a fake dist-git process. It is just a serving some files in a tmp dir""" + tmp_dir = tmpdir_factory.mktemp("distgit") f = open(tmp_dir.strpath + "/gating.yaml", "w+") f.close() - start_server_arguments = [sys.executable, '-m', 'http.server', '5678'] + start_server_arguments = [sys.executable, "-m", "http.server", "5678"] with server_subprocess( - name='dist-git', - port=5678, - source_path=tmp_dir.strpath, - start_server_arguments=start_server_arguments) as url: + name="dist-git", + port=5678, + source_path=tmp_dir.strpath, + start_server_arguments=start_server_arguments, + ) as url: yield url -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def cache_config(tmpdir_factory): - cache_file = tmpdir_factory.mktemp('greenwave').join('cache.dbm') - if 'GREENWAVE_TEST_URL' in os.environ: + cache_file = tmpdir_factory.mktemp("greenwave").join("cache.dbm") + if "GREENWAVE_TEST_URL" in os.environ: # This should point to the same cache as the Greenwave server used by tests. return { - 'backend': 'dogpile.cache.pymemcache', - 'expiration_time': 300, - 'arguments': { - 'url': 'memcached:11211', - 'distributed_lock': True - } + "backend": "dogpile.cache.pymemcache", + "expiration_time": 300, + "arguments": {"url": "memcached:11211", "distributed_lock": True}, } return { - 'backend': 'dogpile.cache.dbm', - 'expiration_time': 300, - 'arguments': {'filename': cache_file.strpath}, + "backend": "dogpile.cache.dbm", + "expiration_time": 300, + "arguments": {"filename": cache_file.strpath}, } -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def greenwave_server(tmpdir_factory, cache_config, resultsdb_server, waiverdb_server): settings_content = """ - CACHE = %s - RESULTSDB_API_URL = '%sapi/v2.0' - WAIVERDB_API_URL = '%sapi/v1.0' - """ % (json.dumps(cache_config), resultsdb_server, waiverdb_server) + CACHE = {} + RESULTSDB_API_URL = '{}api/v2.0' + WAIVERDB_API_URL = '{}api/v1.0' + """.format(json.dumps(cache_config), resultsdb_server, waiverdb_server) start_server_arguments = [ - 'gunicorn-3', - '--bind=127.0.0.1:5005', - '--access-logfile=-', - 'greenwave.wsgi:app'] + "gunicorn-3", + "--bind=127.0.0.1:5005", + "--access-logfile=-", + "greenwave.wsgi:app", + ] with server_subprocess( - name='greenwave', - port=5005, - source_path='.', - settings_content=settings_content, - start_server_arguments=start_server_arguments, - tmpdir_factory=tmpdir_factory) as url: + name="greenwave", + port=5005, + source_path=".", + settings_content=settings_content, + start_server_arguments=start_server_arguments, + tmpdir_factory=tmpdir_factory, + ) as url: yield url -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def requests_session(request): s = requests.Session() request.addfinalizer(s.close) return s -class TestDataBuilder(object): +class TestDataBuilder: """ Test fixture object which has helper methods for setting up test data in ResultsDB and WaiverDB. @@ -255,89 +268,107 @@ def __init__(self, requests_session, resultsdb_url, waiverdb_url, distgit_url): self.distgit_url = distgit_url self._counter = itertools.count(time.time()) - def unique_nvr(self, name='glibc', product_version='el7'): - return '{}-1.0-{}.{}'.format(name, next(self._counter), product_version) + def unique_nvr(self, name="glibc", product_version="el7"): + return f"{name}-1.0-{next(self._counter)}.{product_version}" def unique_compose_id(self): - return 'Fedora-Rawhide-19700101.n.{}'.format(next(self._counter)) + return f"Fedora-Rawhide-19700101.n.{next(self._counter)}" def _create_result(self, data): response = self.requests_session.post( - self.resultsdb_url + 'api/v2.0/results', + self.resultsdb_url + "api/v2.0/results", timeout=TEST_HTTP_TIMEOUT, - json=data + json=data, ) response.raise_for_status() return response.json() def create_compose_result(self, compose_id, testcase_name, outcome, scenario=None): data = { - 'testcase': {'name': testcase_name}, - 'data': {'productmd.compose.id': compose_id}, - 'outcome': outcome, + "testcase": {"name": testcase_name}, + "data": {"productmd.compose.id": compose_id}, + "outcome": outcome, } if scenario: - data['data']['scenario'] = scenario + data["data"]["scenario"] = scenario return self._create_result(data) def create_rtt_compose_result(self, compose_id, outcome, variant, architecture): data = { - 'testcase': {'name': 'rtt.acceptance.validation'}, - 'outcome': outcome, - 'data': { - 'productmd.compose.id': [compose_id], - 'system_variant': [variant], - 'system_architecture': [architecture], - } + "testcase": {"name": "rtt.acceptance.validation"}, + "outcome": outcome, + "data": { + "productmd.compose.id": [compose_id], + "system_variant": [variant], + "system_architecture": [architecture], + }, } return self._create_result(data) - def create_koji_build_result(self, nvr, testcase_name, outcome, type_='koji_build'): + def create_koji_build_result(self, nvr, testcase_name, outcome, type_="koji_build"): data = { - 'testcase': {'name': testcase_name}, - 'outcome': outcome, - 'data': {'item': nvr, 'type': type_}, + "testcase": {"name": testcase_name}, + "outcome": outcome, + "data": {"item": nvr, "type": type_}, } return self._create_result(data) - def create_result(self, item, testcase_name, outcome, - scenario=None, key=None, _type='koji_build', **custom_data): + def create_result( + self, + item, + testcase_name, + outcome, + scenario=None, + key=None, + _type="koji_build", + **custom_data, + ): data = { - 'testcase': {'name': testcase_name}, - 'outcome': outcome, + "testcase": {"name": testcase_name}, + "outcome": outcome, } if not key: - data['data'] = {'item': item, 'type': _type} + data["data"] = {"item": item, "type": _type} else: - data['data'] = {key: item} + data["data"] = {key: item} if scenario: - data['data']['scenario'] = scenario - data['data'].update(custom_data) + data["data"]["scenario"] = scenario + data["data"].update(custom_data) return self._create_result(data) - def create_waiver(self, nvr, testcase_name, product_version, comment, waived=True, - subject_type='koji_build'): + def create_waiver( + self, + nvr, + testcase_name, + product_version, + comment, + waived=True, + subject_type="koji_build", + ): data = { - 'subject_type': subject_type, - 'subject_identifier': nvr, - 'testcase': testcase_name, - 'product_version': product_version, - 'waived': waived, - 'comment': comment + "subject_type": subject_type, + "subject_identifier": nvr, + "testcase": testcase_name, + "product_version": product_version, + "waived": waived, + "comment": comment, } # We assume WaiverDB is configured with # AUTH_METHOD = 'dummy' to accept Basic with any credentials. response = self.requests_session.post( - self.waiverdb_url + 'api/v1.0/waivers/', - auth=('dummy', 'dummy'), + self.waiverdb_url + "api/v1.0/waivers/", + auth=("dummy", "dummy"), timeout=TEST_HTTP_TIMEOUT, - json=data + json=data, ) response.raise_for_status() return response.json() -@pytest.fixture(scope='session') -def testdatabuilder(requests_session, resultsdb_server, waiverdb_server, distgit_server): - return TestDataBuilder(requests_session, resultsdb_server, waiverdb_server, - distgit_server) +@pytest.fixture(scope="session") +def testdatabuilder( + requests_session, resultsdb_server, waiverdb_server, distgit_server +): + return TestDataBuilder( + requests_session, resultsdb_server, waiverdb_server, distgit_server + ) diff --git a/functional-tests/consumers/handlers.py b/functional-tests/consumers/handlers.py index e40b8446..fe559a86 100644 --- a/functional-tests/consumers/handlers.py +++ b/functional-tests/consumers/handlers.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock +from unittest import mock from greenwave.config import TestingConfig @@ -8,12 +8,12 @@ def create_handler(handler_class, topic, greenwave_server, cache_config=None): hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } config = TestingConfig() - config.GREENWAVE_API_URL = greenwave_server + '/api/v1.0' + config.GREENWAVE_API_URL = greenwave_server + "/api/v1.0" if cache_config: config.CACHE = cache_config diff --git a/functional-tests/consumers/test_resultsdb.py b/functional-tests/consumers/test_resultsdb.py index 83874387..e24d7155 100644 --- a/functional-tests/consumers/test_resultsdb.py +++ b/functional-tests/consumers/test_resultsdb.py @@ -1,46 +1,51 @@ # SPDX-License-Identifier: GPL-2.0+ import hashlib -import mock import time +from unittest import mock + +import handlers from greenwave.consumers import resultsdb from greenwave.utils import right_before_this_time -import handlers - def create_resultdb_handler(greenwave_server, cache_config=None): return handlers.create_handler( resultsdb.ResultsDBHandler, - 'topic_prefix.environment.taskotron.result.new', + "topic_prefix.environment.taskotron.result.new", greenwave_server, - cache_config) + cache_config, + ) -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_consume_new_result( - mock_fedora_messaging, requests_session, greenwave_server, - testdatabuilder, koji_proxy): - nvr = testdatabuilder.unique_nvr(product_version='fc26') - result = testdatabuilder.create_result(item=nvr, - testcase_name='dist.rpmdeplint', - outcome='PASSED') + mock_fedora_messaging, + requests_session, + greenwave_server, + testdatabuilder, + koji_proxy, +): + nvr = testdatabuilder.unique_nvr(product_version="fc26") + result = testdatabuilder.create_result( + item=nvr, testcase_name="dist.rpmdeplint", outcome="PASSED" + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'dist.rpmdeplint', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": result["id"], + "outcome": "PASSED", + "testcase": { + "name": "dist.rpmdeplint", }, - 'data': { - 'item': [nvr], - 'type': ['koji_build'], + "data": { + "item": [nvr], + "type": ["koji_build"], }, - 'submit_time': result['submit_time'] - } + "submit_time": result["submit_time"], + }, } } handler = create_resultdb_handler(greenwave_server) @@ -53,158 +58,164 @@ def test_consume_new_result( ) actual_msgs_sent = [call[1][0].body for call in mock_fedora_messaging.mock_calls] assert actual_msgs_sent[0] == { - 'policies_satisfied': False, - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'satisfied_requirements': [ + "policies_satisfied": False, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "satisfied_requirements": [ { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': result['id'], - 'testcase': 'dist.rpmdeplint', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-passed', + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": result["id"], + "testcase": "dist.rpmdeplint", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-passed", }, ], - 'unsatisfied_requirements': [ + "unsatisfied_requirements": [ { - 'testcase': 'dist.abicheck', - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "testcase": "dist.abicheck", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, }, { - 'testcase': 'dist.upgradepath', - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, - } + "testcase": "dist.upgradepath", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, + }, ], - 'summary': 'Of 3 required tests, 2 results missing', - 'subject': [ - {'item': nvr, 'type': 'koji_build'}, + "summary": "Of 3 required tests, 2 results missing", + "subject": [ + {"item": nvr, "type": "koji_build"}, ], - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'applicable_policies': ['taskotron_release_critical_tasks_with_blocklist', - 'taskotron_release_critical_tasks'], - 'previous': { - 'applicable_policies': ['taskotron_release_critical_tasks_with_blocklist', - 'taskotron_release_critical_tasks'], - 'policies_satisfied': False, - 'summary': 'Of 3 required tests, 3 results missing', - 'satisfied_requirements': [], - 'unsatisfied_requirements': [ + "subject_type": "koji_build", + "subject_identifier": nvr, + "applicable_policies": [ + "taskotron_release_critical_tasks_with_blocklist", + "taskotron_release_critical_tasks", + ], + "previous": { + "applicable_policies": [ + "taskotron_release_critical_tasks_with_blocklist", + "taskotron_release_critical_tasks", + ], + "policies_satisfied": False, + "summary": "Of 3 required tests, 3 results missing", + "satisfied_requirements": [], + "unsatisfied_requirements": [ { - 'testcase': 'dist.abicheck', - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "testcase": "dist.abicheck", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, }, { - 'testcase': 'dist.rpmdeplint', - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "testcase": "dist.rpmdeplint", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, }, { - 'testcase': 'dist.upgradepath', - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "testcase": "dist.upgradepath", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, }, ], }, } assert actual_msgs_sent[1] == { - 'policies_satisfied': True, - 'decision_context': 'bodhi_update_push_testing', - 'product_version': 'fedora-26', - 'satisfied_requirements': [ + "policies_satisfied": True, + "decision_context": "bodhi_update_push_testing", + "product_version": "fedora-26", + "satisfied_requirements": [ { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': result['id'], - 'testcase': 'dist.rpmdeplint', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'type': 'test-result-passed', - 'source': None, + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": result["id"], + "testcase": "dist.rpmdeplint", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "type": "test-result-passed", + "source": None, }, ], - 'unsatisfied_requirements': [], - 'summary': 'All required tests (1 total) have passed or been waived', - 'subject': [ - {'item': nvr, 'type': 'koji_build'}, + "unsatisfied_requirements": [], + "summary": "All required tests (1 total) have passed or been waived", + "subject": [ + {"item": nvr, "type": "koji_build"}, ], - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'applicable_policies': ['taskotron_release_critical_tasks_for_testing'], - 'previous': { - 'applicable_policies': ['taskotron_release_critical_tasks_for_testing'], - 'policies_satisfied': False, - 'summary': 'Of 1 required test, 1 result missing', - 'satisfied_requirements': [], - 'unsatisfied_requirements': [ + "subject_type": "koji_build", + "subject_identifier": nvr, + "applicable_policies": ["taskotron_release_critical_tasks_for_testing"], + "previous": { + "applicable_policies": ["taskotron_release_critical_tasks_for_testing"], + "policies_satisfied": False, + "summary": "Of 1 required test, 1 result missing", + "satisfied_requirements": [], + "unsatisfied_requirements": [ { - 'testcase': 'dist.rpmdeplint', - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "testcase": "dist.rpmdeplint", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, }, ], }, } -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_consume_unchanged_result( - mock_fedora_messaging, requests_session, greenwave_server, - testdatabuilder): - nvr = testdatabuilder.unique_nvr(product_version='fc26') + mock_fedora_messaging, requests_session, greenwave_server, testdatabuilder +): + nvr = testdatabuilder.unique_nvr(product_version="fc26") testdatabuilder.create_result( - item=nvr, testcase_name='dist.rpmdeplint', outcome='PASSED') + item=nvr, testcase_name="dist.rpmdeplint", outcome="PASSED" + ) new_result = testdatabuilder.create_result( - item=nvr, testcase_name='dist.rpmdeplint', outcome='PASSED') + item=nvr, testcase_name="dist.rpmdeplint", outcome="PASSED" + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': new_result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'dist.rpmdeplint', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": new_result["id"], + "outcome": "PASSED", + "testcase": { + "name": "dist.rpmdeplint", }, - 'data': { - 'item': [nvr], - 'type': ['koji_build'], + "data": { + "item": [nvr], + "type": ["koji_build"], }, - 'submit_time': new_result['submit_time'] - } + "submit_time": new_result["submit_time"], + }, } } handler = create_resultdb_handler(greenwave_server) @@ -213,30 +224,31 @@ def test_consume_unchanged_result( assert len(mock_fedora_messaging.mock_calls) == 0 -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_consume_compose_id_result( - mock_fedora_messaging, requests_session, greenwave_server, - testdatabuilder): + mock_fedora_messaging, requests_session, greenwave_server, testdatabuilder +): compose_id = testdatabuilder.unique_compose_id() result = testdatabuilder.create_compose_result( compose_id=compose_id, - testcase_name='compose.install_no_user', - scenario='scenario1', - outcome='PASSED') + testcase_name="compose.install_no_user", + scenario="scenario1", + outcome="PASSED", + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'compose.install_no_user', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": result["id"], + "outcome": "PASSED", + "testcase": { + "name": "compose.install_no_user", }, - 'data': { - 'productmd.compose.id': [compose_id], + "data": { + "productmd.compose.id": [compose_id], }, - 'submit_time': result['submit_time'] - } + "submit_time": result["submit_time"], + }, } } handler = create_resultdb_handler(greenwave_server) @@ -244,44 +256,48 @@ def test_consume_compose_id_result( # get old decision data = { - 'decision_context': 'rawhide_compose_sync_to_mirrors', - 'product_version': 'fedora-rawhide', - 'subject': [{'productmd.compose.id': compose_id}], - 'when': right_before_this_time(result['submit_time']) + "decision_context": "rawhide_compose_sync_to_mirrors", + "product_version": "fedora-rawhide", + "subject": [{"productmd.compose.id": compose_id}], + "when": right_before_this_time(result["submit_time"]), } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 old_decision = r.json() msg = { - 'applicable_policies': ['openqa_important_stuff_for_rawhide'], - 'decision_context': 'rawhide_compose_sync_to_mirrors', - 'policies_satisfied': False, - 'product_version': 'fedora-rawhide', - 'subject': [{'productmd.compose.id': compose_id}], - 'subject_type': 'compose', - 'subject_identifier': compose_id, - 'summary': 'Of 2 required tests, 1 result missing', - 'previous': old_decision, - 'satisfied_requirements': [{ - 'subject_type': 'compose', - 'subject_identifier': compose_id, - 'result_id': result['id'], - 'scenario': 'scenario1', - 'system_architecture': None, - 'system_variant': None, - 'testcase': 'compose.install_no_user', - 'source': None, - 'type': 'test-result-passed' - }], - 'unsatisfied_requirements': [{ - 'item': {'productmd.compose.id': compose_id}, - 'subject_type': 'compose', - 'subject_identifier': compose_id, - 'scenario': 'scenario2', - 'source': None, - 'testcase': 'compose.install_no_user', - 'type': 'test-result-missing'} - ] + "applicable_policies": ["openqa_important_stuff_for_rawhide"], + "decision_context": "rawhide_compose_sync_to_mirrors", + "policies_satisfied": False, + "product_version": "fedora-rawhide", + "subject": [{"productmd.compose.id": compose_id}], + "subject_type": "compose", + "subject_identifier": compose_id, + "summary": "Of 2 required tests, 1 result missing", + "previous": old_decision, + "satisfied_requirements": [ + { + "subject_type": "compose", + "subject_identifier": compose_id, + "result_id": result["id"], + "scenario": "scenario1", + "system_architecture": None, + "system_variant": None, + "testcase": "compose.install_no_user", + "source": None, + "type": "test-result-passed", + } + ], + "unsatisfied_requirements": [ + { + "item": {"productmd.compose.id": compose_id}, + "subject_type": "compose", + "subject_identifier": compose_id, + "scenario": "scenario2", + "source": None, + "testcase": "compose.install_no_user", + "type": "test-result-missing", + } + ], } assert len(mock_fedora_messaging.mock_calls) == 1 @@ -293,35 +309,32 @@ def test_consume_compose_id_result( assert actual_msgs_sent[0] == msg -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_consume_legacy_result( - mock_fedora_messaging, requests_session, greenwave_server, - testdatabuilder, koji_proxy): - """ Test that we can still handle the old legacy "taskotron" format. + mock_fedora_messaging, + requests_session, + greenwave_server, + testdatabuilder, + koji_proxy, +): + """Test that we can still handle the old legacy "taskotron" format. We should be using resultsdb.result.new everywhere now, but we also need to be able to handle this taskotron format for the transition. """ - nvr = testdatabuilder.unique_nvr(product_version='fc26') - result = testdatabuilder.create_result(item=nvr, - testcase_name='dist.rpmdeplint', - outcome='PASSED') + nvr = testdatabuilder.unique_nvr(product_version="fc26") + result = testdatabuilder.create_result( + item=nvr, testcase_name="dist.rpmdeplint", outcome="PASSED" + ) message = { - 'body': { - 'topic': 'taskotron.result.new', - 'msg': { - 'result': { - 'id': result['id'], - 'outcome': 'PASSED' - }, - 'task': { - 'item': nvr, - 'type': 'koji_build', - 'name': 'dist.rpmdeplint' - }, - 'submit_time': result['submit_time'] - } + "body": { + "topic": "taskotron.result.new", + "msg": { + "result": {"id": result["id"], "outcome": "PASSED"}, + "task": {"item": nvr, "type": "koji_build", "name": "dist.rpmdeplint"}, + "submit_time": result["submit_time"], + }, } } handler = create_resultdb_handler(greenwave_server) @@ -329,69 +342,62 @@ def test_consume_legacy_result( # get old decision data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject': [{'item': nvr, 'type': 'koji_build'}], - 'when': right_before_this_time(result['submit_time']), + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject": [{"item": nvr, "type": "koji_build"}], + "when": right_before_this_time(result["submit_time"]), } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 old_decision = r.json() # should have two messages published as we have two decision contexts applicable to # this subject. first_msg = { - 'policies_satisfied': False, - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'satisfied_requirements': [{ - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': result['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': 'dist.rpmdeplint', - 'source': None, - 'type': 'test-result-passed' - }], - 'unsatisfied_requirements': [ - { - 'testcase': 'dist.abicheck', - 'item': { - 'item': nvr, - 'type': 'koji_build' - }, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, - }, + "policies_satisfied": False, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "satisfied_requirements": [ { - 'testcase': 'dist.upgradepath', - 'item': { - 'item': nvr, - 'type': 'koji_build' - }, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": result["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": "dist.rpmdeplint", + "source": None, + "type": "test-result-passed", } ], - 'summary': 'Of 3 required tests, 2 results missing', - 'subject': [ + "unsatisfied_requirements": [ { - 'item': nvr, - 'type': 'koji_build' - } + "testcase": "dist.abicheck", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, + }, + { + "testcase": "dist.upgradepath", + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "type": "test-result-missing", + "scenario": None, + "source": None, + }, + ], + "summary": "Of 3 required tests, 2 results missing", + "subject": [{"item": nvr, "type": "koji_build"}], + "subject_type": "koji_build", + "subject_identifier": nvr, + "applicable_policies": [ + "taskotron_release_critical_tasks_with_blocklist", + "taskotron_release_critical_tasks", ], - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'applicable_policies': ['taskotron_release_critical_tasks_with_blocklist', - 'taskotron_release_critical_tasks'], - 'previous': old_decision, + "previous": old_decision, } assert all( @@ -403,41 +409,38 @@ def test_consume_legacy_result( # get the old decision for the second policy data = { - 'decision_context': 'bodhi_update_push_testing', - 'product_version': 'fedora-26', - 'subject': [{'item': nvr, 'type': 'koji_build'}], - 'when': right_before_this_time(result['submit_time']), + "decision_context": "bodhi_update_push_testing", + "product_version": "fedora-26", + "subject": [{"item": nvr, "type": "koji_build"}], + "when": right_before_this_time(result["submit_time"]), } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 old_decision = r.json() second_msg = { - 'policies_satisfied': True, - 'decision_context': 'bodhi_update_push_testing', - 'product_version': 'fedora-26', - 'satisfied_requirements': [{ - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': result['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': 'dist.rpmdeplint', - 'source': None, - 'type': 'test-result-passed' - }], - 'unsatisfied_requirements': [], - 'summary': 'All required tests (1 total) have passed or been waived', - 'subject': [ + "policies_satisfied": True, + "decision_context": "bodhi_update_push_testing", + "product_version": "fedora-26", + "satisfied_requirements": [ { - 'item': nvr, - 'type': 'koji_build' + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": result["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": "dist.rpmdeplint", + "source": None, + "type": "test-result-passed", } ], - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'applicable_policies': ['taskotron_release_critical_tasks_for_testing'], - 'previous': old_decision, + "unsatisfied_requirements": [], + "summary": "All required tests (1 total) have passed or been waived", + "subject": [{"item": nvr, "type": "koji_build"}], + "subject_type": "koji_build", + "subject_identifier": nvr, + "applicable_policies": ["taskotron_release_critical_tasks_for_testing"], + "previous": old_decision, } assert all( @@ -448,35 +451,34 @@ def test_consume_legacy_result( assert actual_msgs_sent[1] == second_msg -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_no_message_for_nonapplicable_policies( - mock_fedora_messaging, requests_session, greenwave_server, - testdatabuilder): + mock_fedora_messaging, requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() # One result gets the decision in a certain state. - testdatabuilder.create_result(item=nvr, - testcase_name='a_package_test', - outcome='FAILED') + testdatabuilder.create_result( + item=nvr, testcase_name="a_package_test", outcome="FAILED" + ) # Recording a new version of the same result shouldn't change our decision at all. new_result = testdatabuilder.create_result( - item=nvr, - testcase_name='a_package_test', - outcome='PASSED') + item=nvr, testcase_name="a_package_test", outcome="PASSED" + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': new_result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'a_package_test', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": new_result["id"], + "outcome": "PASSED", + "testcase": { + "name": "a_package_test", }, - 'data': { - 'item': [nvr], - 'type': ['koji_build'], + "data": { + "item": [nvr], + "type": ["koji_build"], }, - 'submit_time': new_result['submit_time'] - } + "submit_time": new_result["submit_time"], + }, } } handler = create_resultdb_handler(greenwave_server) @@ -486,16 +488,19 @@ def test_no_message_for_nonapplicable_policies( mock_fedora_messaging.assert_not_called() -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_consume_new_result_container_image( - mock_fedora_messaging, requests_session, greenwave_server, - testdatabuilder): - unique_id = str(time.time()).encode('utf-8') + mock_fedora_messaging, requests_session, greenwave_server, testdatabuilder +): + unique_id = str(time.time()).encode("utf-8") sha256 = hashlib.sha256(unique_id).hexdigest() - item_hash = 'fedora@sha256:{}'.format(sha256) - result = testdatabuilder.create_result(item=item_hash, - testcase_name='baseos-qe.baseos-ci.tier1.functional', - outcome='PASSED', _type='container-image') + item_hash = f"fedora@sha256:{sha256}" + result = testdatabuilder.create_result( + item=item_hash, + testcase_name="baseos-qe.baseos-ci.tier1.functional", + outcome="PASSED", + _type="container-image", + ) message = { "body": { "username": None, @@ -514,94 +519,57 @@ def test_consume_new_result_container_image( "destination": "/queue/Consumer.client-datanommer.upshift-dev.VirtualTopic.eng.>", "priority": "4", "message-id": "ID:umb-test-9999-umb-3-r7xk4-46608-1545211261969-3:248:-1:1:1", - "subscription": "/queue/Consumer.client-datanommer.upshift-dev.VirtualTopic.eng.>" + "subscription": "/queue/Consumer.client-datanommer.upshift-dev.VirtualTopic.eng.>", }, "signature": None, "source_version": "0.9.1", "msg": { "testcase": { "ref_url": "https://example.com", - "href": ("http://resultsdb-test-9999-api-yuxzhu.cloud.paas.upshift.redhat." - "com/api/v2.0/testcases/baseos-qe.baseos-ci.tier1.functional"), - "name": "baseos-qe.baseos-ci.tier1.functional" + "href": ( + "http://resultsdb-test-9999-api-yuxzhu.cloud.paas.upshift.redhat." + "com/api/v2.0/testcases/baseos-qe.baseos-ci.tier1.functional" + ), + "name": "baseos-qe.baseos-ci.tier1.functional", }, "ref_url": "https://somewhere.com/job/ci-openstack/4794", "note": "", - "href": ("http://resultsdb-test-9999-api-yuxzhu.cloud.paas.upshift.redhat." - "com/api/v2.0/results/58"), - "groups": [ - "341d4cba-ffe2-4d83-b36c-5d819181e86d" - ], - "submit_time": result['submit_time'], + "href": ( + "http://resultsdb-test-9999-api-yuxzhu.cloud.paas.upshift.redhat." + "com/api/v2.0/results/58" + ), + "groups": ["341d4cba-ffe2-4d83-b36c-5d819181e86d"], + "submit_time": result["submit_time"], "outcome": "PASSED", "data": { - "category": [ - "functional" - ], - "log": [ - "https://somewhere.com/job/ci-openstack/4794/console" - ], - "recipients": [ - "mvadkert", - "ovasik" - ], - "ci_environment": [ - "production" - ], - "scratch": [ - "True" - ], + "category": ["functional"], + "log": ["https://somewhere.com/job/ci-openstack/4794/console"], + "recipients": ["mvadkert", "ovasik"], + "ci_environment": ["production"], + "scratch": ["True"], "rebuild": [ "https://somewhere.com/job/ci-openstack/4794/rebuild/parametrized" ], - "ci_email": [ - "pnt-devops-dev@example.com" - ], - "nvr": [ - "fedora:28" - ], - "ci_name": [ - "C3I Jenkins" - ], - "repository": [ - "fedora" - ], - "item": [ - item_hash - ], - "system_provider": [ - "openstack" - ], - "ci_url": [ - "https://example.com" - ], - "digest": [ - "sha256:{}".format(sha256) - ], + "ci_email": ["pnt-devops-dev@example.com"], + "nvr": ["fedora:28"], + "ci_name": ["C3I Jenkins"], + "repository": ["fedora"], + "item": [item_hash], + "system_provider": ["openstack"], + "ci_url": ["https://example.com"], + "digest": [f"sha256:{sha256}"], "xunit": [ "https://somewhere.com/job/ci-openstack/4794/artifacts/results.xml" ], - "system_architecture": [ - "x86_64" - ], - "ci_team": [ - "DevOps" - ], - "type": [ - "container-image" - ], - "system_os": [ - "rhel-7.4-server-x86_64-updated" - ], - "ci_irc": [ - "#pnt-devops-dev" - ], - "issuer": [ - "yuxzhu" - ] + "system_architecture": ["x86_64"], + "ci_team": ["DevOps"], + "type": ["container-image"], + "system_os": ["rhel-7.4-server-x86_64-updated"], + "ci_irc": ["#pnt-devops-dev"], + "issuer": ["yuxzhu"], }, - "id": result['id'] - } + "id": result["id"], + }, } } handler = create_resultdb_handler(greenwave_server) @@ -610,36 +578,38 @@ def test_consume_new_result_container_image( # get old decision data = { - 'decision_context': 'container-image-test', - 'product_version': 'c3i', - 'subject': [{'item': item_hash, 'type': 'container-image'}], - 'when': right_before_this_time(result['submit_time']), + "decision_context": "container-image-test", + "product_version": "c3i", + "subject": [{"item": item_hash, "type": "container-image"}], + "when": right_before_this_time(result["submit_time"]), } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 old_decision = r.json() actual_msgs_sent = [call[1][0].body for call in mock_fedora_messaging.mock_calls] assert actual_msgs_sent[0] == { - 'applicable_policies': ['container-image-policy'], - 'decision_context': 'container-image-test', - 'policies_satisfied': True, - 'product_version': 'c3i', - 'subject': [{'item': item_hash, 'type': 'container-image'}], - 'subject_type': 'container-image', - 'subject_identifier': item_hash, - 'summary': 'All required tests (1 total) have passed or been waived', - 'previous': old_decision, - 'satisfied_requirements': [{ - 'subject_type': 'container-image', - 'subject_identifier': item_hash, - 'result_id': result['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': 'baseos-qe.baseos-ci.tier1.functional', - 'source': None, - 'type': 'test-result-passed' - }], - 'unsatisfied_requirements': [] + "applicable_policies": ["container-image-policy"], + "decision_context": "container-image-test", + "policies_satisfied": True, + "product_version": "c3i", + "subject": [{"item": item_hash, "type": "container-image"}], + "subject_type": "container-image", + "subject_identifier": item_hash, + "summary": "All required tests (1 total) have passed or been waived", + "previous": old_decision, + "satisfied_requirements": [ + { + "subject_type": "container-image", + "subject_identifier": item_hash, + "result_id": result["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": "baseos-qe.baseos-ci.tier1.functional", + "source": None, + "type": "test-result-passed", + } + ], + "unsatisfied_requirements": [], } diff --git a/functional-tests/consumers/test_waiverdb.py b/functional-tests/consumers/test_waiverdb.py index edd92fca..ee04acac 100644 --- a/functional-tests/consumers/test_waiverdb.py +++ b/functional-tests/consumers/test_waiverdb.py @@ -1,62 +1,64 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock -import pytest - -from greenwave.consumers import waiverdb +from unittest import mock import handlers +import pytest +from greenwave.consumers import waiverdb TASKTRON_RELEASE_CRITICAL_TASKS = [ - 'dist.abicheck', - 'dist.rpmdeplint', - 'dist.upgradepath', + "dist.abicheck", + "dist.rpmdeplint", + "dist.upgradepath", ] def create_waiverdb_handler(greenwave_server): return handlers.create_handler( waiverdb.WaiverDBHandler, - 'topic_prefix.environment.waiver.new', - greenwave_server) + "topic_prefix.environment.waiver.new", + greenwave_server, + ) -@pytest.mark.parametrize('subject_type', ('koji_build', 'brew-build')) -@mock.patch('greenwave.consumers.consumer.fedora_messaging.api.publish') +@pytest.mark.parametrize("subject_type", ("koji_build", "brew-build")) +@mock.patch("greenwave.consumers.consumer.fedora_messaging.api.publish") def test_consume_new_waiver( - mock_fedora_messaging, requests_session, greenwave_server, testdatabuilder, - subject_type): + mock_fedora_messaging, + requests_session, + greenwave_server, + testdatabuilder, + subject_type, +): nvr = testdatabuilder.unique_nvr() failing_test = TASKTRON_RELEASE_CRITICAL_TASKS[0] result = testdatabuilder.create_result( - item=nvr, - testcase_name=failing_test, - outcome='FAILED', - _type=subject_type) + item=nvr, testcase_name=failing_test, outcome="FAILED", _type=subject_type + ) # The rest passed passing_tests = TASKTRON_RELEASE_CRITICAL_TASKS[1:] results = [ testdatabuilder.create_result( - item=nvr, - testcase_name=testcase_name, - outcome='PASSED', - _type=subject_type) + item=nvr, testcase_name=testcase_name, outcome="PASSED", _type=subject_type + ) for testcase_name in passing_tests ] - testcase = str(result['testcase']['name']) - waiver = testdatabuilder.create_waiver(nvr=nvr, - testcase_name=testcase, - product_version='fedora-26', - comment='Because I said so', - subject_type=subject_type) + testcase = str(result["testcase"]["name"]) + waiver = testdatabuilder.create_waiver( + nvr=nvr, + testcase_name=testcase, + product_version="fedora-26", + comment="Because I said so", + subject_type=subject_type, + ) message = { - 'body': { - 'topic': 'waiver.new', - 'msg': waiver, + "body": { + "topic": "waiver.new", + "msg": waiver, } } handler = create_waiverdb_handler(greenwave_server) @@ -69,97 +71,101 @@ def test_consume_new_waiver( ) actual_msgs_sent = [call[1][0].body for call in mock_fedora_messaging.mock_calls] assert actual_msgs_sent[0] == { - 'applicable_policies': ['taskotron_release_critical_tasks_with_blocklist', - 'taskotron_release_critical_tasks'], - 'policies_satisfied': True, - 'decision_context': 'bodhi_update_push_stable', - 'previous': { - 'applicable_policies': ['taskotron_release_critical_tasks_with_blocklist', - 'taskotron_release_critical_tasks'], - 'policies_satisfied': False, - 'summary': 'Of 3 required tests, 1 test failed', - 'satisfied_requirements': [ + "applicable_policies": [ + "taskotron_release_critical_tasks_with_blocklist", + "taskotron_release_critical_tasks", + ], + "policies_satisfied": True, + "decision_context": "bodhi_update_push_stable", + "previous": { + "applicable_policies": [ + "taskotron_release_critical_tasks_with_blocklist", + "taskotron_release_critical_tasks", + ], + "policies_satisfied": False, + "summary": "Of 3 required tests, 1 test failed", + "satisfied_requirements": [ { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': results[0]['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': passing_tests[0], - 'source': None, - 'type': 'test-result-passed' + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": results[0]["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": passing_tests[0], + "source": None, + "type": "test-result-passed", }, { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': results[1]['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': passing_tests[1], - 'source': None, - 'type': 'test-result-passed' - } + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": results[1]["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": passing_tests[1], + "source": None, + "type": "test-result-passed", + }, ], - 'unsatisfied_requirements': [ + "unsatisfied_requirements": [ { - 'result_id': result['id'], - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_identifier': nvr, - 'subject_type': 'koji_build', - 'testcase': failing_test, - 'type': 'test-result-failed', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, + "result_id": result["id"], + "item": {"item": nvr, "type": "koji_build"}, + "subject_identifier": nvr, + "subject_type": "koji_build", + "testcase": failing_test, + "type": "test-result-failed", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, }, ], }, - 'product_version': 'fedora-26', - 'subject': [ - {'item': nvr, 'type': 'koji_build'}, + "product_version": "fedora-26", + "subject": [ + {"item": nvr, "type": "koji_build"}, ], - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'satisfied_requirements': [ + "subject_type": "koji_build", + "subject_identifier": nvr, + "satisfied_requirements": [ { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': result['id'], - 'waiver_id': waiver['id'], - 'testcase': failing_test, - 'type': 'test-result-failed-waived', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": result["id"], + "waiver_id": waiver["id"], + "testcase": failing_test, + "type": "test-result-failed-waived", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, }, { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': results[0]['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': passing_tests[0], - 'source': None, - 'type': 'test-result-passed' + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": results[0]["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": passing_tests[0], + "source": None, + "type": "test-result-passed", }, { - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': results[1]['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'testcase': passing_tests[1], - 'source': None, - 'type': 'test-result-passed' - } + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": results[1]["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "testcase": passing_tests[1], + "source": None, + "type": "test-result-passed", + }, ], - 'unsatisfied_requirements': [], - 'summary': 'All required tests (3 total) have passed or been waived', - 'testcase': testcase, + "unsatisfied_requirements": [], + "summary": "All required tests (3 total) have passed or been waived", + "testcase": testcase, } diff --git a/functional-tests/test_api_v1.py b/functional-tests/test_api_v1.py index 7adfd3d1..340ee623 100644 --- a/functional-tests/test_api_v1.py +++ b/functional-tests/test_api_v1.py @@ -1,34 +1,30 @@ # SPDX-License-Identifier: GPL-2.0+ -import pytest -import re import os - +import re from hashlib import sha256 from textwrap import dedent +import pytest + from greenwave import __version__ from greenwave.utils import right_before_this_time - TASKTRON_RELEASE_CRITICAL_TASKS = [ - 'dist.abicheck', - 'dist.rpmdeplint', - 'dist.upgradepath', + "dist.abicheck", + "dist.rpmdeplint", + "dist.upgradepath", ] OPENQA_SCENARIOS = [ - 'scenario1', - 'scenario2', + "scenario1", + "scenario2", ] def drop_href(obj): if isinstance(obj, dict): - return { - k: drop_href(v) for k, v in obj.items() - if k != "href" - } + return {k: drop_href(v) for k, v in obj.items() if k != "href"} return obj @@ -38,280 +34,319 @@ def drop_hrefs(results): @pytest.mark.smoke def test_any_policies_loaded(requests_session, greenwave_server): - r = requests_session.get(greenwave_server + 'api/v1.0/policies', - headers={'Content-Type': 'application/json'}) + r = requests_session.get( + greenwave_server + "api/v1.0/policies", + headers={"Content-Type": "application/json"}, + ) assert r.status_code == 200 body = r.json() - policies = body['policies'] + policies = body["policies"] assert len(policies) > 0 def test_inspect_policies(requests_session, greenwave_server): - r = requests_session.get(greenwave_server + 'api/v1.0/policies', - headers={'Content-Type': 'application/json'}) + r = requests_session.get( + greenwave_server + "api/v1.0/policies", + headers={"Content-Type": "application/json"}, + ) assert r.status_code == 200 body = r.json() - policies = body['policies'] + policies = body["policies"] assert len(policies) == 16 - assert any(p['id'] == 'taskotron_release_critical_tasks' for p in policies) - assert any(p['decision_context'] == 'bodhi_update_push_stable' for p in policies) - assert any(p['product_versions'] == ['fedora-26'] for p in policies) + assert any(p["id"] == "taskotron_release_critical_tasks" for p in policies) + assert any(p["decision_context"] == "bodhi_update_push_stable" for p in policies) + assert any(p["product_versions"] == ["fedora-26"] for p in policies) expected_rules = [ - {'rule': 'PassingTestCaseRule', - 'test_case_name': 'dist.abicheck', - 'scenario': None}, + { + "rule": "PassingTestCaseRule", + "test_case_name": "dist.abicheck", + "scenario": None, + }, ] - assert any(p['rules'] == expected_rules for p in policies) + assert any(p["rules"] == expected_rules for p in policies) expected_rules = [ - {'rule': 'PassingTestCaseRule', - 'test_case_name': 'dist.rpmdeplint', - 'scenario': None}, - {'rule': 'PassingTestCaseRule', - 'test_case_name': 'dist.upgradepath', - 'scenario': None}] - assert any(p['rules'] == expected_rules for p in policies) + { + "rule": "PassingTestCaseRule", + "test_case_name": "dist.rpmdeplint", + "scenario": None, + }, + { + "rule": "PassingTestCaseRule", + "test_case_name": "dist.upgradepath", + "scenario": None, + }, + ] + assert any(p["rules"] == expected_rules for p in policies) @pytest.mark.smoke def test_version_endpoint(requests_session, greenwave_server): - r = requests_session.get(greenwave_server + 'api/v1.0/version') + r = requests_session.get(greenwave_server + "api/v1.0/version") assert r.status_code == 200 - assert {'version': __version__} == r.json() + assert {"version": __version__} == r.json() @pytest.mark.smoke def test_about_endpoint_jsonp(requests_session, greenwave_server): - r = requests_session.get(greenwave_server + 'api/v1.0/about?callback=bac123') + r = requests_session.get(greenwave_server + "api/v1.0/about?callback=bac123") assert r.status_code == 200 - expected = 'bac123({"version":"%s"});' % __version__ - actual = re.sub(r'\s+', '', r.text) + expected = f'bac123({{"version":"{__version__}"}});' + actual = re.sub(r"\s+", "", r.text) assert expected == actual @pytest.mark.smoke def test_version_redirect(requests_session, greenwave_server): - r = requests_session.get(greenwave_server + 'api/v1.0/version') + r = requests_session.get(greenwave_server + "api/v1.0/version") assert r.status_code == 200 - assert __version__, r.json()['version'] - assert r.url.endswith('about') + assert __version__, r.json()["version"] + assert r.url.endswith("about") @pytest.mark.smoke -def test_cannot_make_decision_without_product_version(requests_session, greenwave_server): +def test_cannot_make_decision_without_product_version( + requests_session, greenwave_server +): data = { - 'decision_context': 'bodhi_update_push_stable', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', + "decision_context": "bodhi_update_push_stable", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2018-ec7cb4d5eb", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert 'Missing required product version' == r.json()['message'] + assert "Missing required product version" == r.json()["message"] @pytest.mark.smoke def test_cannot_make_decision_without_decision_context_and_user_policies( - requests_session, greenwave_server): + requests_session, greenwave_server +): data = { - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2018-ec7cb4d5eb", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert 'Either decision_context or rules is required.' == r.json()['message'] + assert "Either decision_context or rules is required." == r.json()["message"] @pytest.mark.smoke def test_cannot_make_decision_without_subject_type(requests_session, greenwave_server): data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_identifier": "FEDORA-2018-ec7cb4d5eb", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert 'Missing required "subject_type" parameter' == r.json()['message'] + assert 'Missing required "subject_type" parameter' == r.json()["message"] @pytest.mark.smoke -def test_cannot_make_decision_without_subject_identifier(requests_session, greenwave_server): +def test_cannot_make_decision_without_subject_identifier( + requests_session, greenwave_server +): data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "bodhi_update", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert 'Missing required "subject_identifier" parameter' == r.json()['message'] + assert 'Missing required "subject_identifier" parameter' == r.json()["message"] @pytest.mark.smoke def test_cannot_make_decision_with_invalid_subject(requests_session, greenwave_server): data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject': 'foo-1.0.0-1.el7', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject": "foo-1.0.0-1.el7", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert 'Invalid subject, must be a list of dicts' == r.json()['message'] + assert "Invalid subject, must be a list of dicts" == r.json()["message"] data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject': ['foo-1.0.0-1.el7'], + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject": ["foo-1.0.0-1.el7"], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert 'Invalid subject, must be a list of dicts' == r.json()['message'] + assert "Invalid subject, must be a list of dicts" == r.json()["message"] @pytest.mark.smoke -def test_404_for_invalid_product_version(requests_session, greenwave_server, testdatabuilder): +def test_404_for_invalid_product_version( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() data = { - 'decision_context': 'bodhi_push_update_stable', - 'product_version': 'f26', # not a real product version - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_push_update_stable", + "product_version": "f26", # not a real product version + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 404 - expected = ('Found no applicable policies for koji_build subjects ' - 'at gating point(s) bodhi_push_update_stable in f26') - assert expected == r.json()['message'] + expected = ( + "Found no applicable policies for koji_build subjects " + "at gating point(s) bodhi_push_update_stable in f26" + ) + assert expected == r.json()["message"] @pytest.mark.smoke -def test_404_for_invalid_decision_context(requests_session, greenwave_server, testdatabuilder): +def test_404_for_invalid_decision_context( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() data = { - 'decision_context': 'bodhi_push_update', # missing the _stable part! - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_push_update", # missing the _stable part! + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 404 - expected = ('Found no applicable policies for koji_build subjects ' - 'at gating point(s) bodhi_push_update in fedora-26') - assert expected == r.json()['message'] + expected = ( + "Found no applicable policies for koji_build subjects " + "at gating point(s) bodhi_push_update in fedora-26" + ) + assert expected == r.json()["message"] @pytest.mark.smoke def test_415_for_missing_request_content_type(requests_session, greenwave_server): - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json={}) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json={}) assert r.status_code == 415 expected = "No JSON payload in request" - assert expected == r.json()['message'] + assert expected == r.json()["message"] @pytest.mark.smoke def test_invalid_payload(requests_session, greenwave_server): - r = requests_session.post(greenwave_server + 'api/v1.0/decision', - headers={'Content-Type': 'application/json'}, - data='not a json') + r = requests_session.post( + greenwave_server + "api/v1.0/decision", + headers={"Content-Type": "application/json"}, + data="not a json", + ) assert r.status_code == 400 - message = r.json()['message'] - assert "Failed to decode JSON object" in message or \ - "sent a request that this server could not understand" in message + message = r.json()["message"] + assert ( + "Failed to decode JSON object" in message + or "sent a request that this server could not understand" in message + ) -def test_make_a_decision_on_passed_result(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_on_passed_result( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert res_data['applicable_policies'] == [ - 'taskotron_release_critical_tasks_with_blocklist', - 'taskotron_release_critical_tasks', + assert res_data["policies_satisfied"] is True + assert res_data["applicable_policies"] == [ + "taskotron_release_critical_tasks_with_blocklist", + "taskotron_release_critical_tasks", ] - expected_summary = 'All required tests (3 total) have passed or been waived' - assert res_data['summary'] == expected_summary + expected_summary = "All required tests (3 total) have passed or been waived" + assert res_data["summary"] == expected_summary -def test_make_a_decision_with_verbose_flag(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_with_verbose_flag( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() results = [] for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'verbose': True, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == len(TASKTRON_RELEASE_CRITICAL_TASKS) - assert drop_hrefs(res_data['results']) == drop_hrefs(list(reversed(results))) + assert len(res_data["results"]) == len(TASKTRON_RELEASE_CRITICAL_TASKS) + assert drop_hrefs(res_data["results"]) == drop_hrefs(list(reversed(results))) expected_waivers = [] - assert res_data['waivers'] == expected_waivers + assert res_data["waivers"] == expected_waivers expected_satisfied_requirements = [ { - 'result_id': result['id'], - 'testcase': result['testcase']['name'], - 'type': 'test-result-passed', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'source': None, - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - } for result in results + "result_id": result["id"], + "testcase": result["testcase"]["name"], + "type": "test-result-passed", + "subject_type": "koji_build", + "subject_identifier": nvr, + "source": None, + "scenario": None, + "system_architecture": None, + "system_variant": None, + } + for result in results ] - assert res_data['satisfied_requirements'] == expected_satisfied_requirements + assert res_data["satisfied_requirements"] == expected_satisfied_requirements def test_make_a_decision_with_verbose_flag_and_multiple_nvrs_with_results( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): build_nvrs = [testdatabuilder.unique_nvr(), testdatabuilder.unique_nvr()] results = [] for nvr in reversed(build_nvrs): for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - results.append(testdatabuilder.create_result( - item=nvr, testcase_name=testcase_name, outcome='PASSED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject': [ - {'type': 'koji_build', 'item': nvr} - for nvr in build_nvrs - ], - 'verbose': True, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject": [{"type": "koji_build", "item": nvr} for nvr in build_nvrs], + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == len(TASKTRON_RELEASE_CRITICAL_TASKS) * len(build_nvrs) - assert drop_hrefs(res_data['results']) == drop_hrefs(list(reversed(results))) + assert len(res_data["results"]) == len(TASKTRON_RELEASE_CRITICAL_TASKS) * len( + build_nvrs + ) + assert drop_hrefs(res_data["results"]) == drop_hrefs(list(reversed(results))) def test_make_a_decision_with_verbose_flag_and_multiple_nvrs_with_waivers( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): build_nvrs = [testdatabuilder.unique_nvr(), testdatabuilder.unique_nvr()] waivers = [] @@ -319,426 +354,468 @@ def test_make_a_decision_with_verbose_flag_and_multiple_nvrs_with_waivers( for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: waiver = testdatabuilder.create_waiver( nvr=nvr, - product_version='fedora-26', + product_version="fedora-26", testcase_name=testcase_name, - comment='This is fine') + comment="This is fine", + ) waivers.append(waiver) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject': [ - {'type': 'koji_build', 'item': nvr} - for nvr in build_nvrs - ], - 'verbose': True, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject": [{"type": "koji_build", "item": nvr} for nvr in build_nvrs], + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['waivers']) == len(TASKTRON_RELEASE_CRITICAL_TASKS) * len(build_nvrs) - assert res_data['waivers'] == list(reversed(waivers)) + assert len(res_data["waivers"]) == len(TASKTRON_RELEASE_CRITICAL_TASKS) * len( + build_nvrs + ) + assert res_data["waivers"] == list(reversed(waivers)) def test_make_a_decision_on_failed_result_with_waiver( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() # First one failed but was waived - testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED') - testdatabuilder.create_waiver(nvr=nvr, - product_version='fedora-26', - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - comment='This is fine') + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) + testdatabuilder.create_waiver( + nvr=nvr, + product_version="fedora-26", + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + comment="This is fine", + ) # The rest passed for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - expected_summary = 'All required tests (3 total) have passed or been waived' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is True + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + expected_summary = "All required tests (3 total) have passed or been waived" + assert res_data["summary"] == expected_summary -def test_make_a_decision_on_failed_result(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_on_failed_result( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() - result = testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED') + result = testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - expected_summary = 'Of 3 required tests, 2 results missing, 1 test failed' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is False + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + expected_summary = "Of 3 required tests, 2 results missing, 1 test failed" + assert res_data["summary"] == expected_summary expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'result_id': result['id'], - 'testcase': TASKTRON_RELEASE_CRITICAL_TASKS[0], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-failed' + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "result_id": result["id"], + "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-failed", }, ] + [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'testcase': name, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, - } for name in TASKTRON_RELEASE_CRITICAL_TASKS[1:] + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "testcase": name, + "type": "test-result-missing", + "scenario": None, + "source": None, + } + for name in TASKTRON_RELEASE_CRITICAL_TASKS[1:] ] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements -def test_make_a_decision_on_queued_result(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_on_queued_result( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() - result = testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='QUEUED') + result = testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="QUEUED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - expected_summary = 'Of 3 required tests, 2 results missing, 1 test incomplete' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is False + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + expected_summary = "Of 3 required tests, 2 results missing, 1 test incomplete" + assert res_data["summary"] == expected_summary expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_identifier': result['data']['item'][0], - 'subject_type': result['data']['type'][0], - 'testcase': TASKTRON_RELEASE_CRITICAL_TASKS[0], - 'result_id': result['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-missing' + "item": {"item": nvr, "type": "koji_build"}, + "subject_identifier": result["data"]["item"][0], + "subject_type": result["data"]["type"][0], + "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0], + "result_id": result["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-missing", }, ] + [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'testcase': name, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, - } for name in TASKTRON_RELEASE_CRITICAL_TASKS[1:] + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "testcase": name, + "type": "test-result-missing", + "scenario": None, + "source": None, + } + for name in TASKTRON_RELEASE_CRITICAL_TASKS[1:] ] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements -def test_make_a_decision_on_running_result(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_on_running_result( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() - result = testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='RUNNING') + result = testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="RUNNING" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - expected_summary = 'Of 3 required tests, 2 results missing, 1 test incomplete' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is False + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + expected_summary = "Of 3 required tests, 2 results missing, 1 test incomplete" + assert res_data["summary"] == expected_summary expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_identifier': result['data']['item'][0], - 'subject_type': result['data']['type'][0], - 'testcase': TASKTRON_RELEASE_CRITICAL_TASKS[0], - 'result_id': result['id'], - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-missing' + "item": {"item": nvr, "type": "koji_build"}, + "subject_identifier": result["data"]["item"][0], + "subject_type": result["data"]["type"][0], + "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0], + "result_id": result["id"], + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-missing", }, ] + [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'testcase': name, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, - } for name in TASKTRON_RELEASE_CRITICAL_TASKS[1:] + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "testcase": name, + "type": "test-result-missing", + "scenario": None, + "source": None, + } + for name in TASKTRON_RELEASE_CRITICAL_TASKS[1:] ] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements -def test_make_a_decision_on_no_results(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_on_no_results( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - expected_summary = 'Of 3 required tests, 3 results missing' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is False + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + expected_summary = "Of 3 required tests, 3 results missing" + assert res_data["summary"] == expected_summary expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'testcase': name, - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, - } for name in TASKTRON_RELEASE_CRITICAL_TASKS + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "testcase": name, + "type": "test-result-missing", + "scenario": None, + "source": None, + } + for name in TASKTRON_RELEASE_CRITICAL_TASKS ] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements -def test_make_a_decision_on_redhat_cont_image(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_on_redhat_cont_image( + requests_session, greenwave_server, testdatabuilder +): item1_nvr = testdatabuilder.unique_nvr() # _type='koji-build' - result1 = testdatabuilder.create_result(item=item1_nvr, - testcase_name='test.testcase1', - outcome='FAILED') - result2 = testdatabuilder.create_result(item=item1_nvr, - testcase_name='test.testcase2', - outcome='FAILED', _type='redhat-container-image', - nvr=item1_nvr) + result1 = testdatabuilder.create_result( + item=item1_nvr, testcase_name="test.testcase1", outcome="FAILED" + ) + result2 = testdatabuilder.create_result( + item=item1_nvr, + testcase_name="test.testcase2", + outcome="FAILED", + _type="redhat-container-image", + nvr=item1_nvr, + ) data = { - 'product_version': 'fedora-26', - 'subject_type': 'redhat-container-image', - 'subject_identifier': item1_nvr, + "product_version": "fedora-26", + "subject_type": "redhat-container-image", + "subject_identifier": item1_nvr, "rules": [ - { - "type": "PassingTestCaseRule", - "test_case_name": "test.testcase1" - }, - { - "type": "PassingTestCaseRule", - "test_case_name": "test.testcase2" - } - ] + {"type": "PassingTestCaseRule", "test_case_name": "test.testcase1"}, + {"type": "PassingTestCaseRule", "test_case_name": "test.testcase2"}, + ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - expected_summary = 'Of 2 required tests, 2 tests failed' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is False + expected_summary = "Of 2 required tests, 2 tests failed" + assert res_data["summary"] == expected_summary expected_unsatisfied_requirements = [ { - 'item': {'item': item1_nvr, 'type': 'redhat-container-image'}, - 'subject_identifier': result1['data']['item'][0], - 'subject_type': 'redhat-container-image', - 'result_id': result1['id'], - 'testcase': 'test.testcase1', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-failed' + "item": {"item": item1_nvr, "type": "redhat-container-image"}, + "subject_identifier": result1["data"]["item"][0], + "subject_type": "redhat-container-image", + "result_id": result1["id"], + "testcase": "test.testcase1", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-failed", }, { - 'item': {'item': item1_nvr, 'type': 'redhat-container-image'}, - 'subject_identifier': result2['data']['item'][0], - 'subject_type': 'redhat-container-image', - 'result_id': result2['id'], - 'testcase': 'test.testcase2', - 'type': 'test-result-failed', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, - } + "item": {"item": item1_nvr, "type": "redhat-container-image"}, + "subject_identifier": result2["data"]["item"][0], + "subject_type": "redhat-container-image", + "result_id": result2["id"], + "testcase": "test.testcase2", + "type": "test-result-failed", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, + }, ] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements def test_subject_type_group(requests_session, greenwave_server, testdatabuilder): - results_item = 'sha256:' + sha256(os.urandom(50)).hexdigest() + results_item = "sha256:" + sha256(os.urandom(50)).hexdigest() testdatabuilder.create_result( - item=results_item, testcase_name='testcase_name', outcome='PASSED', _type='group' + item=results_item, + testcase_name="testcase_name", + outcome="PASSED", + _type="group", ) data = { - 'decision_context': 'compose_test_scenario_group', - 'product_version': 'fedora-30', - 'subject_type': 'group', - 'subject_identifier': results_item, + "decision_context": "compose_test_scenario_group", + "product_version": "fedora-30", + "subject_type": "group", + "subject_identifier": results_item, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['satisfied_requirements'][0]['testcase'] == 'testcase_name' - assert res_data['satisfied_requirements'][0]['type'] == 'test-result-passed' - assert res_data['policies_satisfied'] is True + assert res_data["satisfied_requirements"][0]["testcase"] == "testcase_name" + assert res_data["satisfied_requirements"][0]["type"] == "test-result-passed" + assert res_data["policies_satisfied"] is True - expected_summary = 'All required tests (1 total) have passed or been waived' - assert res_data['summary'] == expected_summary + expected_summary = "All required tests (1 total) have passed or been waived" + assert res_data["summary"] == expected_summary def test_empty_policy_is_always_satisfied( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-24', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2000-abcdef01', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-24", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2000-abcdef01", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert res_data['applicable_policies'] == ['empty-policy'] - expected_summary = 'No tests are required' - assert res_data['summary'] == expected_summary - assert res_data['unsatisfied_requirements'] == [] + assert res_data["policies_satisfied"] is True + assert res_data["applicable_policies"] == ["empty-policy"] + expected_summary = "No tests are required" + assert res_data["summary"] == expected_summary + assert res_data["unsatisfied_requirements"] == [] def test_bodhi_push_update_stable_policy( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - expected_summary = 'All required tests (3 total) have passed or been waived' - assert res_data['summary'] == expected_summary - assert res_data['unsatisfied_requirements'] == [] + assert res_data["policies_satisfied"] is True + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + expected_summary = "All required tests (3 total) have passed or been waived" + assert res_data["summary"] == expected_summary + assert res_data["unsatisfied_requirements"] == [] def test_bodhi_nonexistent_bodhi_update_policy( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2000-deadbeaf', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2000-deadbeaf", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert res_data['applicable_policies'] == [] - assert res_data['summary'] == 'No tests are required' - assert res_data['unsatisfied_requirements'] == [] + assert res_data["policies_satisfied"] is True + assert res_data["applicable_policies"] == [] + assert res_data["summary"] == "No tests are required" + assert res_data["unsatisfied_requirements"] == [] def test_multiple_results_in_a_subject( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): """ This makes sure that Greenwave uses the latest test result when a subject has multiple test restuls. """ nvr = testdatabuilder.unique_nvr() - testdatabuilder.create_result(item=nvr, - testcase_name='dist.abicheck', - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name="dist.abicheck", outcome="PASSED" + ) # create one failed test result for dist.abicheck - result = testdatabuilder.create_result(item=nvr, - testcase_name='dist.abicheck', - outcome='FAILED') + result = testdatabuilder.create_result( + item=nvr, testcase_name="dist.abicheck", outcome="FAILED" + ) # the rest passed for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() # The failed result should be taken into account. - assert res_data['policies_satisfied'] is False - assert 'taskotron_release_critical_tasks' in res_data['applicable_policies'] - assert 'taskotron_release_critical_tasks_with_blocklist' in res_data['applicable_policies'] - assert res_data['summary'] == 'Of 3 required tests, 1 test failed' + assert res_data["policies_satisfied"] is False + assert "taskotron_release_critical_tasks" in res_data["applicable_policies"] + assert ( + "taskotron_release_critical_tasks_with_blocklist" + in res_data["applicable_policies"] + ) + assert res_data["summary"] == "Of 3 required tests, 1 test failed" expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_identifier': nvr, - 'subject_type': 'koji_build', - 'result_id': result['id'], - 'testcase': 'dist.abicheck', - 'type': 'test-result-failed', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, + "item": {"item": nvr, "type": "koji_build"}, + "subject_identifier": nvr, + "subject_type": "koji_build", + "result_id": result["id"], + "testcase": "dist.abicheck", + "type": "test-result-failed", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, }, ] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements def test_ignore_result(requests_session, greenwave_server, testdatabuilder): @@ -747,133 +824,136 @@ def test_ignore_result(requests_session, greenwave_server, testdatabuilder): """ nvr = testdatabuilder.unique_nvr() for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) result = testdatabuilder.create_result( - item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='PASSED') + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True # Ignore one passing result - data.update({ - 'ignore_result': [result['id']] - }) - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + data.update({"ignore_result": [result["id"]]}) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'testcase': TASKTRON_RELEASE_CRITICAL_TASKS[0], - 'type': 'test-result-missing', - 'scenario': None, - 'source': None, + "item": {"item": nvr, "type": "koji_build"}, + "subject_type": "koji_build", + "subject_identifier": nvr, + "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0], + "type": "test-result-missing", + "scenario": None, + "source": None, }, ] assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["policies_satisfied"] is False + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements # repeating the test for "when" parameter instead of "ignore_result" # ...we should get the same behaviour. - del data['ignore_result'] - data['when'] = right_before_this_time(result['submit_time']) - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + del data["ignore_result"] + data["when"] = right_before_this_time(result["submit_time"]) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["policies_satisfied"] is False + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements def test_make_a_decision_on_passed_result_with_scenario( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): """ If we require two scenarios to pass, and both pass, then we pass. """ compose_id = testdatabuilder.unique_compose_id() - testcase_name = 'compose.install_no_user' - for outcome in ('QUEUED', 'PASSED'): + testcase_name = "compose.install_no_user" + for outcome in ("QUEUED", "PASSED"): for scenario in OPENQA_SCENARIOS: testdatabuilder.create_compose_result( compose_id=compose_id, testcase_name=testcase_name, scenario=scenario, - outcome=outcome) + outcome=outcome, + ) data = { - 'decision_context': 'rawhide_compose_sync_to_mirrors', - 'product_version': 'fedora-rawhide', - 'subject_type': 'compose', - 'subject_identifier': compose_id, + "decision_context": "rawhide_compose_sync_to_mirrors", + "product_version": "fedora-rawhide", + "subject_type": "compose", + "subject_identifier": compose_id, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert res_data['applicable_policies'] == ['openqa_important_stuff_for_rawhide'] - expected_summary = 'All required tests (2 total) have passed or been waived' - assert res_data['summary'] == expected_summary + assert res_data["policies_satisfied"] is True + assert res_data["applicable_policies"] == ["openqa_important_stuff_for_rawhide"] + expected_summary = "All required tests (2 total) have passed or been waived" + assert res_data["summary"] == expected_summary def test_make_a_decision_on_failing_result_with_scenario( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): """ If we require two scenarios to pass, and one is failing, then we fail. """ compose_id = testdatabuilder.unique_compose_id() - testcase_name = 'compose.install_no_user' + testcase_name = "compose.install_no_user" outcomes = [ - ('QUEUED', OPENQA_SCENARIOS[0]), - ('QUEUED', OPENQA_SCENARIOS[1]), - ('PASSED', OPENQA_SCENARIOS[0]), - ('FAILED', OPENQA_SCENARIOS[1]), + ("QUEUED", OPENQA_SCENARIOS[0]), + ("QUEUED", OPENQA_SCENARIOS[1]), + ("PASSED", OPENQA_SCENARIOS[0]), + ("FAILED", OPENQA_SCENARIOS[1]), ] results = [ testdatabuilder.create_compose_result( compose_id=compose_id, testcase_name=testcase_name, scenario=scenario, - outcome=outcome) + outcome=outcome, + ) for outcome, scenario in outcomes ] data = { - 'decision_context': 'rawhide_compose_sync_to_mirrors', - 'product_version': 'fedora-rawhide', - 'subject_type': 'compose', - 'subject_identifier': compose_id, + "decision_context": "rawhide_compose_sync_to_mirrors", + "product_version": "fedora-rawhide", + "subject_type": "compose", + "subject_identifier": compose_id, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is False - assert res_data['applicable_policies'] == ['openqa_important_stuff_for_rawhide'] - expected_summary = 'Of 2 required tests, 1 test failed' - assert res_data['summary'] == expected_summary - expected_unsatisfied_requirements = [{ - 'item': {'productmd.compose.id': compose_id}, - 'subject_identifier': compose_id, - 'subject_type': 'compose', - 'result_id': results[-1]['id'], - 'testcase': testcase_name, - 'type': 'test-result-failed', - 'scenario': 'scenario2', - 'system_architecture': None, - 'system_variant': None, - 'source': None, - }] - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["policies_satisfied"] is False + assert res_data["applicable_policies"] == ["openqa_important_stuff_for_rawhide"] + expected_summary = "Of 2 required tests, 1 test failed" + assert res_data["summary"] == expected_summary + expected_unsatisfied_requirements = [ + { + "item": {"productmd.compose.id": compose_id}, + "subject_identifier": compose_id, + "subject_type": "compose", + "result_id": results[-1]["id"], + "testcase": testcase_name, + "type": "test-result-failed", + "scenario": "scenario2", + "system_architecture": None, + "system_variant": None, + "source": None, + } + ] + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements def test_ignore_waiver(requests_session, greenwave_server, testdatabuilder): @@ -881,74 +961,75 @@ def test_ignore_waiver(requests_session, greenwave_server, testdatabuilder): This tests that a waiver can be ignored when making the decision. """ nvr = testdatabuilder.unique_nvr() - result = testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED') + result = testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) # The rest passed for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') - waiver = testdatabuilder.create_waiver(nvr=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - product_version='fedora-26', - comment='This is fine') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + waiver = testdatabuilder.create_waiver( + nvr=nvr, + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + product_version="fedora-26", + comment="This is fine", + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r_ = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r_ = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r_.status_code == 200 res_data = r_.json() - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True # Ignore the waiver - data.update({ - 'ignore_waiver': [waiver['id']] - }) - r_ = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + data.update({"ignore_waiver": [waiver["id"]]}) + r_ = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r_.status_code == 200 res_data = r_.json() expected_unsatisfied_requirements = [ { - 'item': {'item': nvr, 'type': 'koji_build'}, - 'subject_identifier': nvr, - 'subject_type': 'koji_build', - 'result_id': result['id'], - 'testcase': TASKTRON_RELEASE_CRITICAL_TASKS[0], - 'type': 'test-result-failed', - 'scenario': None, - 'system_architecture': None, - 'system_variant': None, - 'source': None, + "item": {"item": nvr, "type": "koji_build"}, + "subject_identifier": nvr, + "subject_type": "koji_build", + "result_id": result["id"], + "testcase": TASKTRON_RELEASE_CRITICAL_TASKS[0], + "type": "test-result-failed", + "scenario": None, + "system_architecture": None, + "system_variant": None, + "source": None, }, ] - assert res_data['policies_satisfied'] is False - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["policies_satisfied"] is False + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements # repeating the test for "when" parameter instead of "ignore_waiver" # ...we should get the same behaviour. - del data['ignore_waiver'] - data['when'] = right_before_this_time(waiver['timestamp']) - r_ = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + del data["ignore_waiver"] + data["when"] = right_before_this_time(waiver["timestamp"]) + r_ = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r_.status_code == 200 res_data = r_.json() - assert res_data['policies_satisfied'] is False - assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + assert res_data["policies_satisfied"] is False + assert res_data["unsatisfied_requirements"] == expected_unsatisfied_requirements def test_distgit_server(requests_session, distgit_server, tmpdir): - """ This test is checking if the distgit server is working. - Check that the file is present and that the server is running. + """This test is checking if the distgit server is working. + Check that the file is present and that the server is running. """ - r_ = requests_session.head(distgit_server, headers={'Content-Type': 'application/json'}, - timeout=60) + r_ = requests_session.head( + distgit_server, headers={"Content-Type": "application/json"}, timeout=60 + ) assert r_.status_code == 200 def test_cached_false_positive(requests_session, greenwave_server, testdatabuilder): - """ Test that caching without invalidation produces false positives. + """Test that caching without invalidation produces false positives. This just tests that our caching works in the first place. - Check a decision, it passes. @@ -959,97 +1040,107 @@ def test_cached_false_positive(requests_session, greenwave_server, testdatabuild """ nvr = testdatabuilder.unique_nvr() for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2000-abcdef01', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2000-abcdef01", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True # Now, insert a *failing* result. The cache should return the old results # that exclude the failing one (erroneously). - testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[-1], - outcome='FAILED') - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[-1], outcome="FAILED" + ) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True def test_blocklist(requests_session, greenwave_server, testdatabuilder): """ Test that packages on the blocklist will be excluded when applying the policy. """ - nvr = 'firefox-1.0-1.el7' - testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED') + nvr = "firefox-1.0-1.el7" + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED') + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2000-abcdef01', + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2000-abcdef01", } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() # the failed test result of dist.abicheck should be ignored and thus the policy # is satisfied. - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True def test_excluded_packages(requests_session, greenwave_server, testdatabuilder): """ Test that packages in the excluded_packages list will be excluded when applying the policy. """ - nvr = testdatabuilder.unique_nvr(name='module-build-service') + nvr = testdatabuilder.unique_nvr(name="module-build-service") testdatabuilder.create_koji_build_result( - nvr=nvr, testcase_name='osci.brew-build.tier0.functional', - outcome='FAILED', type_='brew-build') + nvr=nvr, + testcase_name="osci.brew-build.tier0.functional", + outcome="FAILED", + type_="brew-build", + ) data = { - 'decision_context': 'osci_compose_gate', - 'product_version': 'rhel-something', - 'subject': [{'type': 'brew-build', 'item': nvr}], + "decision_context": "osci_compose_gate", + "product_version": "rhel-something", + "subject": [{"type": "brew-build", "item": nvr}], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() # the failed test result of sci.brew-build.tier0.functiona should be ignored and thus the # policy is satisfied. - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True -def test_make_a_decision_about_brew_build(requests_session, greenwave_server, testdatabuilder): +def test_make_a_decision_about_brew_build( + requests_session, greenwave_server, testdatabuilder +): # The 'brew-build' type is used internally within Red Hat. We treat it as # the 'koji_build' subject type. - nvr = testdatabuilder.unique_nvr(name='avahi') + nvr = testdatabuilder.unique_nvr(name="avahi") testdatabuilder.create_koji_build_result( - nvr=nvr, testcase_name='osci.brew-build.tier0.functional', - outcome='PASSED', type_='brew-build') + nvr=nvr, + testcase_name="osci.brew-build.tier0.functional", + outcome="PASSED", + type_="brew-build", + ) data = { - 'decision_context': 'osci_compose_gate', - 'product_version': 'rhel-something', - 'subject': [{'type': 'brew-build', 'item': nvr}], + "decision_context": "osci_compose_gate", + "product_version": "rhel-something", + "subject": [{"type": "brew-build", "item": nvr}], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert res_data['applicable_policies'] == ['osci_compose'] - assert res_data['summary'] == 'All required tests (1 total) have passed or been waived' + assert res_data["policies_satisfied"] is True + assert res_data["applicable_policies"] == ["osci_compose"] + assert ( + res_data["summary"] == "All required tests (1 total) have passed or been waived" + ) def test_validate_gating_yaml_valid(requests_session, greenwave_server): @@ -1063,14 +1154,15 @@ def test_validate_gating_yaml_valid(requests_session, greenwave_server): - !PassingTestCaseRule {test_case_name: test} """) result = requests_session.post( - greenwave_server + 'api/v1.0/validate-gating-yaml', data=gating_yaml) - assert result.json().get('message') == 'All OK' + greenwave_server + "api/v1.0/validate-gating-yaml", data=gating_yaml + ) + assert result.json().get("message") == "All OK" assert result.status_code == 200 def test_validate_gating_yaml_empty(requests_session, greenwave_server): - result = requests_session.post(greenwave_server + 'api/v1.0/validate-gating-yaml') - assert result.json().get('message') == 'No policies defined' + result = requests_session.post(greenwave_server + "api/v1.0/validate-gating-yaml") + assert result.json().get("message") == "No policies defined" assert result.status_code == 400 @@ -1088,10 +1180,12 @@ def test_validate_gating_yaml_obsolete_rule(requests_session, greenwave_server): } """) result = requests_session.post( - greenwave_server + 'api/v1.0/validate-gating-yaml', data=gating_yaml) - assert result.json().get('message') == ( - 'Policy \'test\': Attribute \'rules\': !PackageSpecificBuild is obsolete. ' - 'Please use the "packages" allowlist instead.') + greenwave_server + "api/v1.0/validate-gating-yaml", data=gating_yaml + ) + assert result.json().get("message") == ( + "Policy 'test': Attribute 'rules': !PackageSpecificBuild is obsolete. " + 'Please use the "packages" allowlist instead.' + ) assert result.status_code == 400 @@ -1106,11 +1200,14 @@ def test_validate_gating_yaml_missing_optional_tag(requests_session, greenwave_s - !PassingTestCaseRule {test_case_name: test} """) result = requests_session.post( - greenwave_server + 'api/v1.0/validate-gating-yaml', data=gating_yaml) + greenwave_server + "api/v1.0/validate-gating-yaml", data=gating_yaml + ) assert result.status_code == 200 -def test_validate_gating_yaml_missing_decision_context(requests_session, greenwave_server): +def test_validate_gating_yaml_missing_decision_context( + requests_session, greenwave_server +): gating_yaml = dedent(""" --- !Policy id: "test" @@ -1129,117 +1226,125 @@ def test_validate_gating_yaml_missing_decision_context(requests_session, greenwa - !PassingTestCaseRule {test_case_name: test_2} """) result = requests_session.post( - greenwave_server + 'api/v1.0/validate-gating-yaml', data=gating_yaml) - assert result.json().get('message') == ('Greenwave could not find a parent policy(ies) for ' - 'following decision context(s): ' - 'test_missing_1, test_missing_2. Please change your' - ' policy so that it will match a decision' - ' context in the parent policies.') + greenwave_server + "api/v1.0/validate-gating-yaml", data=gating_yaml + ) + assert result.json().get("message") == ( + "Greenwave could not find a parent policy(ies) for " + "following decision context(s): " + "test_missing_1, test_missing_2. Please change your" + " policy so that it will match a decision" + " context in the parent policies." + ) assert result.status_code == 200 -@pytest.mark.parametrize(('variant1', 'variant2'), ( +@pytest.mark.parametrize( + ("variant1", "variant2"), ( - dict(variant='BaseOS', architecture='ppc64'), - dict(variant='BaseOS', architecture='x86_64'), + ( + dict(variant="BaseOS", architecture="ppc64"), + dict(variant="BaseOS", architecture="x86_64"), + ), + ( + dict(variant="BaseOS", architecture="ppc64"), + dict(variant="Cloud_Base", architecture="ppc64"), + ), ), - ( - dict(variant='BaseOS', architecture='ppc64'), - dict(variant='Cloud_Base', architecture='ppc64'), - ), -)) +) def test_make_a_decision_about_compose_all_variants_architectures( - variant1, variant2, requests_session, greenwave_server, testdatabuilder): + variant1, variant2, requests_session, greenwave_server, testdatabuilder +): compose_id = testdatabuilder.unique_compose_id() failed_results = testdatabuilder.create_rtt_compose_result( - compose_id=compose_id, - outcome='FAILED', - **variant1) + compose_id=compose_id, outcome="FAILED", **variant1 + ) testdatabuilder.create_rtt_compose_result( - compose_id=compose_id, - outcome='PASSED', - **variant2) + compose_id=compose_id, outcome="PASSED", **variant2 + ) data = { - 'decision_context': 'rtt_compose_gate', - 'product_version': 'rhel-something', - 'subject_type': 'compose', - 'subject_identifier': compose_id, + "decision_context": "rtt_compose_gate", + "product_version": "rhel-something", + "subject_type": "compose", + "subject_identifier": compose_id, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert not res_data['policies_satisfied'] - assert res_data['unsatisfied_requirements'] == [{ - 'item': {'productmd.compose.id': compose_id}, - 'subject_identifier': compose_id, - 'subject_type': 'compose', - 'result_id': failed_results['id'], - 'scenario': None, - 'source': None, - 'system_architecture': variant1['architecture'], - 'system_variant': variant1['variant'], - 'testcase': 'rtt.acceptance.validation', - 'type': 'test-result-failed' - }] + assert not res_data["policies_satisfied"] + assert res_data["unsatisfied_requirements"] == [ + { + "item": {"productmd.compose.id": compose_id}, + "subject_identifier": compose_id, + "subject_type": "compose", + "result_id": failed_results["id"], + "scenario": None, + "source": None, + "system_architecture": variant1["architecture"], + "system_variant": variant1["variant"], + "testcase": "rtt.acceptance.validation", + "type": "test-result-failed", + } + ] def test_make_a_decision_about_compose_latest_variants_architectures( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): compose_id = testdatabuilder.unique_compose_id() testdatabuilder.create_rtt_compose_result( - compose_id=compose_id, - outcome='FAILED', - variant='BaseOS', - architecture='ppc64') + compose_id=compose_id, outcome="FAILED", variant="BaseOS", architecture="ppc64" + ) testdatabuilder.create_rtt_compose_result( - compose_id=compose_id, - outcome='PASSED', - variant='BaseOS', - architecture='ppc64') + compose_id=compose_id, outcome="PASSED", variant="BaseOS", architecture="ppc64" + ) data = { - 'decision_context': 'rtt_compose_gate', - 'product_version': 'rhel-something', - 'subject_type': 'compose', - 'subject_identifier': compose_id, + "decision_context": "rtt_compose_gate", + "product_version": "rhel-something", + "subject_type": "compose", + "subject_identifier": compose_id, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] - assert len(res_data['satisfied_requirements']) == 1 + assert res_data["policies_satisfied"] + assert len(res_data["satisfied_requirements"]) == 1 def test_make_a_decision_about_compose_new_variants_architectures( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): compose_id = testdatabuilder.unique_compose_id() - for arch, outcome in [('ppc64', 'FAILED'), ('ppc64', 'PASSED'), ('x86_64', 'PASSED')]: + for arch, outcome in [ + ("ppc64", "FAILED"), + ("ppc64", "PASSED"), + ("x86_64", "PASSED"), + ]: testdatabuilder.create_rtt_compose_result( - compose_id=compose_id, - variant='BaseOS', - architecture=arch, - outcome=outcome) + compose_id=compose_id, variant="BaseOS", architecture=arch, outcome=outcome + ) data = { - 'decision_context': 'rtt_compose_gate', - 'product_version': 'rhel-something', - 'subject_type': 'compose', - 'subject_identifier': compose_id, + "decision_context": "rtt_compose_gate", + "product_version": "rhel-something", + "subject_type": "compose", + "subject_identifier": compose_id, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] + assert res_data["policies_satisfied"] def test_make_a_decision_for_bodhi_with_verbose_flag( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): """ Bodhi uses verbose flag to get all results. """ @@ -1251,479 +1356,501 @@ def test_make_a_decision_for_bodhi_with_verbose_flag( results = [] for nvr in reversed(nvrs): for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - results.append(testdatabuilder.create_result( - item=nvr, testcase_name=testcase_name, outcome='PASSED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + ) data = { - 'decision_context': 'bodhi_update_push_stable_with_no_rules', - 'product_version': 'fedora-28', - 'subject': [ - {'type': 'koji_build', 'item': nvr} - for nvr in nvrs - ] + [ - {'type': 'bodhi_update', 'item': 'FEDORA-2000-abcdef01'} - ], - 'verbose': True, + "decision_context": "bodhi_update_push_stable_with_no_rules", + "product_version": "fedora-28", + "subject": [{"type": "koji_build", "item": nvr} for nvr in nvrs] + + [{"type": "bodhi_update", "item": "FEDORA-2000-abcdef01"}], + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == len(TASKTRON_RELEASE_CRITICAL_TASKS) * len(nvrs) - assert drop_hrefs(res_data['results']) == drop_hrefs(list(reversed(results))) - assert res_data['waivers'] == [] - assert res_data['satisfied_requirements'] == [] + assert len(res_data["results"]) == len(TASKTRON_RELEASE_CRITICAL_TASKS) * len(nvrs) + assert drop_hrefs(res_data["results"]) == drop_hrefs(list(reversed(results))) + assert res_data["waivers"] == [] + assert res_data["satisfied_requirements"] == [] def test_decision_on_redhat_module(requests_session, greenwave_server, testdatabuilder): - nvr = '389-ds-1.4-820181127205924.9edba152' + nvr = "389-ds-1.4-820181127205924.9edba152" new_result_data = { - 'testcase': {'name': 'baseos-ci.redhat-module.tier0.functional'}, - 'outcome': 'PASSED', - 'data': {'item': nvr, 'type': 'redhat-module'} + "testcase": {"name": "baseos-ci.redhat-module.tier0.functional"}, + "outcome": "PASSED", + "data": {"item": nvr, "type": "redhat-module"}, } - result = testdatabuilder._create_result(new_result_data) # noqa + result = testdatabuilder._create_result(new_result_data) # noqa data = { - 'decision_context': 'osci_compose_gate_modules', - 'product_version': 'rhel-8', - 'subject_type': 'redhat-module', - 'subject_identifier': nvr, - 'verbose': True + "decision_context": "osci_compose_gate_modules", + "product_version": "rhel-8", + "subject_type": "redhat-module", + "subject_identifier": nvr, + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - expected_summary = 'All required tests (1 total) have passed or been waived' - assert res_data['summary'] == expected_summary - assert res_data['results'][0]['data']['type'][0] == 'redhat-module' + assert res_data["policies_satisfied"] is True + expected_summary = "All required tests (1 total) have passed or been waived" + assert res_data["summary"] == expected_summary + assert res_data["results"][0]["data"]["type"][0] == "redhat-module" -def test_verbose_retrieve_latest_results(requests_session, greenwave_server, testdatabuilder): +def test_verbose_retrieve_latest_results( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() - for outcome in ['FAILED', 'PASSED']: + for outcome in ["FAILED", "PASSED"]: for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome=outcome) + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome=outcome + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'verbose': True + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - expected_summary = 'All required tests (3 total) have passed or been waived' - assert res_data['summary'] == expected_summary - assert len(res_data['results']) == 3 - for result in res_data['results']: - assert result['outcome'] == 'PASSED' + assert res_data["policies_satisfied"] is True + expected_summary = "All required tests (3 total) have passed or been waived" + assert res_data["summary"] == expected_summary + assert len(res_data["results"]) == 3 + for result in res_data["results"]: + assert result["outcome"] == "PASSED" def test_make_decision_passed_on_subject_type_bodhi_with_waiver( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() # First one failed but was waived - testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED', - _type='bodhi_update') - testdatabuilder.create_waiver(nvr=nvr, - product_version='fedora-26', - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - comment='This is fine', - subject_type='bodhi_update') + testdatabuilder.create_result( + item=nvr, + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + outcome="FAILED", + _type="bodhi_update", + ) + testdatabuilder.create_waiver( + nvr=nvr, + product_version="fedora-26", + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + comment="This is fine", + subject_type="bodhi_update", + ) for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED', - _type='bodhi_update') + testdatabuilder.create_result( + item=nvr, + testcase_name=testcase_name, + outcome="PASSED", + _type="bodhi_update", + ) data = { - 'decision_context': 'bodhi_update_push', - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': nvr, + "decision_context": "bodhi_update_push", + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True + assert res_data["policies_satisfied"] is True - assert res_data['applicable_policies'] == [ - 'bodhi-test-policy', + assert res_data["applicable_policies"] == [ + "bodhi-test-policy", ] - expected_summary = 'All required tests (3 total) have passed or been waived' - assert res_data['summary'] == expected_summary + expected_summary = "All required tests (3 total) have passed or been waived" + assert res_data["summary"] == expected_summary def test_make_a_decision_with_verbose_flag_all_results_returned( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() results = [] expected_waivers = [] # First one failed but was waived - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) + ) expected_waivers.append( - testdatabuilder.create_waiver(nvr=nvr, - product_version='fedora-30', - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - comment='This is fine')) + testdatabuilder.create_waiver( + nvr=nvr, + product_version="fedora-30", + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + comment="This is fine", + ) + ) for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + ) data = { - 'decision_context': 'koji_build_push_missing_results', - 'product_version': 'fedora-30', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'verbose': True, + "decision_context": "koji_build_push_missing_results", + "product_version": "fedora-30", + "subject_type": "koji_build", + "subject_identifier": nvr, + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == len(results) - assert drop_hrefs(res_data['results']) == drop_hrefs(list(reversed(results))) - assert len(res_data['waivers']) == len(expected_waivers) - assert res_data['waivers'] == expected_waivers + assert len(res_data["results"]) == len(results) + assert drop_hrefs(res_data["results"]) == drop_hrefs(list(reversed(results))) + assert len(res_data["waivers"]) == len(expected_waivers) + assert res_data["waivers"] == expected_waivers -def test_verbose_retrieve_latest_results_scenario(requests_session, greenwave_server, - testdatabuilder): +def test_verbose_retrieve_latest_results_scenario( + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() results = [] - for scenario in ['fedora.universal.x86_64.uefi', 'fedora.universal.x86_64.64bit']: - results.append(testdatabuilder.create_compose_result(compose_id=nvr, - testcase_name='testcase_name', outcome='PASSED', scenario=scenario)) + for scenario in ["fedora.universal.x86_64.uefi", "fedora.universal.x86_64.64bit"]: + results.append( + testdatabuilder.create_compose_result( + compose_id=nvr, + testcase_name="testcase_name", + outcome="PASSED", + scenario=scenario, + ) + ) data = { - 'decision_context': 'compose_test_scenario', - 'product_version': 'fedora-29', - 'subject_type': 'compose', - 'subject_identifier': nvr, - 'verbose': True + "decision_context": "compose_test_scenario", + "product_version": "fedora-29", + "subject_type": "compose", + "subject_identifier": nvr, + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - expected_summary = 'All required tests (2 total) have passed or been waived' - assert res_data['summary'] == expected_summary - assert len(res_data['results']) == 2 - for result in res_data['results']: - assert result['outcome'] == 'PASSED' + assert res_data["policies_satisfied"] is True + expected_summary = "All required tests (2 total) have passed or been waived" + assert res_data["summary"] == expected_summary + assert len(res_data["results"]) == 2 + for result in res_data["results"]: + assert result["outcome"] == "PASSED" def test_api_returns_not_repeated_waiver_in_verbose_info( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): """ This tests that the API doesn't return repeated waivers when the flag verbose==True """ nvr = testdatabuilder.unique_nvr() - testdatabuilder.create_waiver(nvr=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - product_version='fedora-26', - comment='This is fine') + testdatabuilder.create_waiver( + nvr=nvr, + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + product_version="fedora-26", + comment="This is fine", + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject': [ - {'item': nvr, 'type': 'koji_build'}, - {'original_spec_nvr': nvr}, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject": [ + {"item": nvr, "type": "koji_build"}, + {"original_spec_nvr": nvr}, ], - 'verbose': True + "verbose": True, } - r_ = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r_ = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r_.status_code == 200 res_data = r_.json() - assert len(res_data['waivers']) == 1 + assert len(res_data["waivers"]) == 1 def test_api_with_when(requests_session, greenwave_server, testdatabuilder): nvr = testdatabuilder.unique_nvr() results = [] - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[1], - outcome='PASSED')) - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[2], - outcome='FAILED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[1], outcome="PASSED" + ) + ) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[2], outcome="FAILED" + ) + ) data = { - 'decision_context': 'bodhi_update_push_stable', - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'when': right_before_this_time(results[1]['submit_time']), - 'verbose': True, + "decision_context": "bodhi_update_push_stable", + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, + "when": right_before_this_time(results[1]["submit_time"]), + "verbose": True, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == 1 - assert drop_hrefs(res_data['results']) == drop_hrefs([results[0]]) + assert len(res_data["results"]) == 1 + assert drop_hrefs(res_data["results"]) == drop_hrefs([results[0]]) - del data['when'] - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + del data["when"] + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == 2 + assert len(res_data["results"]) == 2 @pytest.mark.smoke def test_cannot_make_decision_with_both_decision_context_and_user_policies( - requests_session, greenwave_server): + requests_session, greenwave_server +): data = { - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', - 'decision_context': 'koji_build_push_missing_results', - 'rules': [ + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2018-ec7cb4d5eb", + "decision_context": "koji_build_push_missing_results", + "rules": [ { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'osci.brew-build.rpmdeplint.functional' + "type": "PassingTestCaseRule", + "test_case_name": "osci.brew-build.rpmdeplint.functional", }, ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 - assert ('Cannot have both decision_context and rules') == r.json()['message'] + assert ("Cannot have both decision_context and rules") == r.json()["message"] @pytest.mark.smoke def test_cannot_make_decision_without_required_rule_type( - requests_session, greenwave_server): + requests_session, greenwave_server +): data = { - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', - 'rules': [ - { - 'typo': 'PassingTestCaseRule', - 'test_case_name': 'dist.abicheck' - }, - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.rpmdeplint' - }, + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2018-ec7cb4d5eb", + "rules": [ + {"typo": "PassingTestCaseRule", "test_case_name": "dist.abicheck"}, + {"type": "PassingTestCaseRule", "test_case_name": "dist.rpmdeplint"}, ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 assert ( "Failed to parse on demand policy: Attribute 'rules': " "Key 'type' is required for each list item" - ) == r.json()['message'] + ) == r.json()["message"] @pytest.mark.smoke def test_cannot_make_decision_without_required_rule_testcase_name( - requests_session, greenwave_server): + requests_session, greenwave_server +): data = { - 'product_version': 'fedora-26', - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2018-ec7cb4d5eb', - 'rules': [ - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.abicheck' - }, - { - 'type': 'PassingTestCaseRule' - }, + "product_version": "fedora-26", + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2018-ec7cb4d5eb", + "rules": [ + {"type": "PassingTestCaseRule", "test_case_name": "dist.abicheck"}, + {"type": "PassingTestCaseRule"}, ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 400 assert ( "Failed to parse on demand policy: Attribute 'rules': " "Attribute 'test_case_name' is required" - ) == r.json()['message'] + ) == r.json()["message"] def test_make_a_decision_with_verbose_flag_on_demand_policy( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() results = [] expected_waivers = [] # First one failed but was waived - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) + ) expected_waivers.append( - testdatabuilder.create_waiver(nvr=nvr, - product_version='fedora-31', - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - comment='This is fine')) + testdatabuilder.create_waiver( + nvr=nvr, + product_version="fedora-31", + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + comment="This is fine", + ) + ) for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + ) data = { - 'product_version': 'fedora-31', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'verbose': True, - 'rules': [ - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.abicheck' - }, - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.rpmdeplint' - }, - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.upgradepath' - }, + "product_version": "fedora-31", + "subject_type": "koji_build", + "subject_identifier": nvr, + "verbose": True, + "rules": [ + {"type": "PassingTestCaseRule", "test_case_name": "dist.abicheck"}, + {"type": "PassingTestCaseRule", "test_case_name": "dist.rpmdeplint"}, + {"type": "PassingTestCaseRule", "test_case_name": "dist.upgradepath"}, ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['results']) == len(results) - assert drop_hrefs(res_data['results']) == drop_hrefs(list(reversed(results))) - assert len(res_data['waivers']) == len(expected_waivers) - assert res_data['waivers'] == expected_waivers - assert len(res_data['satisfied_requirements']) == len(results) - assert len(res_data['unsatisfied_requirements']) == 0 + assert len(res_data["results"]) == len(results) + assert drop_hrefs(res_data["results"]) == drop_hrefs(list(reversed(results))) + assert len(res_data["waivers"]) == len(expected_waivers) + assert res_data["waivers"] == expected_waivers + assert len(res_data["satisfied_requirements"]) == len(results) + assert len(res_data["unsatisfied_requirements"]) == 0 def test_make_a_decision_on_demand_policy( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() results = [] expected_waivers = [] # First one failed but was waived - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - outcome='FAILED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], outcome="FAILED" + ) + ) expected_waivers.append( - testdatabuilder.create_waiver(nvr=nvr, - product_version='fedora-31', - testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], - comment='This is fine')) + testdatabuilder.create_waiver( + nvr=nvr, + product_version="fedora-31", + testcase_name=TASKTRON_RELEASE_CRITICAL_TASKS[0], + comment="This is fine", + ) + ) for testcase_name in TASKTRON_RELEASE_CRITICAL_TASKS[1:]: - results.append(testdatabuilder.create_result(item=nvr, - testcase_name=testcase_name, - outcome='PASSED')) + results.append( + testdatabuilder.create_result( + item=nvr, testcase_name=testcase_name, outcome="PASSED" + ) + ) data = { - 'id': 'on_demand', - 'product_version': 'fedora-31', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'rules': [ - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.abicheck' - }, - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.rpmdeplint' - }, - { - 'type': 'PassingTestCaseRule', - 'test_case_name': 'dist.upgradepath' - }, + "id": "on_demand", + "product_version": "fedora-31", + "subject_type": "koji_build", + "subject_identifier": nvr, + "rules": [ + {"type": "PassingTestCaseRule", "test_case_name": "dist.abicheck"}, + {"type": "PassingTestCaseRule", "test_case_name": "dist.rpmdeplint"}, + {"type": "PassingTestCaseRule", "test_case_name": "dist.upgradepath"}, ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert len(res_data['satisfied_requirements']) == len(results) - assert len(res_data['unsatisfied_requirements']) == 0 + assert len(res_data["satisfied_requirements"]) == len(results) + assert len(res_data["unsatisfied_requirements"]) == 0 @pytest.mark.smoke def test_installed_subject_types(requests_session, greenwave_server): - response = requests_session.get(greenwave_server + 'api/v1.0/subject_types') + response = requests_session.get(greenwave_server + "api/v1.0/subject_types") assert response.status_code == 200 data = response.json() - assert len(data['subject_types']) - assert [x['id'] for x in data['subject_types']] == [ - 'bodhi_update', - 'compose', - 'koji_build', - 'redhat-container-image', - 'redhat-module', + assert len(data["subject_types"]) + assert [x["id"] for x in data["subject_types"]] == [ + "bodhi_update", + "compose", + "koji_build", + "redhat-container-image", + "redhat-module", ] def test_make_a_decision_on_passed_result_with_custom_scenario( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): item1_nvr = testdatabuilder.unique_nvr() testdatabuilder.create_result( - item=item1_nvr, - testcase_name='test.testcase1', - outcome='FAILED' + item=item1_nvr, testcase_name="test.testcase1", outcome="FAILED" ) result2 = testdatabuilder.create_result( item=item1_nvr, - testcase_name='test.testcase1', - outcome='PASSED', - scenario='scenario1', + testcase_name="test.testcase1", + outcome="PASSED", + scenario="scenario1", ) data = { - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': item1_nvr, + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": item1_nvr, "rules": [ { "type": "PassingTestCaseRule", "test_case_name": "test.testcase1", "scenario": "scenario1", } - ] + ], } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['policies_satisfied'] is True - assert res_data['summary'] == 'All required tests (1 total) have passed or been waived' - assert res_data['satisfied_requirements'] == [ + assert res_data["policies_satisfied"] is True + assert ( + res_data["summary"] == "All required tests (1 total) have passed or been waived" + ) + assert res_data["satisfied_requirements"] == [ { - 'subject_identifier': item1_nvr, - 'subject_type': 'koji_build', - 'result_id': result2['id'], - 'testcase': 'test.testcase1', - 'scenario': 'scenario1', - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-passed' + "subject_identifier": item1_nvr, + "subject_type": "koji_build", + "result_id": result2["id"], + "testcase": "test.testcase1", + "scenario": "scenario1", + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-passed", } ] def test_make_a_decision_all_scenarios_waived( - requests_session, greenwave_server, testdatabuilder): + requests_session, greenwave_server, testdatabuilder +): nvr = testdatabuilder.unique_nvr() - scenarios = ('scenario1', 'scenario2') + scenarios = ("scenario1", "scenario2") results = [ testdatabuilder.create_result( item=nvr, - testcase_name='test1', - outcome='FAILED', + testcase_name="test1", + outcome="FAILED", scenario=scenario, ) for scenario in scenarios @@ -1731,38 +1858,44 @@ def test_make_a_decision_all_scenarios_waived( waiver = testdatabuilder.create_waiver( nvr=nvr, - product_version='fedora-26', - testcase_name='test1', - comment='This is fine', + product_version="fedora-26", + testcase_name="test1", + comment="This is fine", ) data = { - 'rules': [ - {"type": "PassingTestCaseRule", "test_case_name": "test1", "scenario": scenario} + "rules": [ + { + "type": "PassingTestCaseRule", + "test_case_name": "test1", + "scenario": scenario, + } for scenario in scenarios ], - 'product_version': 'fedora-26', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, + "product_version": "fedora-26", + "subject_type": "koji_build", + "subject_identifier": nvr, } - r = requests_session.post(greenwave_server + 'api/v1.0/decision', json=data) + r = requests_session.post(greenwave_server + "api/v1.0/decision", json=data) assert r.status_code == 200 res_data = r.json() - assert res_data['unsatisfied_requirements'] == [] - assert res_data['satisfied_requirements'] == [ + assert res_data["unsatisfied_requirements"] == [] + assert res_data["satisfied_requirements"] == [ { - 'subject_identifier': nvr, - 'subject_type': 'koji_build', - 'result_id': result['id'], - 'waiver_id': waiver['id'], - 'testcase': 'test1', - 'scenario': result['data']['scenario'][0], - 'system_architecture': None, - 'system_variant': None, - 'source': None, - 'type': 'test-result-failed-waived' + "subject_identifier": nvr, + "subject_type": "koji_build", + "result_id": result["id"], + "waiver_id": waiver["id"], + "testcase": "test1", + "scenario": result["data"]["scenario"][0], + "system_architecture": None, + "system_variant": None, + "source": None, + "type": "test-result-failed-waived", } for result in results ] - assert res_data['policies_satisfied'] is True - assert res_data['summary'] == 'All required tests (2 total) have passed or been waived' + assert res_data["policies_satisfied"] is True + assert ( + res_data["summary"] == "All required tests (2 total) have passed or been waived" + ) diff --git a/functional-tests/test_cache.py b/functional-tests/test_cache.py index 2cb97412..52f54457 100644 --- a/functional-tests/test_cache.py +++ b/functional-tests/test_cache.py @@ -8,11 +8,11 @@ def test_cache(): cache = make_region(key_mangler=mangle_key) cache.configure( - backend='dogpile.cache.pymemcache', + backend="dogpile.cache.pymemcache", expiration_time=5, arguments={ - 'url': '127.0.0.1:11211', - 'distributed_lock': True, + "url": "127.0.0.1:11211", + "distributed_lock": True, }, ) key = uuid.uuid1().hex diff --git a/functional-tests/test_healthcheck.py b/functional-tests/test_healthcheck.py index 499137ed..1db7ea23 100644 --- a/functional-tests/test_healthcheck.py +++ b/functional-tests/test_healthcheck.py @@ -2,6 +2,6 @@ def test_healthcheck(requests_session, greenwave_server): - r = requests_session.get(greenwave_server + 'healthcheck') + r = requests_session.get(greenwave_server + "healthcheck") assert r.status_code == 200 - assert r.text == 'Health check OK' + assert r.text == "Health check OK" diff --git a/greenwave/__init__.py b/greenwave/__init__.py index d3379e54..c696ae66 100644 --- a/greenwave/__init__.py +++ b/greenwave/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """ Greenwave is a web application built using `Flask`_ and `SQLAlchemy`_. @@ -8,6 +7,7 @@ .. _Flask: http://flask.pocoo.org/ .. _SQLAlchemy: http://sqlalchemy.org/ """ + import importlib.metadata as importlib_metadata try: @@ -20,7 +20,7 @@ except ImportError: import toml as tomllib # type: ignore - with open("pyproject.toml", "r") as f: + with open("pyproject.toml") as f: pyproject = tomllib.load(f) # type: ignore __version__ = pyproject["tool"]["poetry"]["version"] diff --git a/greenwave/api_v1.py b/greenwave/api_v1.py index 1024d055..0a8ff716 100644 --- a/greenwave/api_v1.py +++ b/greenwave/api_v1.py @@ -1,27 +1,28 @@ # SPDX-License-Identifier: GPL-2.0+ import logging -from flask import Blueprint, request, current_app, jsonify, url_for, redirect + +from flask import Blueprint, current_app, jsonify, redirect, request, url_for from werkzeug.exceptions import BadRequest + +import greenwave.decision from greenwave import __version__ +from greenwave.monitor import ( + decision_exception_counter, + decision_request_duration_seconds, +) from greenwave.policies import ( RemotePolicy, _missing_decision_contexts_in_parent_policies, ) from greenwave.safe_yaml import SafeYAMLError from greenwave.utils import insert_headers, jsonp -from greenwave.monitor import ( - decision_exception_counter, - decision_request_duration_seconds, -) -import greenwave.decision - -api = (Blueprint('api_v1', __name__)) +api = Blueprint("api_v1", __name__) log = logging.getLogger(__name__) -@api.route('/', methods=['GET']) +@api.route("/", methods=["GET"]) def landing_page(): """ Landing page with links to documentation and other APIs and interpretation @@ -54,33 +55,35 @@ def landing_page(): } """ # noqa: E501 return ( - jsonify({ - "documentation": current_app.config["DOCUMENTATION_URL"], - "api_v1": current_app.config["GREENWAVE_API_URL"], - "resultsdb_api": current_app.config["RESULTSDB_API_URL"], - "waiverdb_api": current_app.config["WAIVERDB_API_URL"], - "koji_api": current_app.config["KOJI_BASE_URL"], - "outcomes_passed": current_app.config["OUTCOMES_PASSED"], - "outcomes_error": current_app.config["OUTCOMES_ERROR"], - "outcomes_incomplete": current_app.config["OUTCOMES_INCOMPLETE"], - "remote_rule_policies": current_app.config["REMOTE_RULE_POLICIES"], - }), + jsonify( + { + "documentation": current_app.config["DOCUMENTATION_URL"], + "api_v1": current_app.config["GREENWAVE_API_URL"], + "resultsdb_api": current_app.config["RESULTSDB_API_URL"], + "waiverdb_api": current_app.config["WAIVERDB_API_URL"], + "koji_api": current_app.config["KOJI_BASE_URL"], + "outcomes_passed": current_app.config["OUTCOMES_PASSED"], + "outcomes_error": current_app.config["OUTCOMES_ERROR"], + "outcomes_incomplete": current_app.config["OUTCOMES_INCOMPLETE"], + "remote_rule_policies": current_app.config["REMOTE_RULE_POLICIES"], + } + ), 200, ) -@api.route('/version', methods=['GET']) +@api.route("/version", methods=["GET"]) def version(): """ Deprecated in favour of (and redirected to) :http:get:`/api/v1.0/about`. """ - return redirect(url_for('api_v1.about')) + return redirect(url_for("api_v1.about")) -@api.route('/about', methods=['GET']) +@api.route("/about", methods=["GET"]) @jsonp def about(): - """ Returns the current running version. + """Returns the current running version. **Sample response**: @@ -98,16 +101,16 @@ def about(): :statuscode 200: Currently running greenwave software version is returned. """ - resp = jsonify({'version': __version__}) + resp = jsonify({"version": __version__}) resp = insert_headers(resp) resp.status_code = 200 return resp -@api.route('/policies', methods=['GET']) +@api.route("/policies", methods=["GET"]) @jsonp def get_policies(): - """ Returns all currently loaded policies. + """Returns all currently loaded policies. **Sample response**: @@ -147,34 +150,34 @@ def get_policies(): :statuscode 200: Currently loaded policies are returned. """ - policies = [policy.to_json() for policy in current_app.config['policies']] - resp = jsonify({'policies': policies}) + policies = [policy.to_json() for policy in current_app.config["policies"]] + resp = jsonify({"policies": policies}) resp = insert_headers(resp) resp.status_code = 200 return resp -@api.route('/subject_types', methods=['GET']) +@api.route("/subject_types", methods=["GET"]) @jsonp def get_subject_types(): - """ Returns all currently loaded subject_types (sorted by "id").""" - subject_types = [type_.to_json() for type_ in current_app.config['subject_types']] - subject_types.sort(key=lambda x: x['id']) - resp = jsonify({'subject_types': subject_types}) + """Returns all currently loaded subject_types (sorted by "id").""" + subject_types = [type_.to_json() for type_ in current_app.config["subject_types"]] + subject_types.sort(key=lambda x: x["id"]) + resp = jsonify({"subject_types": subject_types}) resp = insert_headers(resp) resp.status_code = 200 return resp -@api.route('/decision', methods=['OPTIONS']) +@api.route("/decision", methods=["OPTIONS"]) @jsonp def make_decision_options(): - """ Handles the OPTIONS requests to the /decision endpoint. """ + """Handles the OPTIONS requests to the /decision endpoint.""" resp = current_app.make_default_options_response() return insert_headers(resp) -@api.route('/decision', methods=['POST']) +@api.route("/decision", methods=["POST"]) @decision_exception_counter.count_exceptions() @decision_request_duration_seconds.time() @jsonp @@ -422,14 +425,14 @@ def make_decision(): """ # noqa: E501 data = request.get_json() response = greenwave.decision.make_decision(data, current_app.config) - log.debug('Response: %s', response) + log.debug("Response: %s", response) resp = jsonify(response) resp = insert_headers(resp) resp.status_code = 200 return resp -@api.route('/validate-gating-yaml', methods=['POST']) +@api.route("/validate-gating-yaml", methods=["POST"]) @jsonp def validate_gating_yaml_post(): """ @@ -468,22 +471,27 @@ def validate_gating_yaml_post(): "message": "All OK" } """ - content = request.get_data().decode('utf-8') + content = request.get_data().decode("utf-8") try: policies = RemotePolicy.safe_load_all(content) except SafeYAMLError as e: raise BadRequest(str(e)) if not policies: - raise BadRequest('No policies defined') + raise BadRequest("No policies defined") missing_decision_contexts = _missing_decision_contexts_in_parent_policies(policies) if missing_decision_contexts: - msg = {'message': ('Greenwave could not find a parent policy(ies) for following decision' - ' context(s): {}. Please change your policy so that it will match a ' - 'decision context in the parent policies.'.format( - ', '.join(sorted(missing_decision_contexts))))} + msg = { + "message": ( + "Greenwave could not find a parent policy(ies) for following decision" + " context(s): {}. Please change your policy so that it will match a " + "decision context in the parent policies.".format( + ", ".join(sorted(missing_decision_contexts)) + ) + ) + } else: - msg = {'message': 'All OK'} + msg = {"message": "All OK"} return jsonify(msg) diff --git a/greenwave/app_factory.py b/greenwave/app_factory.py index 502d34e7..4e358edf 100644 --- a/greenwave/app_factory.py +++ b/greenwave/app_factory.py @@ -3,16 +3,16 @@ import logging import logging.config +import requests +from dogpile.cache import make_region from flask import Flask +from werkzeug.exceptions import default_exceptions + from greenwave.api_v1 import api, landing_page -from greenwave.utils import json_error, load_config, mangle_key from greenwave.policies import load_policies from greenwave.subjects.subject_type import load_subject_types from greenwave.tracing import init_tracing - -from dogpile.cache import make_region -import requests -from werkzeug.exceptions import default_exceptions +from greenwave.utils import json_error, load_config, mangle_key log = logging.getLogger(__name__) @@ -22,27 +22,30 @@ def create_app(config_obj=None): app = Flask(__name__) app.config.update(load_config(config_obj)) - if app.config['PRODUCTION'] and app.secret_key == 'replace-me-with-something-random': # nosec + if ( + app.config["PRODUCTION"] + and app.secret_key == "replace-me-with-something-random" # nosec + ): # nosec raise Warning("You need to change the app.secret_key value for production") - logging_config = app.config.get('LOGGING') + logging_config = app.config.get("LOGGING") if logging_config: logging.config.dictConfig(logging_config) init_tracing(app) - policies_dir = app.config['POLICIES_DIR'] + policies_dir = app.config["POLICIES_DIR"] log.debug("config: Loading policies from %r", policies_dir) - app.config['policies'] = load_policies(policies_dir) + app.config["policies"] = load_policies(policies_dir) - subject_types_dir = app.config['SUBJECT_TYPES_DIR'] + subject_types_dir = app.config["SUBJECT_TYPES_DIR"] log.debug("config: Loading subject types from %r", subject_types_dir) - app.config['subject_types'] = load_subject_types(subject_types_dir) + app.config["subject_types"] = load_subject_types(subject_types_dir) - if app.config.get('DIST_GIT_URL_TEMPLATE') and app.config.get('DIST_GIT_BASE_URL'): - app.config['DIST_GIT_URL_TEMPLATE'] = app.config['DIST_GIT_URL_TEMPLATE'].replace( - '{DIST_GIT_BASE_URL}', app.config['DIST_GIT_BASE_URL'] - ) + if app.config.get("DIST_GIT_URL_TEMPLATE") and app.config.get("DIST_GIT_BASE_URL"): + app.config["DIST_GIT_URL_TEMPLATE"] = app.config[ + "DIST_GIT_URL_TEMPLATE" + ].replace("{DIST_GIT_BASE_URL}", app.config["DIST_GIT_BASE_URL"]) # register error handlers for code in default_exceptions.keys(): @@ -53,12 +56,12 @@ def create_app(config_obj=None): # register blueprints app.register_blueprint(api, url_prefix="/api/v1.0") - app.add_url_rule('/', view_func=landing_page) - app.add_url_rule('/healthcheck', view_func=healthcheck) + app.add_url_rule("/", view_func=landing_page) + app.add_url_rule("/healthcheck", view_func=healthcheck) # Initialize the cache. app.cache = make_region(key_mangler=mangle_key) - app.cache.configure(**app.config['CACHE']) + app.cache.configure(**app.config["CACHE"]) return app @@ -71,4 +74,4 @@ def healthcheck(): Returns a 200 response if the application is alive and able to serve requests. """ - return 'Health check OK', 200, [('Content-Type', 'text/plain')] + return "Health check OK", 200, [("Content-Type", "text/plain")] diff --git a/greenwave/cache.py b/greenwave/cache.py index 37d04eee..4db185ae 100644 --- a/greenwave/cache.py +++ b/greenwave/cache.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0+ import functools + import dogpile.cache import flask @@ -9,10 +10,12 @@ def cached(fn): - """ Cache arguments with a region hung on the flask app. """ + """Cache arguments with a region hung on the flask app.""" + @functools.wraps(fn) def wrapper(*args): decoration = flask.current_app.cache.cache_on_arguments decorator = decoration(function_key_generator=key_generator) return decorator(fn)(*args) + return wrapper diff --git a/greenwave/config.py b/greenwave/config.py index 36eefdfe..0e943f04 100644 --- a/greenwave/config.py +++ b/greenwave/config.py @@ -4,59 +4,59 @@ def _local_conf_dir(subdir): basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - return os.path.join(basedir, 'conf', subdir) + return os.path.join(basedir, "conf", subdir) -class Config(object): +class Config: """ A GreenWave Flask configuration. """ + DEBUG = True # We configure logging explicitly, turn off the Flask-supplied log handler. - LOGGER_HANDLER_POLICY = 'never' - HOST = '127.0.0.1' + LOGGER_HANDLER_POLICY = "never" + HOST = "127.0.0.1" PORT = 5005 PRODUCTION = False - SECRET_KEY = 'replace-me-with-something-random' # nosec + SECRET_KEY = "replace-me-with-something-random" # nosec - RESULTSDB_API_URL = 'https://taskotron.fedoraproject.org/resultsdb_api/api/v2.0' - WAIVERDB_API_URL = 'https://waiverdb.fedoraproject.org/api/v1.0' + RESULTSDB_API_URL = "https://taskotron.fedoraproject.org/resultsdb_api/api/v2.0" + WAIVERDB_API_URL = "https://waiverdb.fedoraproject.org/api/v1.0" # Remote rule configuration # NOTE: DIST_GIT_URL_TEMPLATE is obsolete and used here only for # backward compatibility. They maybe removed in future versions. Use REMOTE_RULE_POLICIES['*'] # instead - DIST_GIT_URL_TEMPLATE = \ - 'https://src.fedoraproject.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml' + DIST_GIT_URL_TEMPLATE = "https://src.fedoraproject.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml" REMOTE_RULE_POLICIES = { - 'brew-build-group': ( - 'https://git.example.com/devops/greenwave-policies/side-tags/raw/master/' - '{subject_id}.yaml' + "brew-build-group": ( + "https://git.example.com/devops/greenwave-policies/side-tags/raw/master/" + "{subject_id}.yaml" ), - '*': 'https://src.fedoraproject.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml' + "*": "https://src.fedoraproject.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml", } REMOTE_RULE_GIT_TIMEOUT = 30 - KOJI_BASE_URL = 'https://koji.fedoraproject.org/kojihub' + KOJI_BASE_URL = "https://koji.fedoraproject.org/kojihub" # Options for outbound HTTP requests made by python-requests REQUESTS_TIMEOUT = (6.1, 15) REQUESTS_VERIFY = True - POLICIES_DIR = '/etc/greenwave/policies' - SUBJECT_TYPES_DIR = '/etc/greenwave/subject_types' + POLICIES_DIR = "/etc/greenwave/policies" + SUBJECT_TYPES_DIR = "/etc/greenwave/subject_types" # By default, don't cache anything. - CACHE = {'backend': 'dogpile.cache.null'} + CACHE = {"backend": "dogpile.cache.null"} # Greenwave API url - GREENWAVE_API_URL = 'https://greenwave.domain.local/api/v1.0' + GREENWAVE_API_URL = "https://greenwave.domain.local/api/v1.0" - OUTCOMES_PASSED = ('PASSED', 'INFO') - OUTCOMES_ERROR = ('ERROR',) - OUTCOMES_INCOMPLETE = ('QUEUED', 'RUNNING') + OUTCOMES_PASSED = ("PASSED", "INFO") + OUTCOMES_ERROR = ("ERROR",) + OUTCOMES_INCOMPLETE = ("QUEUED", "RUNNING") DISTINCT_LATEST_RESULTS_ON = ( - 'scenario', - 'system_architecture', - 'system_variant', + "scenario", + "system_architecture", + "system_variant", ) LISTENER_HOSTS = "" @@ -105,34 +105,34 @@ class ProductionConfig(Config): class DevelopmentConfig(Config): - #RESULTSDB_API_URL = 'https://taskotron.stg.fedoraproject.org/resultsdb_api/api/v2.0' - RESULTSDB_API_URL = 'http://localhost:5001/api/v2.0' - #WAIVERDB_API_URL = 'http://waiverdb-dev.fedorainfracloud.org/api/v1.0' - WAIVERDB_API_URL = 'http://localhost:5004/api/v1.0' - GREENWAVE_API_URL = 'http://localhost:5005/api/v1.0' - POLICIES_DIR = _local_conf_dir('policies') - SUBJECT_TYPES_DIR = _local_conf_dir('subject_types') + # RESULTSDB_API_URL = 'https://taskotron.stg.fedoraproject.org/resultsdb_api/api/v2.0' + RESULTSDB_API_URL = "http://localhost:5001/api/v2.0" + # WAIVERDB_API_URL = 'http://waiverdb-dev.fedorainfracloud.org/api/v1.0' + WAIVERDB_API_URL = "http://localhost:5004/api/v1.0" + GREENWAVE_API_URL = "http://localhost:5005/api/v1.0" + POLICIES_DIR = _local_conf_dir("policies") + SUBJECT_TYPES_DIR = _local_conf_dir("subject_types") REMOTE_RULE_POLICIES = { - 'brew-build-group': ( - 'https://git.example.com/devops/greenwave-policies/side-tags/raw/master/{pkg_namespace}' - '{pkg_name}.yaml' + "brew-build-group": ( + "https://git.example.com/devops/greenwave-policies/side-tags/raw/master/{pkg_namespace}" + "{pkg_name}.yaml" ), - '*': 'https://src.fedoraproject.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml' + "*": "https://src.fedoraproject.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml", } class TestingConfig(Config): - RESULTSDB_API_URL = 'http://localhost:5001/api/v2.0' - WAIVERDB_API_URL = 'http://localhost:5004/api/v1.0' - GREENWAVE_API_URL = 'http://localhost:5005/api/v1.0' - KOJI_BASE_URL = 'http://localhost:5006/kojihub' - POLICIES_DIR = _local_conf_dir('policies') - SUBJECT_TYPES_DIR = _local_conf_dir('subject_types') + RESULTSDB_API_URL = "http://localhost:5001/api/v2.0" + WAIVERDB_API_URL = "http://localhost:5004/api/v1.0" + GREENWAVE_API_URL = "http://localhost:5005/api/v1.0" + KOJI_BASE_URL = "http://localhost:5006/kojihub" + POLICIES_DIR = _local_conf_dir("policies") + SUBJECT_TYPES_DIR = _local_conf_dir("subject_types") class FedoraTestingConfig(Config): - RESULTSDB_API_URL = 'http://localhost:5001/api/v2.0' - WAIVERDB_API_URL = 'http://localhost:5004/api/v1.0' - GREENWAVE_API_URL = 'http://localhost:5005/api/v1.0' - KOJI_BASE_URL = 'http://localhost:5006/kojihub' - POLICIES_DIR = _local_conf_dir('policies') + RESULTSDB_API_URL = "http://localhost:5001/api/v2.0" + WAIVERDB_API_URL = "http://localhost:5004/api/v1.0" + GREENWAVE_API_URL = "http://localhost:5005/api/v1.0" + KOJI_BASE_URL = "http://localhost:5006/kojihub" + POLICIES_DIR = _local_conf_dir("policies") diff --git a/greenwave/consumers/__init__.py b/greenwave/consumers/__init__.py index fc53b5d9..32eae0f8 100644 --- a/greenwave/consumers/__init__.py +++ b/greenwave/consumers/__init__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """Contains Greenwave's fedora-messaging consumers.""" diff --git a/greenwave/consumers/consumer.py b/greenwave/consumers/consumer.py index 842f08a9..c4e32cd3 100644 --- a/greenwave/consumers/consumer.py +++ b/greenwave/consumers/consumer.py @@ -1,33 +1,33 @@ # SPDX-License-Identifier: GPL-2.0+ import logging -import requests import fedora_messaging.api import fedora_messaging.exceptions +import requests +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, +) import greenwave.app_factory import greenwave.decision - from greenwave.monitor import ( decision_changed_counter, decision_failed_counter, decision_unchanged_counter, - messaging_tx_sent_ok_counter, messaging_tx_failed_counter, + messaging_tx_sent_ok_counter, +) +from greenwave.policies import ( + applicable_decision_context_product_version_pairs, ) -from greenwave.policies import applicable_decision_context_product_version_pairs from greenwave.utils import right_before_this_time - -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator - log = logging.getLogger(__name__) def _equals_except_keys(lhs, rhs, except_keys): keys = lhs.keys() - except_keys - return lhs.keys() == rhs.keys() \ - and all(lhs[key] == rhs[key] for key in keys) + return lhs.keys() == rhs.keys() and all(lhs[key] == rhs[key] for key in keys) def _is_decision_unchanged(old_decision, decision): @@ -38,11 +38,11 @@ def _is_decision_unchanged(old_decision, decision): if old_decision is None or decision is None: return old_decision == decision - requirements_keys = ('satisfied_requirements', 'unsatisfied_requirements') + requirements_keys = ("satisfied_requirements", "unsatisfied_requirements") if not _equals_except_keys(old_decision, decision, requirements_keys): return False - ignore_keys = ('result_id',) + ignore_keys = ("result_id",) for key in requirements_keys: old_requirements = old_decision[key] requirements = decision[key] @@ -60,10 +60,11 @@ class Consumer: """ Base class for consumers. """ - config_key = 'greenwave_handler' - hub_config_prefix = 'greenwave_consumer_' - default_topic = 'item.new' - monitor_labels = {'handler': 'greenwave_consumer'} + + config_key = "greenwave_handler" + hub_config_prefix = "greenwave_consumer_" + default_topic = "item.new" + monitor_labels = {"handler": "greenwave_consumer"} context = None def __init__(self, hub, *args, **kwargs): @@ -75,16 +76,18 @@ def __init__(self, hub, *args, **kwargs): messages. It is used to look up the hub config. """ - prefix = hub.config.get('topic_prefix') - env = hub.config.get('environment') - suffix = hub.config.get(f'{self.hub_config_prefix}topic_suffix', self.default_topic) - self.topic = ['.'.join([prefix, env, suffix])] + prefix = hub.config.get("topic_prefix") + env = hub.config.get("environment") + suffix = hub.config.get( + f"{self.hub_config_prefix}topic_suffix", self.default_topic + ) + self.topic = [".".join([prefix, env, suffix])] - config = kwargs.pop('config', None) + config = kwargs.pop("config", None) self.flask_app = greenwave.app_factory.create_app(config) - self.greenwave_api_url = self.flask_app.config['GREENWAVE_API_URL'] - log.info('Greenwave handler listening on: %s', self.topic) + self.greenwave_api_url = self.flask_app.config["GREENWAVE_API_URL"] + log.info("Greenwave handler listening on: %s", self.topic) def consume(self, message): """ @@ -94,7 +97,7 @@ def consume(self, message): message (fedora_messaging.message.Message): A fedora message about a new item. """ try: - message = message.get('body', message) + message = message.get("body", message) log.debug('Processing message "%s"', message) self.context = TraceContextTextMapPropagator().extract(message) @@ -105,7 +108,7 @@ def consume(self, message): # and the message is scheduled to be received later. But it seems # these messages can be only received by other consumer (or after # restart) otherwise the messages can block the queue completely. - log.exception('Unexpected exception') + log.exception("Unexpected exception") def _inc(self, messaging_counter): """Helper method to increase monitoring counter.""" @@ -115,58 +118,57 @@ def _publish_decision_update_fedora_messaging(self, decision): try: TraceContextTextMapPropagator().inject(decision, self.context) msg = fedora_messaging.api.Message( - topic='greenwave.decision.update', - body=decision + topic="greenwave.decision.update", body=decision ) fedora_messaging.api.publish(msg) self._inc(messaging_tx_sent_ok_counter) except fedora_messaging.exceptions.PublishReturned as e: - log.error( - 'Fedora Messaging broker rejected message %s: %s', - msg.id, e) + log.error("Fedora Messaging broker rejected message %s: %s", msg.id, e) except fedora_messaging.exceptions.ConnectionException as e: - log.error('Error sending message %s: %s', msg.id, e) + log.error("Error sending message %s: %s", msg.id, e) self._inc(messaging_tx_failed_counter) except Exception: # pylint: disable=broad-except - log.exception('Error sending fedora-messaging message') + log.exception("Error sending fedora-messaging message") self._inc(messaging_tx_failed_counter) def _old_and_new_decisions(self, submit_time, **request_data): """Returns decision before and after submit time.""" - greenwave_url = self.greenwave_api_url + '/decision' - log.debug('querying greenwave at: %s', greenwave_url) + greenwave_url = self.greenwave_api_url + "/decision" + log.debug("querying greenwave at: %s", greenwave_url) try: - decision = greenwave.decision.make_decision(request_data, self.flask_app.config) + decision = greenwave.decision.make_decision( + request_data, self.flask_app.config + ) - request_data['when'] = right_before_this_time(submit_time) - old_decision = greenwave.decision.make_decision(request_data, self.flask_app.config) - log.debug('old decision: %s', old_decision) + request_data["when"] = right_before_this_time(submit_time) + old_decision = greenwave.decision.make_decision( + request_data, self.flask_app.config + ) + log.debug("old decision: %s", old_decision) except requests.exceptions.HTTPError as e: - log.exception('Failed to retrieve decision for data=%s, error: %s', request_data, e) + log.exception( + "Failed to retrieve decision for data=%s, error: %s", request_data, e + ) return None, None return old_decision, decision def _publish_decision_change( - self, - submit_time, - subject, - testcase, - product_version, - publish_testcase): - + self, submit_time, subject, testcase, product_version, publish_testcase + ): policy_attributes = dict( subject=subject, testcase=testcase, ) if product_version: - policy_attributes['product_version'] = product_version + policy_attributes["product_version"] = product_version - policies = self.flask_app.config['policies'] + policies = self.flask_app.config["policies"] contexts_product_versions = applicable_decision_context_product_version_pairs( - policies, **policy_attributes) + policies, **policy_attributes + ) for decision_context, product_version in sorted(contexts_product_versions): old_decision, decision = self._old_and_new_decisions( @@ -177,27 +179,35 @@ def _publish_decision_change( subject_identifier=subject.identifier, ) if decision is None: - self._inc(decision_failed_counter.labels(decision_context=decision_context)) + self._inc( + decision_failed_counter.labels(decision_context=decision_context) + ) continue if _is_decision_unchanged(old_decision, decision): - log.debug('Decision unchanged: %s', decision) - self._inc(decision_unchanged_counter.labels(decision_context=decision_context)) + log.debug("Decision unchanged: %s", decision) + self._inc( + decision_unchanged_counter.labels(decision_context=decision_context) + ) continue - self._inc(decision_changed_counter.labels(decision_context=decision_context)) - - decision.update({ - 'subject_type': subject.type, - 'subject_identifier': subject.identifier, - # subject is for backwards compatibility only: - 'subject': [subject.to_dict()], - 'decision_context': decision_context, - 'product_version': product_version, - 'previous': old_decision, - }) + self._inc( + decision_changed_counter.labels(decision_context=decision_context) + ) + + decision.update( + { + "subject_type": subject.type, + "subject_identifier": subject.identifier, + # subject is for backwards compatibility only: + "subject": [subject.to_dict()], + "decision_context": decision_context, + "product_version": product_version, + "previous": old_decision, + } + ) if publish_testcase: - decision['testcase'] = testcase + decision["testcase"] = testcase - log.info('Publishing a decision update message: %r', decision) + log.info("Publishing a decision update message: %r", decision) self._publish_decision_update_fedora_messaging(decision) diff --git a/greenwave/consumers/fedora_messaging_consumer.py b/greenwave/consumers/fedora_messaging_consumer.py index 28cb7417..e10a24d0 100644 --- a/greenwave/consumers/fedora_messaging_consumer.py +++ b/greenwave/consumers/fedora_messaging_consumer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """ The fedora-messaging consumer. @@ -11,20 +10,22 @@ import logging +from fedora_messaging.config import conf + from greenwave.consumers.resultsdb import ResultsDBHandler from greenwave.consumers.waiverdb import WaiverDBHandler from greenwave.monitor import ( - messaging_rx_counter, messaging_rx_ignored_counter, - messaging_rx_processed_ok_counter, messaging_rx_failed_counter) - -from fedora_messaging.config import conf - + messaging_rx_counter, + messaging_rx_failed_counter, + messaging_rx_ignored_counter, + messaging_rx_processed_ok_counter, +) log = logging.getLogger(__name__) -class Dummy(object): - """ Dummy object only storing a dictionary named "config" that can be passed +class Dummy: + """Dummy object only storing a dictionary named "config" that can be passed onto the base consumer. """ @@ -42,8 +43,7 @@ def fedora_messaging_callback(message): message (fedora_messaging.message.Message): The message we received from the queue. """ - log.info( - 'Received message from fedora-messaging with topic: %s', message.topic) + log.info("Received message from fedora-messaging with topic: %s", message.topic) consumer_config = conf["consumer_config"] if message.topic.endswith("resultsdb.result.new"): # New resultsdb results @@ -51,40 +51,42 @@ def fedora_messaging_callback(message): config = { "topic_prefix": consumer_config["topic_prefix"], "environment": consumer_config["environment"], - "resultsdb_topic_suffix": consumer_config["resultsdb_topic_suffix"] + "resultsdb_topic_suffix": consumer_config["resultsdb_topic_suffix"], } hub = Dummy(config) handler = ResultsDBHandler(hub) - msg = {"body": {'msg': message.body}} - log.info('Sending message received to: ResultsDBHandler') + msg = {"body": {"msg": message.body}} + log.info("Sending message received to: ResultsDBHandler") try: handler.consume(msg) messaging_rx_processed_ok_counter.labels(handler="resultsdb").inc() except Exception: messaging_rx_failed_counter.labels(handler="resultsdb").inc() - log.exception("Could not correctly consume the message with " - "ResultsDBHandler") + log.exception( + "Could not correctly consume the message with ResultsDBHandler" + ) raise - elif message.topic.endswith('waiver.new'): + elif message.topic.endswith("waiver.new"): # New waiver submitted messaging_rx_counter.labels(handler="waiverdb").inc() config = { "topic_prefix": consumer_config["topic_prefix"], "environment": consumer_config["environment"], - "waiverdb_topic_suffix": consumer_config["waiverdb_topic_suffix"] + "waiverdb_topic_suffix": consumer_config["waiverdb_topic_suffix"], } hub = Dummy(config) handler = WaiverDBHandler(hub) - msg = {"body": {'msg': message.body}} - log.info('Sending message received to: WaiverDBHandler') + msg = {"body": {"msg": message.body}} + log.info("Sending message received to: WaiverDBHandler") try: handler.consume(msg) messaging_rx_processed_ok_counter.labels(handler="waiverdb").inc() except Exception: messaging_rx_failed_counter.labels(handler="waiverdb").inc() - log.exception("Could not correctly consume the message with " - "WaiverDBHandler") + log.exception( + "Could not correctly consume the message with " "WaiverDBHandler" + ) raise else: messaging_rx_ignored_counter.inc() diff --git a/greenwave/consumers/resultsdb.py b/greenwave/consumers/resultsdb.py index 022557ac..f9a9f826 100644 --- a/greenwave/consumers/resultsdb.py +++ b/greenwave/consumers/resultsdb.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """ The "resultsdb handler". @@ -14,8 +13,8 @@ from greenwave.consumers.consumer import Consumer from greenwave.product_versions import subject_product_versions from greenwave.subjects.factory import ( - create_subject_from_data, UnknownSubjectDataError, + create_subject_from_data, ) log = logging.getLogger(__name__) @@ -32,11 +31,11 @@ def _unpack_value(value): def _get_brew_task_id(msg): - data = msg.get('data') + data = msg.get("data") if not data: return None - task_id = _unpack_value(data.get('brew_task_id')) + task_id = _unpack_value(data.get("brew_task_id")) try: return int(task_id) except (ValueError, TypeError): @@ -52,14 +51,14 @@ class ResultsDBHandler(Consumer): this consumer listens to. """ - config_key = 'resultsdb_handler' - hub_config_prefix = 'resultsdb_' - default_topic = 'taskotron.result.new' - monitor_labels = {'handler': 'resultsdb_consumer'} + config_key = "resultsdb_handler" + hub_config_prefix = "resultsdb_" + default_topic = "taskotron.result.new" + monitor_labels = {"handler": "resultsdb_consumer"} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.koji_base_url = self.flask_app.config['KOJI_BASE_URL'] + self.koji_base_url = self.flask_app.config["KOJI_BASE_URL"] @staticmethod def announcement_subject(message): @@ -72,14 +71,11 @@ def announcement_subject(message): """ try: - data = message['msg']['data'] # New format + data = message["msg"]["data"] # New format except KeyError: - data = message['msg']['task'] # Old format + data = message["msg"]["task"] # Old format - unpacked = { - k: _unpack_value(v) - for k, v in data.items() - } + unpacked = {k: _unpack_value(v) for k, v in data.items()} try: subject = create_subject_from_data(unpacked) @@ -95,27 +91,27 @@ def announcement_subject(message): # https://pagure.io/taskotron/resultsdb/issue/92 # https://pagure.io/taskotron/resultsdb/pull-request/101 # https://pagure.io/greenwave/pull-request/262#comment-70350 - if subject.type == 'compose' and 'productmd.compose.id' not in data: + if subject.type == "compose" and "productmd.compose.id" not in data: return None return subject def _consume_message(self, message): - msg = message['msg'] + msg = message["msg"] try: - testcase = msg['testcase']['name'] + testcase = msg["testcase"]["name"] except KeyError: - testcase = msg['task']['name'] + testcase = msg["task"]["name"] try: - submit_time = msg['submit_time'] + submit_time = msg["submit_time"] except KeyError: - submit_time = msg['result']['submit_time'] + submit_time = msg["result"]["submit_time"] - outcome = msg.get('outcome') - if outcome in self.flask_app.config['OUTCOMES_INCOMPLETE']: - log.debug('Assuming no decision change on outcome %r', outcome) + outcome = msg.get("outcome") + if outcome in self.flask_app.config["OUTCOMES_INCOMPLETE"]: + log.debug("Assuming no decision change on outcome %r", outcome) return brew_task_id = _get_brew_task_id(msg) @@ -124,7 +120,7 @@ def _consume_message(self, message): if subject is None: return - log.debug('Considering subject: %r', subject) + log.debug("Considering subject: %r", subject) product_versions = subject_product_versions( subject, @@ -132,7 +128,7 @@ def _consume_message(self, message): brew_task_id, ) - log.debug('Guessed product versions: %r', product_versions) + log.debug("Guessed product versions: %r", product_versions) if not product_versions: product_versions = [None] diff --git a/greenwave/consumers/waiverdb.py b/greenwave/consumers/waiverdb.py index dcd169ca..2b5b4765 100644 --- a/greenwave/consumers/waiverdb.py +++ b/greenwave/consumers/waiverdb.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """ The "waiverdb handler". @@ -22,18 +21,18 @@ class WaiverDBHandler(Consumer): this consumer listens to. """ - config_key = 'waiverdb_handler' - hub_config_prefix = 'waiverdb_' - default_topic = 'waiver.new' - monitor_labels = {'handler': 'waiverdb_consumer'} + config_key = "waiverdb_handler" + hub_config_prefix = "waiverdb_" + default_topic = "waiver.new" + monitor_labels = {"handler": "waiverdb_consumer"} def _consume_message(self, message): - msg = message['msg'] + msg = message["msg"] - product_version = msg['product_version'] - testcase = msg['testcase'] - subject = create_subject(msg['subject_type'], msg['subject_identifier']) - submit_time = msg['timestamp'] + product_version = msg["product_version"] + testcase = msg["testcase"] + subject = create_subject(msg["subject_type"], msg["subject_identifier"]) + submit_time = msg["timestamp"] self._publish_decision_change( submit_time=submit_time, diff --git a/greenwave/decision.py b/greenwave/decision.py index c9e1ffe9..669afb9c 100644 --- a/greenwave/decision.py +++ b/greenwave/decision.py @@ -1,23 +1,16 @@ # SPDX-License-Identifier: GPL-2.0+ -import logging import datetime +import logging from opentelemetry import trace -from werkzeug.exceptions import ( - BadRequest, - NotFound, - UnsupportedMediaType, -) +from werkzeug.exceptions import BadRequest, NotFound, UnsupportedMediaType -from greenwave.policies import ( - summarize_answers, - OnDemandPolicy, -) +from greenwave.policies import OnDemandPolicy, summarize_answers from greenwave.resources import ResultsRetriever, WaiversRetriever from greenwave.subjects.factory import ( + UnknownSubjectDataError, create_subject, create_subject_from_data, - UnknownSubjectDataError, ) from greenwave.waivers import waive_answers @@ -30,6 +23,7 @@ class RuleContext: Environment for verifying rules from multiple policies for a single decision subject. """ + def __init__(self, decision_context, product_version, subject, results_retriever): self.decision_context = decision_context self.product_version = product_version @@ -53,6 +47,7 @@ class Decision: """ Collects answers from rules from policies. """ + def __init__(self, decision_context, product_version, verbose=False): # this can be a single string or a list of strings self.decision_context = decision_context @@ -67,12 +62,14 @@ def __init__(self, decision_context, product_version, verbose=False): def check(self, subject, policies, results_retriever): subject_policies = [ - policy for policy in policies + policy + for policy in policies if policy.matches( decision_context=self.decision_context, product_version=self.product_version, match_any_remote_rule=True, - subject=subject) + subject=subject, + ) ] if not subject_policies: @@ -81,20 +78,24 @@ def check(self, subject, policies, results_retriever): dc = self.decision_context if isinstance(dc, list): - dc = ' '.join(dc) + dc = " ".join(dc) raise NotFound( - 'Found no applicable policies for %s subjects at gating point(s) %s in %s' % ( - subject.type, dc, self.product_version)) + "Found no applicable policies for {} subjects at gating point(s) {} in {}".format( + subject.type, dc, self.product_version + ) + ) if self.verbose: # Retrieve test results and waivers for all items when verbose output is requested. self.verbose_results.extend(results_retriever.retrieve(subject)) - self.waiver_filters.append(dict( - subject_type=subject.type, - subject_identifier=subject.identifier, - product_version=self.product_version, - )) + self.waiver_filters.append( + dict( + subject_type=subject.type, + subject_identifier=subject.identifier, + product_version=self.product_version, + ) + ) rule_context = RuleContext( decision_context=self.decision_context, @@ -144,7 +145,7 @@ def _decision_subject(data): try: subject = create_subject_from_data(data) except UnknownSubjectDataError: - raise BadRequest('Could not detect subject_identifier.') + raise BadRequest("Could not detect subject_identifier.") return subject @@ -159,78 +160,81 @@ def _decision_subjects_for_request(data): with WaiverDB < 0.11, but it accepts a single subject dict. Here we accept a list. """ - if 'subject' in data: - subjects = data['subject'] - if (not isinstance(subjects, list) or not subjects or - not all(isinstance(entry, dict) for entry in subjects)): - raise BadRequest('Invalid subject, must be a list of dicts') + if "subject" in data: + subjects = data["subject"] + if ( + not isinstance(subjects, list) + or not subjects + or not all(isinstance(entry, dict) for entry in subjects) + ): + raise BadRequest("Invalid subject, must be a list of dicts") for subject in subjects: yield _decision_subject(subject) else: - if 'subject_type' not in data: + if "subject_type" not in data: raise BadRequest('Missing required "subject_type" parameter') - if 'subject_identifier' not in data: + if "subject_identifier" not in data: raise BadRequest('Missing required "subject_identifier" parameter') - yield create_subject(data['subject_type'], data['subject_identifier']) + yield create_subject(data["subject_type"], data["subject_identifier"]) @tracer.start_as_current_span("make_decision") def make_decision(data, config): if not data: - raise UnsupportedMediaType('No JSON payload in request') + raise UnsupportedMediaType("No JSON payload in request") - if not data.get('product_version'): - raise BadRequest('Missing required product version') + if not data.get("product_version"): + raise BadRequest("Missing required product version") - if not data.get('decision_context') and not data.get('rules'): - raise BadRequest('Either decision_context or rules is required.') + if not data.get("decision_context") and not data.get("rules"): + raise BadRequest("Either decision_context or rules is required.") - log.debug('New decision request for data: %s', data) - product_version = data['product_version'] + log.debug("New decision request for data: %s", data) + product_version = data["product_version"] - decision_contexts = data.get('decision_context', []) + decision_contexts = data.get("decision_context", []) if not isinstance(decision_contexts, list): # this will be a single context as a string decision_contexts = [decision_contexts] - rules = data.get('rules', []) + rules = data.get("rules", []) if decision_contexts and rules: - raise BadRequest('Cannot have both decision_context and rules') + raise BadRequest("Cannot have both decision_context and rules") on_demand_policies = [] if rules: - request_data = {key: data[key] for key in data if key not in ('subject', 'subject_type')} + request_data = { + key: data[key] for key in data if key not in ("subject", "subject_type") + } for subject in _decision_subjects_for_request(data): - request_data['subject_type'] = subject.type - request_data['subject_identifier'] = subject.identifier + request_data["subject_type"] = subject.type + request_data["subject_identifier"] = subject.identifier on_demand_policy = OnDemandPolicy.create_from_json(request_data) on_demand_policies.append(on_demand_policy) - verbose = data.get('verbose', False) + verbose = data.get("verbose", False) if not isinstance(verbose, bool): - raise BadRequest('Invalid verbose flag, must be a bool') - ignore_results = data.get('ignore_result', []) - ignore_waivers = data.get('ignore_waiver', []) - when = data.get('when') + raise BadRequest("Invalid verbose flag, must be a bool") + ignore_results = data.get("ignore_result", []) + ignore_waivers = data.get("ignore_waiver", []) + when = data.get("when") if when: try: - datetime.datetime.strptime(when, '%Y-%m-%dT%H:%M:%S.%f') + datetime.datetime.strptime(when, "%Y-%m-%dT%H:%M:%S.%f") except ValueError: raise BadRequest('Invalid "when" parameter, must be in ISO8601 format') - retriever_args = {'when': when} + retriever_args = {"when": when} results_retriever = ResultsRetriever( - ignore_ids=ignore_results, - url=config['RESULTSDB_API_URL'], - **retriever_args) + ignore_ids=ignore_results, url=config["RESULTSDB_API_URL"], **retriever_args + ) waivers_retriever = WaiversRetriever( - ignore_ids=ignore_waivers, - url=config['WAIVERDB_API_URL'], - **retriever_args) + ignore_ids=ignore_waivers, url=config["WAIVERDB_API_URL"], **retriever_args + ) - policies = on_demand_policies or config['policies'] + policies = on_demand_policies or config["policies"] decision = Decision(decision_contexts, product_version, verbose) for subject in _decision_subjects_for_request(data): decision.check(subject, policies, results_retriever) @@ -238,21 +242,34 @@ def make_decision(data, config): decision.waive_answers(waivers_retriever) response = { - 'policies_satisfied': decision.policies_satisfied(), - 'summary': decision.summary(), - 'satisfied_requirements': decision.satisfied_requirements(), - 'unsatisfied_requirements': decision.unsatisfied_requirements(), + "policies_satisfied": decision.policies_satisfied(), + "summary": decision.summary(), + "satisfied_requirements": decision.satisfied_requirements(), + "unsatisfied_requirements": decision.unsatisfied_requirements(), } # Include applicable_policies if on-demand policy was not specified. if not rules: - response.update({'applicable_policies': [ - policy.id for policy in decision.applicable_policies]}) + response.update( + { + "applicable_policies": [ + policy.id for policy in decision.applicable_policies + ] + } + ) if verbose: - response.update({ - 'results': list({result['id']: result for result in decision.verbose_results}.values()), - 'waivers': list({waiver['id']: waiver for waiver in decision.waivers}.values()), - }) + response.update( + { + "results": list( + { + result["id"]: result for result in decision.verbose_results + }.values() + ), + "waivers": list( + {waiver["id"]: waiver for waiver in decision.waivers}.values() + ), + } + ) return response diff --git a/greenwave/listeners/base.py b/greenwave/listeners/base.py index 2fd0b7d2..bfe8fcb4 100644 --- a/greenwave/listeners/base.py +++ b/greenwave/listeners/base.py @@ -8,6 +8,9 @@ import stomp from opentelemetry.context import Context +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, +) from requests.exceptions import HTTPError import greenwave.app_factory @@ -23,12 +26,11 @@ messaging_tx_failed_counter, messaging_tx_sent_ok_counter, ) -from greenwave.policies import applicable_decision_context_product_version_pairs +from greenwave.policies import ( + applicable_decision_context_product_version_pairs, +) from greenwave.utils import right_before_this_time -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator - - GREENWAVE_LISTENER_PREFIX = "greenwave" @@ -99,7 +101,7 @@ def on_message(self, frame): _send_nack(self, frame.headers) return - self.app.logger.debug("Received a message ID: %s", frame.headers['message-id']) + self.app.logger.debug("Received a message ID: %s", frame.headers["message-id"]) _send_ack(self, frame.headers) self._inc(messaging_rx_counter) @@ -258,7 +260,6 @@ def _old_and_new_decisions(self, submit_time, **request_data): def _publish_decision_change( self, submit_time, subject, testcase, product_version, publish_testcase ): - policy_attributes = dict( subject=subject, testcase=testcase, @@ -281,7 +282,9 @@ def _publish_decision_change( subject_identifier=subject.identifier, ) if decision is None or old_decision is None: - self._inc(decision_failed_counter.labels(decision_context=decision_context)) + self._inc( + decision_failed_counter.labels(decision_context=decision_context) + ) continue log_label = ( @@ -297,10 +300,14 @@ def _publish_decision_change( ) if _is_decision_unchanged(old_decision, decision): - self._inc(decision_unchanged_counter.labels(decision_context=decision_context)) + self._inc( + decision_unchanged_counter.labels(decision_context=decision_context) + ) continue - self._inc(decision_changed_counter.labels(decision_context=decision_context)) + self._inc( + decision_changed_counter.labels(decision_context=decision_context) + ) decision.update( { @@ -316,5 +323,7 @@ def _publish_decision_change( if publish_testcase: decision["testcase"] = testcase - self.app.logger.info("Publishing decision change message for: %s", log_label) + self.app.logger.info( + "Publishing decision change message for: %s", log_label + ) self._publish_decision_update(decision) diff --git a/greenwave/listeners/resultsdb.py b/greenwave/listeners/resultsdb.py index 045a98a5..13d487ea 100644 --- a/greenwave/listeners/resultsdb.py +++ b/greenwave/listeners/resultsdb.py @@ -2,8 +2,8 @@ from greenwave.listeners.base import BaseListener from greenwave.product_versions import subject_product_versions from greenwave.subjects.factory import ( - create_subject_from_data, UnknownSubjectDataError, + create_subject_from_data, ) diff --git a/greenwave/logger.py b/greenwave/logger.py index cf51ddce..1702401e 100644 --- a/greenwave/logger.py +++ b/greenwave/logger.py @@ -5,8 +5,8 @@ def log_to_stdout(level=logging.INFO): - fmt = '[%(asctime)s] [%(process)d] [%(levelname)s] %(name)s: %(message)s' - datefmt = '%Y-%m-%d %H:%M:%S' + fmt = "[%(asctime)s] [%(process)d] [%(levelname)s] %(name)s: %(message)s" + datefmt = "%Y-%m-%d %H:%M:%S" stream_handler = logging.StreamHandler(sys.stdout) stream_handler.setLevel(level) stream_handler.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt)) @@ -19,4 +19,4 @@ def init_logging(): # Note that the log level on the handler above controls what # will actually appear on stdout. logging.getLogger().setLevel(logging.INFO) - logging.getLogger('greenwave').setLevel(logging.DEBUG) + logging.getLogger("greenwave").setLevel(logging.DEBUG) diff --git a/greenwave/monitor.py b/greenwave/monitor.py index 303db065..1f77639d 100644 --- a/greenwave/monitor.py +++ b/greenwave/monitor.py @@ -13,9 +13,9 @@ # Note: StatsClient instances are thread-safe. @lru_cache def stats_client(): - statsd_url = os.environ.get('GREENWAVE_STATSD_HOST') + statsd_url = os.environ.get("GREENWAVE_STATSD_HOST") if statsd_url: - server, port = statsd_url.split(':') + server, port = statsd_url.split(":") return StatsClient(server, int(port)) return None @@ -30,11 +30,8 @@ def __str__(self): if not self.labeldict: return self.name - labels = ','.join( - f'{name}={value}' - for name, value in self.labeldict.items() - ) - return f'{self.name}[{labels}]' + labels = ",".join(f"{name}={value}" for name, value in self.labeldict.items()) + return f"{self.name}[{labels}]" class Counter(Stat): @@ -50,6 +47,7 @@ def labels(self, **labeldict): def count_exceptions(self): """Returns function decorator to increase counter on exception.""" + def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): @@ -58,13 +56,16 @@ def wrapper(*args, **kwargs): except BaseException: self.inc() raise + return wrapper + return decorator class Histogram(Stat): def time(self): """Returns function decorator to that sends recorder call time.""" + def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): @@ -73,31 +74,33 @@ def wrapper(*args, **kwargs): with client.timer(str(self)): return fn(*args, **kwargs) return fn(*args) + return wrapper + return decorator # Total number of messages received -messaging_rx_counter = Counter('messaging_rx') +messaging_rx_counter = Counter("messaging_rx") # Number of received messages, which were ignored -messaging_rx_ignored_counter = Counter('messaging_rx_ignored') +messaging_rx_ignored_counter = Counter("messaging_rx_ignored") # Number of received messages, which were processed successfully -messaging_rx_processed_ok_counter = Counter('messaging_rx_processed_ok') +messaging_rx_processed_ok_counter = Counter("messaging_rx_processed_ok") # Number of received messages, which failed during processing -messaging_rx_failed_counter = Counter('messaging_rx_failed') +messaging_rx_failed_counter = Counter("messaging_rx_failed") # Number of messages, which were sent successfully -messaging_tx_sent_ok_counter = Counter('messaging_tx_sent_ok') +messaging_tx_sent_ok_counter = Counter("messaging_tx_sent_ok") # Number of messages, for which the sender failed -messaging_tx_failed_counter = Counter('messaging_tx_failed') +messaging_tx_failed_counter = Counter("messaging_tx_failed") # All exceptions occurred in Greenwave "decision" API -decision_exception_counter = Counter('total_decision_exceptions') +decision_exception_counter = Counter("total_decision_exceptions") # Decision latency -decision_request_duration_seconds = Histogram('decision_request_duration_seconds') +decision_request_duration_seconds = Histogram("decision_request_duration_seconds") # New result/waiver caused specific decision to change -decision_changed_counter = Counter('decision_changed') +decision_changed_counter = Counter("decision_changed") # New result/waiver did not cause specific decision to change -decision_unchanged_counter = Counter('decision_unchanged') +decision_unchanged_counter = Counter("decision_unchanged") # Failed to retrieve decision for a new result/waiver -decision_failed_counter = Counter('decision_failed') +decision_failed_counter = Counter("decision_failed") diff --git a/greenwave/policies.py b/greenwave/policies.py index 08ba4412..447baaeb 100644 --- a/greenwave/policies.py +++ b/greenwave/policies.py @@ -1,24 +1,23 @@ # SPDX-License-Identifier: GPL-2.0+ -from collections import defaultdict -from fnmatch import fnmatch import glob import logging import os import re -from typing import Optional +from collections import defaultdict +from fnmatch import fnmatch -from werkzeug.exceptions import BadGateway, BadRequest, NotFound from flask import current_app +from werkzeug.exceptions import BadGateway, BadRequest, NotFound import greenwave.resources from greenwave.safe_yaml import ( SafeYAMLBool, SafeYAMLDateTime, + SafeYAMLError, SafeYAMLList, SafeYAMLObject, SafeYAMLString, - SafeYAMLError, ) log = logging.getLogger(__name__) @@ -32,26 +31,28 @@ def load_policies(policies_dir): :return: A list of policies. """ - policy_pathnames = glob.glob(os.path.join(policies_dir, '*.yaml')) + policy_pathnames = glob.glob(os.path.join(policies_dir, "*.yaml")) policies = [] for policy_pathname in policy_pathnames: - with open(policy_pathname, 'r') as f: + with open(policy_pathname) as f: policies.extend(greenwave.policies.Policy.safe_load_all(f)) log.debug("Loaded %i policies from %s", len(policies), policies_dir) return policies def _remote_url_templates(subject): - rr_policies_conf = current_app.config.get('REMOTE_RULE_POLICIES', {}) + rr_policies_conf = current_app.config.get("REMOTE_RULE_POLICIES", {}) cur_subject_urls = ( - rr_policies_conf.get(subject.type) or - rr_policies_conf.get('*') or - current_app.config.get('DIST_GIT_URL_TEMPLATE') + rr_policies_conf.get(subject.type) + or rr_policies_conf.get("*") + or current_app.config.get("DIST_GIT_URL_TEMPLATE") ) if not cur_subject_urls: - raise RuntimeError(f'Cannot use a remote rule for {subject} subject ' - f'as it has not been configured') + raise RuntimeError( + f"Cannot use a remote rule for {subject} subject " + "as it has not been configured" + ) if not isinstance(cur_subject_urls, list): cur_subject_urls = [cur_subject_urls] @@ -65,11 +66,14 @@ def _remote_urls(subject, url_templates): """ for current_url in url_templates: url_params = {} - if '{pkg_name}' in current_url or '{pkg_namespace}' in current_url or \ - '{rev}' in current_url: + if ( + "{pkg_name}" in current_url + or "{pkg_namespace}" in current_url + or "{rev}" in current_url + ): try: - pkg_namespace, pkg_name, rev = greenwave.resources.retrieve_scm_from_koji( - subject.identifier + pkg_namespace, pkg_name, rev = ( + greenwave.resources.retrieve_scm_from_koji(subject.identifier) ) except greenwave.resources.NoSourceException as e: log.warning(e) @@ -78,15 +82,15 @@ def _remote_urls(subject, url_templates): # if the element is actually a container and not a pkg there will be a "-container" # string at the end of the "pkg_name" and it will not match with the one in the # remote rule file URL - if pkg_namespace == 'containers': - pkg_name = re.sub('-container$', '', pkg_name) + if pkg_namespace == "containers": + pkg_name = re.sub("-container$", "", pkg_name) if pkg_namespace: - pkg_namespace += '/' + pkg_namespace += "/" url_params.update(rev=rev, pkg_name=pkg_name, pkg_namespace=pkg_namespace) - if '{subject_id}' in current_url: + if "{subject_id}" in current_url: subj_id = subject.identifier - if subj_id.startswith('sha256:'): + if subj_id.startswith("sha256:"): subj_id = subj_id[7:] url_params.update(subject_id=subj_id) @@ -97,7 +101,7 @@ class DisallowedRuleError(RuntimeError): pass -class Answer(object): +class Answer: """ Represents the result of evaluating a policy rule against a particular item. But we call it an "answer" because the word "result" is a bit @@ -116,8 +120,8 @@ def to_json(self): raise NotImplementedError() def __repr__(self): - attributes = ' '.join(f'{k}={v}' for k, v in self.to_json().items()) - return f'<{self.__class__.__name__} {attributes}>' + attributes = " ".join(f"{k}={v}" for k, v in self.to_json().items()) + return f"<{self.__class__.__name__} {attributes}>" class RuleSatisfied(Answer): @@ -171,15 +175,14 @@ def __init__(self, subject, test_case_name, scenario, source): def to_json(self): return { - 'type': 'test-result-missing', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'scenario': self.scenario, - 'source': self.source, - + "type": "test-result-missing", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "scenario": self.scenario, + "source": self.source, # For backwards compatibility only: - 'item': self.subject.to_dict() + "item": self.subject.to_dict(), } def to_waived(self, waiver_id): @@ -203,20 +206,19 @@ def __init__(self, subject, test_case_name, source, result_id, data): @property def scenario(self): - return self.data.get('scenario') + return self.data.get("scenario") def to_json(self): data = { # Same type as TestResultMissing for backwards compatibility - 'type': 'test-result-missing', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'source': self.source, - 'result_id': self.result_id, - + "type": "test-result-missing", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "source": self.source, + "result_id": self.result_id, # For backwards compatibility only: - 'item': self.subject.to_dict() + "item": self.subject.to_dict(), } data.update(self.data) return data @@ -232,17 +234,18 @@ class TestResultWaived(RuleSatisfied): Contains same data as unsatisfied rule except the type has "-waived" suffix. Also, the deprecated "item" field is dropped. """ + def __init__(self, unsatisfied_rule, waiver_id): self.unsatisfied_rule = unsatisfied_rule self.waiver_id = waiver_id def to_json(self): satisfied_rule = self.unsatisfied_rule.to_json() - satisfied_rule['type'] += '-waived' - satisfied_rule['waiver_id'] = self.waiver_id + satisfied_rule["type"] += "-waived" + satisfied_rule["waiver_id"] = self.waiver_id - if 'item' in satisfied_rule: - del satisfied_rule['item'] + if "item" in satisfied_rule: + del satisfied_rule["item"] return satisfied_rule @@ -264,21 +267,20 @@ def __init__(self, subject, test_case_name, source, result_id, data): @property def scenario(self): - return self.data.get('scenario') + return self.data.get("scenario") def to_json(self): data = { - 'type': 'test-result-failed', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'source': self.source, - 'result_id': self.result_id, - + "type": "test-result-failed", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "source": self.source, + "result_id": self.result_id, # These are for backwards compatibility only # (the values are already visible in the result data itself, the # caller shouldn't need them repeated here): - 'item': self.subject.to_dict(), + "item": self.subject.to_dict(), } data.update(self.data) return data @@ -296,14 +298,7 @@ class TestResultErrored(RuleNotSatisfied): summary_text = "test{s} errored" - def __init__( - self, - subject, - test_case_name, - source, - result_id, - data, - error_reason): + def __init__(self, subject, test_case_name, source, result_id, data, error_reason): self.subject = subject self.test_case_name = test_case_name self.source = source @@ -313,22 +308,21 @@ def __init__( @property def scenario(self): - return self.data.get('scenario') + return self.data.get("scenario") def to_json(self): data = { - 'type': 'test-result-errored', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'source': self.source, - 'result_id': self.result_id, - 'error_reason': self.error_reason, - + "type": "test-result-errored", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "source": self.source, + "result_id": self.result_id, + "error_reason": self.error_reason, # These are for backwards compatibility only # (the values are already visible in the result data itself, the # caller shouldn't need them repeated here): - 'item': self.subject.to_dict(), + "item": self.subject.to_dict(), } data.update(self.data) return data @@ -354,13 +348,13 @@ def __init__(self, subject, test_case_name, details, source): def to_json(self): return { - 'type': 'invalid-gating-yaml', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'scenario': self.scenario, - 'source': self.source, - 'details': self.details + "type": "invalid-gating-yaml", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "scenario": self.scenario, + "source": self.source, + "details": self.details, } def to_waived(self, waiver_id): @@ -372,7 +366,7 @@ class MissingRemoteRuleYaml(RuleNotSatisfied): Remote policy not found in remote repository. """ - test_case_name = 'missing-gating-yaml' + test_case_name = "missing-gating-yaml" scenario = None is_test_result = False summary_text = "error{s} due to missing remote rule file" @@ -383,12 +377,12 @@ def __init__(self, subject, sources): def to_json(self): return { - 'type': 'missing-gating-yaml', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'scenario': self.scenario, - 'sources': self.sources, + "type": "missing-gating-yaml", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "scenario": self.scenario, + "sources": self.sources, } def to_waived(self, waiver_id): @@ -400,7 +394,7 @@ class FailedFetchRemoteRuleYaml(RuleNotSatisfied): Error while fetching remote policy. """ - test_case_name = 'failed-fetch-gating-yaml' + test_case_name = "failed-fetch-gating-yaml" scenario = None is_test_result = False summary_text = "error{s} while trying to fetch remote rule file" @@ -412,13 +406,13 @@ def __init__(self, subject, sources, error): def to_json(self): return { - 'type': 'failed-fetch-gating-yaml', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'scenario': self.scenario, - 'sources': self.sources, - 'error': self.error, + "type": "failed-fetch-gating-yaml", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "scenario": self.scenario, + "sources": self.sources, + "error": self.error, } def to_waived(self, waiver_id): @@ -438,11 +432,11 @@ def __init__(self, subject, source): def to_json(self): return { - 'type': 'fetched-gating-yaml', - 'testcase': 'fetched-gating-yaml', - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'source': self.source, + "type": "fetched-gating-yaml", + "testcase": "fetched-gating-yaml", + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "source": self.source, } @@ -451,6 +445,7 @@ class TestResultPassed(RuleSatisfied): A required test case passed (that is, its outcome in ResultsDB was passing) or a corresponding waiver was found. """ + def __init__(self, subject, test_case_name, source, result_id, data): self.subject = subject self.test_case_name = test_case_name @@ -460,12 +455,12 @@ def __init__(self, subject, test_case_name, source, result_id, data): def to_json(self): data = { - 'type': 'test-result-passed', - 'testcase': self.test_case_name, - 'subject_type': self.subject.type, - 'subject_identifier': self.subject.identifier, - 'source': self.source, - 'result_id': self.result_id, + "type": "test-result-passed", + "testcase": self.test_case_name, + "subject_type": self.subject.type, + "subject_identifier": self.subject.identifier, + "source": self.source, + "result_id": self.result_id, } data.update(self.data) return data @@ -484,17 +479,18 @@ def __init__(self, subject_identifier, policy): def to_json(self): return { - 'type': 'excluded', - 'subject_identifier': self.subject_identifier, - 'policy': self.policy.id, - 'source': self.policy.source, + "type": "excluded", + "subject_identifier": self.subject_identifier, + "policy": self.policy.id, + "source": self.policy.source, } -class _Summary(object): +class _Summary: """ Internal class using for generating the result summary. """ + def __init__(self): self.test_msgs = defaultdict(int) self.non_test_msgs = defaultdict(int) @@ -523,10 +519,10 @@ def to_text(self): # if we got here, there should be no unsatisfied results if self.test_count: # this means we had some passed/waived tests - return f'All required tests ({self.test_count} total) have passed or been waived' + return f"All required tests ({self.test_count} total) have passed or been waived" # otherwise, should mean we had no required tests - return 'No tests are required' + return "No tests are required" def append(self, answer: Answer): if isinstance(answer, RuleNotSatisfied): @@ -539,7 +535,7 @@ def append(self, answer: Answer): @staticmethod def sformat(text: str, number: int) -> str: - return text.format(num=number, s='s' if number != 1 else '') + return text.format(num=number, s="s" if number != 1 else "") def summarize_answers(answers): @@ -582,7 +578,7 @@ def check(self, policy, rule_context): raise NotImplementedError() def matches(self, policy, **attributes): - #pylint: disable=unused-argument + # pylint: disable=unused-argument """ Same as Policy.matches() for a rule attributes. @@ -599,14 +595,14 @@ def __eq__(self, other): class RemoteRule(Rule): - yaml_tag = '!RemoteRule' + yaml_tag = "!RemoteRule" safe_yaml_attributes = { - 'sources': SafeYAMLList(str, optional=True), - 'required': SafeYAMLBool(optional=True, default=False), + "sources": SafeYAMLList(str, optional=True), + "required": SafeYAMLBool(optional=True, default=False), } def _get_sub_policies(self, policy, subject): - #pylint: disable=broad-except + # pylint: disable=broad-except """ Returns matching policies from the first available remote rule file, and answers (including FetchedRemoteRuleYaml, MissingRemoteRuleYaml, @@ -621,19 +617,25 @@ def _get_sub_policies(self, policy, subject): url_templates = self.sources or _remote_url_templates(subject) for remote_policies_url in _remote_urls(subject, url_templates): remote_policies_urls.append(remote_policies_url) - response = greenwave.resources.retrieve_yaml_remote_rule(remote_policies_url) + response = greenwave.resources.retrieve_yaml_remote_rule( + remote_policies_url + ) if response is not None: break except NotFound: - error = f'Koji build not found for {subject}' + error = f"Koji build not found for {subject}" return [], [FailedFetchRemoteRuleYaml(subject, remote_policies_urls, error)] except greenwave.resources.KojiScmUrlParseError as err: - return [], [FailedFetchRemoteRuleYaml(subject, remote_policies_urls, err.description)] + return [], [ + FailedFetchRemoteRuleYaml( + subject, remote_policies_urls, err.description + ) + ] except BadGateway: raise except Exception: - logging.exception('Failed to retrieve policies for %r', subject) - error = 'Unexpected error while fetching remote policies' + logging.exception("Failed to retrieve policies for %r", subject) + error = "Unexpected error while fetching remote policies" raise BadGateway(error) # Remote rule file not found? @@ -649,14 +651,17 @@ def _get_sub_policies(self, policy, subject): except SafeYAMLError as e: answers.append( InvalidRemoteRuleYaml( - subject, 'invalid-gating-yaml', str(e), remote_policies_url)) + subject, "invalid-gating-yaml", str(e), remote_policies_url + ) + ) policies = [] for sub_policy in policies: sub_policy.source = remote_policies_url sub_policies = [ - sub_policy for sub_policy in policies + sub_policy + for sub_policy in policies if policy.matches_sub_policy(sub_policy) ] return sub_policies, answers @@ -682,10 +687,10 @@ def check(self, policy, rule_context): return answers def matches(self, policy, **attributes): - if attributes.get('match_any_remote_rule'): + if attributes.get("match_any_remote_rule"): return True - subject = attributes.get('subject') + subject = attributes.get("subject") if not subject: return True @@ -705,9 +710,9 @@ def matches(self, policy, **attributes): def to_json(self): return { - 'rule': self.__class__.__name__, - 'required': self.required, - 'sources': self.sources, + "rule": self.__class__.__name__, + "required": self.required, + "sources": self.sources, } @@ -716,20 +721,24 @@ class PassingTestCaseRule(Rule): This rule requires either a passing result for the given test case, or a non-passing result with a waiver. """ - yaml_tag = '!PassingTestCaseRule' + + yaml_tag = "!PassingTestCaseRule" safe_yaml_attributes = { - 'test_case_name': SafeYAMLString(), - 'scenario': SafeYAMLString(optional=True), - 'valid_since': SafeYAMLDateTime(optional=True), - 'valid_until': SafeYAMLDateTime(optional=True), + "test_case_name": SafeYAMLString(), + "scenario": SafeYAMLString(optional=True), + "valid_since": SafeYAMLDateTime(optional=True), + "valid_until": SafeYAMLDateTime(optional=True), } def check(self, policy, rule_context): if self.valid_since or self.valid_until: koji_url = current_app.config["KOJI_BASE_URL"] - subject_creation_time = greenwave.resources.retrieve_koji_build_creation_time( - rule_context.subject.identifier, koji_url) + subject_creation_time = ( + greenwave.resources.retrieve_koji_build_creation_time( + rule_context.subject.identifier, koji_url + ) + ) if self.valid_since and subject_creation_time < self.valid_since: return [] if self.valid_until and self.valid_until <= subject_creation_time: @@ -739,14 +748,20 @@ def check(self, policy, rule_context): if self.scenario is not None: matching_results = [ - result for result in matching_results - if self.scenario in result['data'].get('scenario', [])] + result + for result in matching_results + if self.scenario in result["data"].get("scenario", []) + ] # Investigate the absence of result first. if not matching_results: return [ TestResultMissing( - rule_context.subject, self.test_case_name, self.scenario, policy.source) + rule_context.subject, + self.test_case_name, + self.scenario, + policy.source, + ) ] # If we find multiple matching results, we always use the first one which @@ -758,51 +773,72 @@ def check(self, policy, rule_context): ] def matches(self, policy, **attributes): - testcase = attributes.get('testcase') + testcase = attributes.get("testcase") return not testcase or testcase == self.test_case_name def to_json(self): return { - 'rule': self.__class__.__name__, - 'test_case_name': self.test_case_name, - 'scenario': self.scenario, + "rule": self.__class__.__name__, + "test_case_name": self.test_case_name, + "scenario": self.scenario, } def _answer_for_result(self, result, subject, source): - outcome = result['outcome'] + outcome = result["outcome"] + + additional_keys = current_app.config["DISTINCT_LATEST_RESULTS_ON"] + data = {key: (result["data"].get(key) or [None])[0] for key in additional_keys} + + if outcome in current_app.config["OUTCOMES_PASSED"]: + log.debug( + "Test result passed for the result_id %s and testcase %s," + " because the outcome is %s", + result["id"], + self.test_case_name, + outcome, + ) + return TestResultPassed( + subject, self.test_case_name, source, result["id"], data + ) - additional_keys = current_app.config['DISTINCT_LATEST_RESULTS_ON'] - data = { - key: (result['data'].get(key) or [None])[0] - for key in additional_keys - } + if outcome in current_app.config["OUTCOMES_INCOMPLETE"]: + log.debug( + "Test result MISSING for the %s and " + "testcase %s, because the outcome is %s", + subject, + self.test_case_name, + outcome, + ) + return TestResultIncomplete( + subject, self.test_case_name, source, result["id"], data + ) - if outcome in current_app.config['OUTCOMES_PASSED']: - log.debug('Test result passed for the result_id %s and testcase %s,' - ' because the outcome is %s', result['id'], self.test_case_name, - outcome) - return TestResultPassed(subject, self.test_case_name, source, result['id'], data) - - if outcome in current_app.config['OUTCOMES_INCOMPLETE']: - log.debug('Test result MISSING for the %s and ' - 'testcase %s, because the outcome is %s', subject, - self.test_case_name, outcome) - return TestResultIncomplete(subject, self.test_case_name, source, result['id'], data) - - if outcome in current_app.config['OUTCOMES_ERROR']: - error_reason = result.get('error_reason') - log.debug('Test result ERROR for the %s and ' - 'testcase %s, because the outcome is %s; error reason: %s', - subject, self.test_case_name, outcome, error_reason) + if outcome in current_app.config["OUTCOMES_ERROR"]: + error_reason = result.get("error_reason") + log.debug( + "Test result ERROR for the %s and " + "testcase %s, because the outcome is %s; error reason: %s", + subject, + self.test_case_name, + outcome, + error_reason, + ) return TestResultErrored( - subject, self.test_case_name, source, result['id'], data, - error_reason) + subject, self.test_case_name, source, result["id"], data, error_reason + ) - log.debug('Test result failed for the %s and ' - 'testcase %s, because the outcome is %s and it didn\'t match any of the ' - 'previous cases', subject, self.test_case_name, outcome) + log.debug( + "Test result failed for the %s and " + "testcase %s, because the outcome is %s and it didn't match any of the " + "previous cases", + subject, + self.test_case_name, + outcome, + ) - return TestResultFailed(subject, self.test_case_name, source, result['id'], data) + return TestResultFailed( + subject, self.test_case_name, source, result["id"], data + ) class ObsoleteRule(Rule): @@ -810,47 +846,48 @@ class ObsoleteRule(Rule): The base class for an obsolete rule. When these rules are parsed, a SafeYAMLError exception will be raised. """ - advice = 'Please refer to the documentation for more information.' + + advice = "Please refer to the documentation for more information." safe_yaml_attributes = {} def __init__(self): - tag = self.yaml_tag or '!' + type(self).__name__ - raise SafeYAMLError('{} is obsolete. {}'.format(tag, self.advice)) + tag = self.yaml_tag or "!" + type(self).__name__ + raise SafeYAMLError(f"{tag} is obsolete. {self.advice}") def check(self, policy, rule_context): - raise ValueError('This rule is obsolete and can\'t be checked') + raise ValueError("This rule is obsolete and can't be checked") class PackageSpecificBuild(ObsoleteRule): - yaml_tag = '!PackageSpecificBuild' + yaml_tag = "!PackageSpecificBuild" advice = 'Please use the "packages" allowlist instead.' class FedoraAtomicCi(PackageSpecificBuild): - yaml_tag = '!FedoraAtomicCi' + yaml_tag = "!FedoraAtomicCi" class Policy(SafeYAMLObject): - root_yaml_tag: Optional[str] = '!Policy' + root_yaml_tag: str | None = "!Policy" safe_yaml_attributes = { - 'id': SafeYAMLString(), - 'product_versions': SafeYAMLList(str), - 'decision_context': SafeYAMLString(optional=True), - 'decision_contexts': SafeYAMLList(str, optional=True, default=list()), - 'subject_type': SafeYAMLString(), - 'rules': SafeYAMLList(Rule), - 'excluded_packages': SafeYAMLList(str, optional=True), - 'packages': SafeYAMLList(str, optional=True), - 'relevance_key': SafeYAMLString(optional=True), - 'relevance_value': SafeYAMLString(optional=True), + "id": SafeYAMLString(), + "product_versions": SafeYAMLList(str), + "decision_context": SafeYAMLString(optional=True), + "decision_contexts": SafeYAMLList(str, optional=True, default=[]), + "subject_type": SafeYAMLString(), + "rules": SafeYAMLList(Rule), + "excluded_packages": SafeYAMLList(str, optional=True), + "packages": SafeYAMLList(str, optional=True), + "relevance_key": SafeYAMLString(optional=True), + "relevance_value": SafeYAMLString(optional=True), } source = None def validate(self): if not self.decision_context and not self.decision_contexts: - raise SafeYAMLError('No decision contexts provided') + raise SafeYAMLError("No decision contexts provided") if self.decision_context and self.decision_contexts: raise SafeYAMLError( 'Both properties "decision_contexts" and "decision_context" were set' @@ -867,28 +904,33 @@ def matches(self, **attributes): There must be at least one matching rule or no rules in the policy. """ - decision_contexts = attributes.get('decision_context') + decision_contexts = attributes.get("decision_context") if decision_contexts and not isinstance(decision_contexts, list): decision_contexts = [decision_contexts] - if decision_contexts and not any(context in self.all_decision_contexts - for context in decision_contexts): + if decision_contexts and not any( + context in self.all_decision_contexts for context in decision_contexts + ): return False - product_version = attributes.get('product_version') + product_version = attributes.get("product_version") if product_version and not self.matches_product_version(product_version): return False if not self.matches_subject_type(**attributes): return False - return not self.rules or any(rule.matches(self, **attributes) for rule in self.rules) + return not self.rules or any( + rule.matches(self, **attributes) for rule in self.rules + ) def matches_subject_type(self, **attributes): - subject = attributes.get('subject') + subject = attributes.get("subject") return not subject or subject.subject_type.matches(self.subject_type) def matches_sub_policy(self, sub_policy): - return set(sub_policy.all_decision_contexts).intersection(self.all_decision_contexts) + return set(sub_policy.all_decision_contexts).intersection( + self.all_decision_contexts + ) def check(self, rule_context): name = rule_context.subject.package_name @@ -896,7 +938,9 @@ def check(self, rule_context): for exclude in self.excluded_packages: if fnmatch(name, exclude): return [ExcludedInPolicy(rule_context.subject.identifier, self)] - if self.packages and not any(fnmatch(name, package) for package in self.packages): + if self.packages and not any( + fnmatch(name, package) for package in self.packages + ): # If the `packages` allowlist is set and this package isn't in the # `packages` allowlist, then the policy doesn't apply to it return [] @@ -907,11 +951,13 @@ def check(self, rule_context): return answers def matches_product_version(self, product_version): - return any(fnmatch(product_version, version) for version in self.product_versions) + return any( + fnmatch(product_version, version) for version in self.product_versions + ) @property def safe_yaml_label(self): - return 'Policy {!r}'.format(self.id or 'untitled') + return "Policy {!r}".format(self.id or "untitled") @property def all_decision_contexts(self): @@ -919,7 +965,7 @@ def all_decision_contexts(self): return self.decision_contexts if self.decision_context: return [self.decision_context] - raise SafeYAMLError('No decision contexts provided') + raise SafeYAMLError("No decision contexts provided") class OnDemandPolicy(Policy): @@ -929,42 +975,44 @@ class OnDemandPolicy(Policy): def create_from_json(cls, data): try: data2 = { - 'id': 'on-demand-policy', - 'product_versions': [data['product_version']], - 'decision_context': 'on-demand-policy', - 'subject_type': 'unused', + "id": "on-demand-policy", + "product_versions": [data["product_version"]], + "decision_context": "on-demand-policy", + "subject_type": "unused", } data2.update(data) result = cls.from_value(data2) return result except SafeYAMLError as e: - raise BadRequest('Failed to parse on demand policy: {}'.format(e)) + raise BadRequest(f"Failed to parse on demand policy: {e}") def matches_subject_type(self, **attributes): return True def matches_sub_policy(self, sub_policy): - return any(sub_policy.matches_product_version(pv) for pv in self.product_versions) + return any( + sub_policy.matches_product_version(pv) for pv in self.product_versions + ) class RemotePolicy(Policy): - root_yaml_tag = '!Policy' + root_yaml_tag = "!Policy" safe_yaml_attributes = { - 'id': SafeYAMLString(optional=True), - 'product_versions': SafeYAMLList(str, default=['*'], optional=True), - 'subject_type': SafeYAMLString(optional=True, default='koji_build'), - 'decision_context': SafeYAMLString(optional=True), - 'decision_contexts': SafeYAMLList(str, optional=True), - 'rules': SafeYAMLList(Rule), - 'excluded_packages': SafeYAMLList(str, optional=True), - 'packages': SafeYAMLList(str, optional=True), + "id": SafeYAMLString(optional=True), + "product_versions": SafeYAMLList(str, default=["*"], optional=True), + "subject_type": SafeYAMLString(optional=True, default="koji_build"), + "decision_context": SafeYAMLString(optional=True), + "decision_contexts": SafeYAMLList(str, optional=True), + "rules": SafeYAMLList(Rule), + "excluded_packages": SafeYAMLList(str, optional=True), + "packages": SafeYAMLList(str, optional=True), } def validate(self): for rule in self.rules: if isinstance(rule, RemoteRule): - raise SafeYAMLError('RemoteRule is not allowed in remote policies') + raise SafeYAMLError("RemoteRule is not allowed in remote policies") super().validate() @@ -973,10 +1021,14 @@ def _applicable_decision_context_product_version_pairs(policies, **attributes): policy for policy in policies if policy.matches(**attributes) ] - log.debug("found %i applicable policies of %i for: %r", - len(applicable_policies), len(policies), attributes) + log.debug( + "found %i applicable policies of %i for: %r", + len(applicable_policies), + len(policies), + attributes, + ) - product_version = attributes.get('product_version') + product_version = attributes.get("product_version") if product_version: for policy in applicable_policies: for decision_context in policy.all_decision_contexts: @@ -990,9 +1042,9 @@ def _applicable_decision_context_product_version_pairs(policies, **attributes): def applicable_decision_context_product_version_pairs(policies, **attributes): - contexts_product_versions = sorted(set( - _applicable_decision_context_product_version_pairs( - policies, **attributes))) + contexts_product_versions = sorted( + set(_applicable_decision_context_product_version_pairs(policies, **attributes)) + ) log.debug("found %i decision contexts", len(contexts_product_versions)) return contexts_product_versions @@ -1001,7 +1053,7 @@ def applicable_decision_context_product_version_pairs(policies, **attributes): def _missing_decision_contexts_in_parent_policies(policies): missing_decision_contexts = set() parent_dcs = set() - for parent_policy in current_app.config['policies']: + for parent_policy in current_app.config["policies"]: parent_dcs.update(set(parent_policy.all_decision_contexts)) for policy in policies: missing_decision_contexts.update( diff --git a/greenwave/product_versions.py b/greenwave/product_versions.py index 2fcec96f..b83db2e3 100644 --- a/greenwave/product_versions.py +++ b/greenwave/product_versions.py @@ -5,8 +5,6 @@ import logging import re -import socket -from typing import List from defusedxml.xmlrpc import xmlrpc_client from werkzeug.exceptions import BadGateway, NotFound @@ -19,25 +17,25 @@ log = logging.getLogger(__name__) -def _guess_product_versions(toparse, koji_build=False) -> List[str]: - if toparse == 'rawhide' or toparse.startswith('Fedora-Rawhide'): - return ['fedora-rawhide'] +def _guess_product_versions(toparse, koji_build=False) -> list[str]: + if toparse == "rawhide" or toparse.startswith("Fedora-Rawhide"): + return ["fedora-rawhide"] product_version = None - if toparse.startswith('f') and koji_build: - product_version = 'fedora-' - elif toparse.startswith('epel'): - product_version = 'epel-' - elif toparse.startswith('el') and len(toparse) > 2 and toparse[2].isdigit(): - product_version = 'rhel-' - elif toparse.startswith('rhel-') and len(toparse) > 5 and toparse[5].isdigit(): - product_version = 'rhel-' - elif toparse.startswith('fc') or toparse.startswith('Fedora'): - product_version = 'fedora-' + if toparse.startswith("f") and koji_build: + product_version = "fedora-" + elif toparse.startswith("epel"): + product_version = "epel-" + elif toparse.startswith("el") and len(toparse) > 2 and toparse[2].isdigit(): + product_version = "rhel-" + elif toparse.startswith("rhel-") and len(toparse) > 5 and toparse[5].isdigit(): + product_version = "rhel-" + elif toparse.startswith("fc") or toparse.startswith("Fedora"): + product_version = "fedora-" if product_version: # seperate the prefix from the number - result = list(filter(None, '-'.join(re.split(r'(\d+)', toparse)).split('-'))) + result = list(filter(None, "-".join(re.split(r"(\d+)", toparse)).split("-"))) if len(result) >= 2: try: int(result[1]) @@ -51,7 +49,8 @@ def _guess_product_versions(toparse, koji_build=False) -> List[str]: def _guess_koji_build_product_versions( - subject, koji_base_url, koji_task_id=None) -> List[str]: + subject, koji_base_url, koji_task_id=None +) -> list[str]: try: if not koji_task_id: try: @@ -72,28 +71,27 @@ def _guess_koji_build_product_versions( return _guess_product_versions(target, koji_build=True) return [] - except (xmlrpc_client.ProtocolError, socket.error) as err: - raise ConnectionError('Could not reach Koji: {}'.format(err)) + except (xmlrpc_client.ProtocolError, OSError) as err: + raise ConnectionError(f"Could not reach Koji: {err}") except BadGateway: - log.warning('Failed to get product version from Koji') + log.warning("Failed to get product version from Koji") return [] def subject_product_versions( - subject, - koji_base_url=None, - koji_task_id=None) -> List[str]: + subject, koji_base_url=None, koji_task_id=None +) -> list[str]: if subject.product_versions: return subject.product_versions if koji_base_url and subject.is_koji_build: - pvs = _guess_koji_build_product_versions( - subject, koji_base_url, koji_task_id) + pvs = _guess_koji_build_product_versions(subject, koji_base_url, koji_task_id) if pvs: return pvs if subject.short_product_version: return _guess_product_versions( - subject.short_product_version, koji_build=subject.is_koji_build) + subject.short_product_version, koji_build=subject.is_koji_build + ) return [] diff --git a/greenwave/request_session.py b/greenwave/request_session.py index f3a04122..5ca12c97 100644 --- a/greenwave/request_session.py +++ b/greenwave/request_session.py @@ -1,15 +1,13 @@ import logging +from json import dumps import requests - -from json import dumps +from flask import current_app, has_app_context from opentelemetry.instrumentation.requests import RequestsInstrumentor from requests.adapters import HTTPAdapter from requests.exceptions import ConnectionError, ConnectTimeout, RetryError -from urllib3.util.retry import Retry from urllib3.exceptions import ProxyError, SSLError - -from flask import current_app, has_app_context +from urllib3.util.retry import Retry from greenwave import __version__ @@ -28,19 +26,19 @@ def __init__(self, status_code, error_message, url): @property def content(self): - return dumps({'message': self._error_message}).encode() + return dumps({"message": self._error_message}).encode() class RequestsSession(requests.Session): def request(self, *args, **kwargs): # pylint:disable=arguments-differ - log.debug('Request: args=%r, kwargs=%r', args, kwargs) + log.debug("Request: args=%r, kwargs=%r", args, kwargs) - req_url = kwargs.get('url', args[1]) + req_url = kwargs.get("url", args[1]) - kwargs.setdefault('headers', {'Content-Type': 'application/json'}) + kwargs.setdefault("headers", {"Content-Type": "application/json"}) if has_app_context(): - kwargs.setdefault('timeout', current_app.config['REQUESTS_TIMEOUT']) - kwargs.setdefault('verify', current_app.config['REQUESTS_VERIFY']) + kwargs.setdefault("timeout", current_app.config["REQUESTS_TIMEOUT"]) + kwargs.setdefault("verify", current_app.config["REQUESTS_VERIFY"]) try: ret_val = super().request(*args, **kwargs) @@ -49,12 +47,12 @@ def request(self, *args, **kwargs): # pylint:disable=arguments-differ except (ConnectionError, ProxyError, SSLError) as e: ret_val = ErrorResponse(502, str(e), req_url) - log.debug('Request finished: %r', ret_val) + log.debug("Request finished: %r", ret_val) return ret_val def get_requests_session(): - """ Get http(s) session for request processing. """ + """Get http(s) session for request processing.""" session = RequestsSession() retry = Retry( @@ -63,10 +61,10 @@ def get_requests_session(): connect=3, backoff_factor=1, status_forcelist=(500, 502, 503, 504), - allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union(('POST',)), + allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union(("POST",)), ) adapter = HTTPAdapter(max_retries=retry) - session.mount('http://', adapter) - session.mount('https://', adapter) + session.mount("http://", adapter) + session.mount("https://", adapter) session.headers["User-Agent"] = f"greenwave {__version__}" return session diff --git a/greenwave/resources.py b/greenwave/resources.py index 8a19c1f0..6f91bec4 100644 --- a/greenwave/resources.py +++ b/greenwave/resources.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0+ -""" Greenwave resources. +"""Greenwave resources. This module contains routines for interacting with other services (resultsdb, waiverdb, etc..). @@ -9,14 +9,12 @@ import datetime import logging import re -import socket import threading -from typing import List, Optional +from urllib.parse import urlparse from dateutil import tz from dateutil.parser import parse from defusedxml.xmlrpc import xmlrpc_client -from urllib.parse import urlparse from flask import current_app from opentelemetry import trace from werkzeug.exceptions import BadGateway, NotFound @@ -45,7 +43,7 @@ def _koji(uri: str): def _requests_timeout(): - timeout = current_app.config['REQUESTS_TIMEOUT'] + timeout = current_app.config["REQUESTS_TIMEOUT"] if isinstance(timeout, tuple): return timeout[1] return timeout @@ -64,28 +62,28 @@ def _raise_for_status(response): class BaseRetriever: - ignore_ids: List[int] + ignore_ids: list[int] url: str - since: Optional[str] + since: str | None - def __init__(self, ignore_ids: List[int], when: str, url: str): + def __init__(self, ignore_ids: list[int], when: str, url: str): self.ignore_ids = ignore_ids self.url = url if when: - self.since = '1900-01-01T00:00:00.000000,{}'.format(when) + self.since = f"1900-01-01T00:00:00.000000,{when}" else: self.since = None @tracer.start_as_current_span("retrieve") def retrieve(self, *args, **kwargs): items = self._retrieve_all(*args, **kwargs) - return [item for item in items if item['id'] not in self.ignore_ids] + return [item for item in items if item["id"] not in self.ignore_ids] def _retrieve_data(self, params): response = self._make_request(params) _raise_for_status(response) - return response.json()['data'] + return response.json()["data"] class ResultsRetriever(BaseRetriever): @@ -95,8 +93,7 @@ class ResultsRetriever(BaseRetriever): def __init__(self, **args): super().__init__(**args) - self._distinct_on = ','.join( - current_app.config['DISTINCT_LATEST_RESULTS_ON']) + self._distinct_on = ",".join(current_app.config["DISTINCT_LATEST_RESULTS_ON"]) self.cache = {} def _retrieve_all(self, subject, testcase=None): @@ -104,25 +101,28 @@ def _retrieve_all(self, subject, testcase=None): # retrieved for given Subject. cache_key = (subject.type, subject.identifier) if testcase and cache_key in self.cache: - return [res for res in self.cache[cache_key] if res['testcase']['name'] == testcase] + return [ + res + for res in self.cache[cache_key] + if res["testcase"]["name"] == testcase + ] # Try to get passing test case result from external cache. external_cache_key = None if testcase: external_cache_key = ( "greenwave.resources:ResultsRetriever|" - f"{subject.type} {subject.identifier} {testcase}") + f"{subject.type} {subject.identifier} {testcase}" + ) results = self.get_external_cache(external_cache_key) if results and self._results_match_time(results): return results - params = { - '_distinct_on': self._distinct_on - } + params = {"_distinct_on": self._distinct_on} if self.since: - params.update({'since': self.since}) + params.update({"since": self.since}) if testcase: - params.update({'testcases': testcase}) + params.update({"testcases": testcase}) results = [] for query in subject.result_queries(): @@ -135,24 +135,24 @@ def _retrieve_all(self, subject, testcase=None): # Store test case results in external cache if all are passing, # otherwise retrieve from ResultsDB again later. if external_cache_key and all( - result.get('outcome') in current_app.config['OUTCOMES_PASSED'] - for result in results): + result.get("outcome") in current_app.config["OUTCOMES_PASSED"] + for result in results + ): self.set_external_cache(external_cache_key, results) return results def _make_request(self, params, **request_args): return requests_session.get( - self.url + '/results/latest', - params=params, - **request_args) + self.url + "/results/latest", params=params, **request_args + ) def _results_match_time(self, results): if not self.since: return True - until = self.since.split(',')[1] - return all(result['submit_time'] < until for result in results) + until = self.since.split(",")[1] + return all(result["submit_time"] < until for result in results) def get_external_cache(self, key): return current_app.cache.get(key) @@ -169,15 +169,14 @@ class WaiversRetriever(BaseRetriever): def _retrieve_all(self, filters): if self.since: for filter_ in filters: - filter_.update({'since': self.since}) + filter_.update({"since": self.since}) waivers = self._retrieve_data(filters) - return [waiver for waiver in waivers if waiver['waived']] + return [waiver for waiver in waivers if waiver["waived"]] def _make_request(self, params, **request_args): return requests_session.post( - self.url + '/waivers/+filtered', - json={'filters': params}, - **request_args) + self.url + "/waivers/+filtered", json={"filters": params}, **request_args + ) class NoSourceException(RuntimeError): @@ -188,18 +187,16 @@ class KojiScmUrlParseError(BadGateway): """ Exception raised when parsing SCM revision from Koji build fails. """ - pass @cached def retrieve_koji_build_target(task_id, koji_url: str): - log.debug('Getting Koji task request ID %r', task_id) + log.debug("Getting Koji task request ID %r", task_id) proxy = _koji(koji_url) try: task_request = proxy.getTaskRequest(task_id) except xmlrpc_client.Fault as e: - error = ( - f'Failed to get Koji task request ID {task_id}: {e.faultString} (code: {e.faultCode})') + error = f"Failed to get Koji task request ID {task_id}: {e.faultString} (code: {e.faultCode})" log.debug(error) raise BadGateway(error) @@ -212,7 +209,7 @@ def retrieve_koji_build_target(task_id, koji_url: str): @cached def _retrieve_koji_build_attributes(nvr: str, koji_url: str): - log.debug('Getting Koji build %r', nvr) + log.debug("Getting Koji build %r", nvr) proxy = _koji(koji_url) try: @@ -223,9 +220,7 @@ def _retrieve_koji_build_attributes(nvr: str, koji_url: str): raise BadGateway(error) if not build: - raise NotFound( - 'Failed to find Koji build for "{}" at "{}"'.format(nvr, koji_url) - ) + raise NotFound(f'Failed to find Koji build for "{nvr}" at "{koji_url}"') task_id = build.get("task_id") @@ -236,7 +231,7 @@ def _retrieve_koji_build_attributes(nvr: str, koji_url: str): except (TypeError, KeyError, AttributeError): source = None - creation_time = build.get('creation_time') + creation_time = build.get("creation_time") return (task_id, source, creation_time) @@ -258,8 +253,7 @@ def retrieve_koji_build_creation_time(nvr: str, koji_url: str): return time except ValueError: log.warning( - 'Could not parse Koji build creation_time %r for nvr %r', - creation_time, nvr + "Could not parse Koji build creation_time %r for nvr %r", creation_time, nvr ) return datetime.datetime.now(tz.tzutc()) @@ -270,8 +264,8 @@ def retrieve_scm_from_koji(nvr: str): koji_url = current_app.config["KOJI_BASE_URL"] try: source = retrieve_koji_build_source(nvr, koji_url) - except (xmlrpc_client.ProtocolError, socket.error) as err: - raise ConnectionError("Could not reach Koji: {}".format(err)) + except (xmlrpc_client.ProtocolError, OSError) as err: + raise ConnectionError(f"Could not reach Koji: {err}") return retrieve_scm_from_koji_build(nvr, source, koji_url) @@ -284,7 +278,7 @@ def retrieve_scm_from_koji_build(nvr: str, source: str, koji_url: str): url = urlparse(source) - path_components = url.path.rsplit('/', 2) + path_components = url.path.rsplit("/", 2) if len(path_components) < 3: namespace = "" else: @@ -294,22 +288,24 @@ def retrieve_scm_from_koji_build(nvr: str, source: str, koji_url: str): if not rev: raise KojiScmUrlParseError( 'Failed to parse SCM URL "{}" from Koji build "{}" at "{}" ' - '(missing URL fragment with SCM revision information)'.format(source, nvr, koji_url) + "(missing URL fragment with SCM revision information)".format( + source, nvr, koji_url + ) ) - pkg_name = url.path.split('/')[-1] - pkg_name = re.sub(r'\.git$', '', pkg_name) + pkg_name = url.path.split("/")[-1] + pkg_name = re.sub(r"\.git$", "", pkg_name) return namespace, pkg_name, rev @cached def retrieve_yaml_remote_rule(url: str): - """ Retrieve a remote rule file content from the git web UI. """ - timeout = current_app.config['REMOTE_RULE_GIT_TIMEOUT'] + """Retrieve a remote rule file content from the git web UI.""" + timeout = current_app.config["REMOTE_RULE_GIT_TIMEOUT"] response = requests_session.get(url, timeout=timeout) if response.status_code == 404: - log.debug('Remote rule not found: %s', url) + log.debug("Remote rule not found: %s", url) return None _raise_for_status(response) diff --git a/greenwave/safe_yaml.py b/greenwave/safe_yaml.py index 71f1eb59..023b3f6e 100644 --- a/greenwave/safe_yaml.py +++ b/greenwave/safe_yaml.py @@ -2,13 +2,12 @@ """ Provides a way of defining type-safe YAML parsing. """ -from typing import Dict +import yaml from dateutil import tz from dateutil.parser import parse -import yaml -safe_yaml_tag_to_class: Dict[str, object] = {} +safe_yaml_tag_to_class: dict[str, object] = {} class SafeYAMLError(RuntimeError): @@ -18,7 +17,7 @@ class SafeYAMLError(RuntimeError): """ -class SafeYAMLAttribute(object): +class SafeYAMLAttribute: """ Base class for SafeYAMLObject attributes (in SafeYAMLObject.safe_yaml_attributes dict). """ @@ -44,6 +43,7 @@ class SafeYAMLDict(SafeYAMLAttribute): """ YAML object attribute representing a dict. """ + def __init__(self, **args): super().__init__(**args) @@ -55,7 +55,7 @@ def from_value(self, value): if isinstance(value, dict): return value - raise SafeYAMLError('Expected a dict value, got: {}'.format(value)) + raise SafeYAMLError(f"Expected a dict value, got: {value}") def to_json(self, value): return value @@ -69,6 +69,7 @@ class SafeYAMLBool(SafeYAMLAttribute): """ YAML object attribute representing a boolean value. """ + def __init__(self, default=False, **args): super().__init__(**args) self.default = default @@ -82,7 +83,7 @@ def from_value(self, value): if isinstance(value, bool): return value - raise SafeYAMLError('Expected a boolean value, got: {}'.format(value)) + raise SafeYAMLError(f"Expected a boolean value, got: {value}") def to_json(self, value): return value @@ -96,6 +97,7 @@ class SafeYAMLString(SafeYAMLAttribute): """ YAML object attribute representing a string value. """ + def __init__(self, default=None, **args): super().__init__(**args) self.default = default @@ -119,6 +121,7 @@ class SafeYAMLDateTime(SafeYAMLAttribute): """ YAML object attribute representing a date/time value. """ + def from_yaml(self, loader, node): value = loader.construct_scalar(node) return self.from_value(value) @@ -127,8 +130,7 @@ def from_value(self, value): try: time = parse(str(value)) except ValueError: - raise SafeYAMLError( - 'Could not parse string as date/time, got: {}'.format(value)) + raise SafeYAMLError(f"Could not parse string as date/time, got: {value}") if time.tzinfo is None: time = time.replace(tzinfo=tz.tzutc()) @@ -146,6 +148,7 @@ class SafeYAMLList(SafeYAMLAttribute): """ YAML object attribute represeting a list of values. """ + def __init__(self, item_type, default=None, **kwargs): if default is None: default = [] @@ -164,14 +167,15 @@ def from_value(self, value): results.append(item) continue - item_type = item.get('type') + item_type = item.get("type") if not item_type: raise SafeYAMLError("Key 'type' is required for each list item") cls = safe_yaml_tag_to_class.get(item_type) if cls is None: raise SafeYAMLError( - "Key 'type' for an list item is not valid: {}".format(item_type)) + f"Key 'type' for an list item is not valid: {item_type}" + ) results.append(cls.from_value(item)) @@ -181,7 +185,8 @@ def _from_value(self, values): for value in values: if not isinstance(value, self.item_type): raise SafeYAMLError( - 'Expected list of {} objects'.format(self.item_type.__name__)) + f"Expected list of {self.item_type.__name__} objects" + ) return values @property @@ -203,11 +208,13 @@ class SafeYAMLObjectMetaclass(yaml.YAMLObjectMetaclass): Enabled YAML loader to accept only root objects of specific type. """ + def __init__(cls, name, bases, kwds): super().__init__(name, bases, kwds) - root_yaml_tag = getattr(cls, 'root_yaml_tag', None) + root_yaml_tag = getattr(cls, "root_yaml_tag", None) if root_yaml_tag: + class Loader(cls.yaml_loader): def get_node(self): node = super().get_node() @@ -217,16 +224,17 @@ def get_node(self): if not isinstance(node, yaml.MappingNode): raise SafeYAMLError( - 'Expected mapping for {} tagged object'.format(root_yaml_tag)) + f"Expected mapping for {root_yaml_tag} tagged object" + ) return node Loader.add_constructor(root_yaml_tag, cls.from_yaml) cls.yaml_loader = Loader - yaml_tag = getattr(cls, 'yaml_tag', None) + yaml_tag = getattr(cls, "yaml_tag", None) if yaml_tag: - safe_yaml_tag_to_class[yaml_tag.lstrip('!')] = cls + safe_yaml_tag_to_class[yaml_tag.lstrip("!")] = cls class SafeYAMLObject(yaml.YAMLObject, metaclass=SafeYAMLObjectMetaclass): @@ -241,9 +249,10 @@ class SafeYAMLObject(yaml.YAMLObject, metaclass=SafeYAMLObjectMetaclass): Define class attribute safe_yaml_attributes which is dict mapping attribute name to a SafeYAMLAttribute object. """ + yaml_loader = yaml.SafeLoader - safe_yaml_attributes: Dict[str, SafeYAMLAttribute] + safe_yaml_attributes: dict[str, SafeYAMLAttribute] @classmethod def __new__(cls, *args, **kwargs): @@ -257,33 +266,32 @@ def __new__(cls, *args, **kwargs): @classmethod def from_yaml(cls, loader, node): - nodes = { - name_node.value: value_node - for name_node, value_node in node.value - } + nodes = {name_node.value: value_node for name_node, value_node in node.value} result = cls() for attribute_name, yaml_attribute in cls.safe_yaml_attributes.items(): child_node = nodes.get(attribute_name) if child_node is None: if not yaml_attribute.optional: - msg = '{}: Attribute {!r} is required'.format( - result.safe_yaml_label, attribute_name) + msg = "{}: Attribute {!r} is required".format( + result.safe_yaml_label, attribute_name + ) raise SafeYAMLError(msg) value = yaml_attribute.default_value else: try: value = yaml_attribute.from_yaml(loader, child_node) except (SafeYAMLError, yaml.YAMLError) as e: - msg = '{}: Attribute {!r}: {}'.format( - result.safe_yaml_label, attribute_name, str(e)) + msg = "{}: Attribute {!r}: {}".format( + result.safe_yaml_label, attribute_name, str(e) + ) raise SafeYAMLError(msg) setattr(result, attribute_name, value) try: result.validate() except SafeYAMLError as e: - msg = '{}: {}'.format(result.safe_yaml_label, str(e)) + msg = f"{result.safe_yaml_label}: {str(e)}" raise SafeYAMLError(msg) return result @@ -300,7 +308,7 @@ def safe_load_all(cls, file_or_content): values = yaml.load_all(file_or_content, Loader=cls.yaml_loader) values = list(values) except yaml.YAMLError as e: - raise SafeYAMLError('YAML Parser Error: {}'.format(e)) + raise SafeYAMLError(f"YAML Parser Error: {e}") return values @@ -312,14 +320,14 @@ def from_value(cls, data): value = data.get(attribute_name) if value is None: if not yaml_attribute.optional: - msg = 'Attribute {!r} is required'.format(attribute_name) + msg = f"Attribute {attribute_name!r} is required" raise SafeYAMLError(msg) value = yaml_attribute.default_value else: try: value = yaml_attribute.from_value(value) except SafeYAMLError as e: - msg = 'Attribute {!r}: {}'.format(attribute_name, str(e)) + msg = f"Attribute {attribute_name!r}: {str(e)}" raise SafeYAMLError(msg) setattr(result, attribute_name, value) @@ -328,14 +336,13 @@ def from_value(cls, data): @property def safe_yaml_label(self): - return 'YAML object {}'.format(self.yaml_tag) + return f"YAML object {self.yaml_tag}" def validate(self): pass def to_json(self): return { - attribute_name: yaml_attribute.to_json( - getattr(self, attribute_name, None)) + attribute_name: yaml_attribute.to_json(getattr(self, attribute_name, None)) for attribute_name, yaml_attribute in self.safe_yaml_attributes.items() } diff --git a/greenwave/subjects/__init__.py b/greenwave/subjects/__init__.py index 1fc1386e..a0369f0f 100644 --- a/greenwave/subjects/__init__.py +++ b/greenwave/subjects/__init__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """Contains Greenwave's subject type handling.""" diff --git a/greenwave/subjects/factory.py b/greenwave/subjects/factory.py index c39cf864..c8269195 100644 --- a/greenwave/subjects/factory.py +++ b/greenwave/subjects/factory.py @@ -14,7 +14,7 @@ class UnknownSubjectDataError(RuntimeError): def subject_types(): - return current_app.config['subject_types'] + return current_app.config["subject_types"] def create_subject_from_data(data): diff --git a/greenwave/subjects/subject.py b/greenwave/subjects/subject.py index c216588b..e1d20892 100644 --- a/greenwave/subjects/subject.py +++ b/greenwave/subjects/subject.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: GPL-2.0+ import re -from typing import Union from greenwave.subjects.subject_type import GenericSubjectType, SubjectType @@ -9,11 +8,11 @@ def _to_dict(format_dict, item): result = {} - item_key = format_dict.get('item_key') + item_key = format_dict.get("item_key") if item_key: result[item_key] = item - keys = format_dict.get('keys', {}) + keys = format_dict.get("keys", {}) for key, value in keys.items(): result[key] = value @@ -28,16 +27,20 @@ class Subject: Item or identifier should uniquely identify the artefact (test subject). """ - subject_type: Union[GenericSubjectType, SubjectType] + + subject_type: GenericSubjectType | SubjectType item: str - def __init__(self, type_: Union[GenericSubjectType, SubjectType], item: str): + def __init__(self, type_: GenericSubjectType | SubjectType, item: str): self.subject_type = type_ self.item = item def product_versions_from_koji_build_target(self, target): - return sorted(self._matching_product_versions( - target, self.subject_type.product_version_from_koji_build_target)) + return sorted( + self._matching_product_versions( + target, self.subject_type.product_version_from_koji_build_target + ) + ) @property def type(self): @@ -72,11 +75,14 @@ def short_product_version(self): @property def product_versions(self): - pvs = sorted({ - pv.lower() - for pv in self._matching_product_versions( - self.item, self.subject_type.product_version_match) - }) + pvs = sorted( + { + pv.lower() + for pv in self._matching_product_versions( + self.item, self.subject_type.product_version_match + ) + } + ) if pvs: return pvs @@ -114,17 +120,13 @@ def result_queries(self): def _matching_product_versions(self, text, match_dict): return { - re.sub(pv_match['match'], pv_match['product_version'], text) + re.sub(pv_match["match"], pv_match["product_version"], text) for pv_match in match_dict - if re.match(pv_match['match'], text) + if re.match(pv_match["match"], text) } def __str__(self): - return "subject_type {!r}, subject_identifier {!r}".format( - self.type, self.item - ) + return f"subject_type {self.type!r}, subject_identifier {self.item!r}" def __repr__(self): - return "Subject({!r}, {!r})".format( - self.subject_type, self.item - ) + return f"Subject({self.subject_type!r}, {self.item!r})" diff --git a/greenwave/subjects/subject_type.py b/greenwave/subjects/subject_type.py index 198bd633..3b549828 100644 --- a/greenwave/subjects/subject_type.py +++ b/greenwave/subjects/subject_type.py @@ -16,47 +16,39 @@ class SubjectType(SafeYAMLObject): - root_yaml_tag = '!SubjectType' + root_yaml_tag = "!SubjectType" safe_yaml_attributes = { - 'id': SafeYAMLString(), - - 'aliases': SafeYAMLList(item_type=str, optional=True), - + "id": SafeYAMLString(), + "aliases": SafeYAMLList(item_type=str, optional=True), # Key name to load from decision 'subject' list or ResultsDB result # data ("item" will be used if the key is empty or not found). - 'item_key': SafeYAMLString(optional=True), - + "item_key": SafeYAMLString(optional=True), # A build for subject identifier can be found on Koji/Brew. - 'is_koji_build': SafeYAMLBool(optional=True, default=False), - + "is_koji_build": SafeYAMLBool(optional=True, default=False), # Is identifier in NVR format? If true, package name and short product # version can be parsed for identifier. - 'is_nvr': SafeYAMLBool(optional=True), - + "is_nvr": SafeYAMLBool(optional=True), # Omit responding with HTTP 404 if there is no applicable policy. - 'ignore_missing_policy': SafeYAMLBool(optional=True, default=False), - + "ignore_missing_policy": SafeYAMLBool(optional=True, default=False), # List of dicts. Each dict must have: # - 'match' field containing regular expression to match subject ID # - 'product_version' field containing product version (can contain # '\1', '\2' etc, expanded to matched groups) - 'product_version_match': SafeYAMLList(item_type=dict, optional=True), - + "product_version_match": SafeYAMLList(item_type=dict, optional=True), # Same as product_version_match, but to match build target from Koji # instead of subject ID. - 'product_version_from_koji_build_target': SafeYAMLList(item_type=dict, optional=True), - + "product_version_from_koji_build_target": SafeYAMLList( + item_type=dict, optional=True + ), # Fixed product version for the subject type used if # product_version_match is undefined or does not match subject ID. - 'product_version': SafeYAMLString(optional=True), - + "product_version": SafeYAMLString(optional=True), # Serialization dict for decision. - 'item_dict': SafeYAMLDict(optional=True), - + "item_dict": SafeYAMLDict(optional=True), # List of serialization dicts for ResultsDB requests. # If not defined, defaults to single request with item_dict value. - 'result_queries': SafeYAMLList(item_type=dict, optional=True), + "result_queries": SafeYAMLList(item_type=dict, optional=True), } def matches(self, id_): @@ -64,10 +56,10 @@ def matches(self, id_): @property def safe_yaml_label(self): - return 'SubjectType {!r}'.format(self.id) + return f"SubjectType {self.id!r}" def __repr__(self): - return ''.format(self.id) + return f"" class GenericSubjectType: @@ -85,7 +77,7 @@ def matches(self, id_): return id_ == self.id or id_ in self.aliases def __repr__(self): - return ''.format(self.id) + return f"" def load_subject_types(subject_types_dir): @@ -95,10 +87,10 @@ def load_subject_types(subject_types_dir): :param str subject_types_dir: A path points to the policies directory. :return: A list of subject_types. """ - paths = glob.glob(os.path.join(subject_types_dir, '*.yaml')) + paths = glob.glob(os.path.join(subject_types_dir, "*.yaml")) subject_types = [] for path in paths: - with open(path, 'r') as f: + with open(path) as f: subject_types.extend(SubjectType.safe_load_all(f)) return subject_types diff --git a/greenwave/tests/__init__.py b/greenwave/tests/__init__.py index 5f1248d9..9b7a58b6 100644 --- a/greenwave/tests/__init__.py +++ b/greenwave/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """ Greenwave uses the Python `unittest`_ framework for unit tests. diff --git a/greenwave/tests/conftest.py b/greenwave/tests/conftest.py index e3e7cb45..2ff34947 100644 --- a/greenwave/tests/conftest.py +++ b/greenwave/tests/conftest.py @@ -1,21 +1,35 @@ # SPDX-License-Identifier: GPL-2.0+ -import pytest +from unittest.mock import Mock, patch + +from pytest import fixture from greenwave.app_factory import create_app -@pytest.fixture(autouse=True) +@fixture(autouse=True) def mock_env_config(monkeypatch): - monkeypatch.delenv('GREENWAVE_CONFIG', raising=False) + monkeypatch.delenv("GREENWAVE_CONFIG", raising=False) + + +@fixture(autouse=True) +def set_environment_variable(monkeypatch): + monkeypatch.setenv("TEST", "true") -@pytest.fixture +@fixture def app(): - app = create_app(config_obj='greenwave.config.TestingConfig') + app = create_app(config_obj="greenwave.config.TestingConfig") with app.app_context(): yield app -@pytest.fixture +@fixture def client(app): yield app.test_client() + + +@fixture +def koji_proxy(): + mock_proxy = Mock() + with patch("greenwave.resources.get_server_proxy", return_value=mock_proxy): + yield mock_proxy diff --git a/greenwave/tests/settings_override.py b/greenwave/tests/settings_override.py index 02a56c85..0589e6a1 100644 --- a/greenwave/tests/settings_override.py +++ b/greenwave/tests/settings_override.py @@ -1,3 +1,3 @@ # Settings file for tests. -POLICIES_DIR = '/src/conf/policies' -SUBJECT_TYPES_DIR = '/src/conf/subject_types' +POLICIES_DIR = "/src/conf/policies" +SUBJECT_TYPES_DIR = "/src/conf/subject_types" diff --git a/greenwave/tests/test_api_v1.py b/greenwave/tests/test_api_v1.py index 2a115f31..827923de 100644 --- a/greenwave/tests/test_api_v1.py +++ b/greenwave/tests/test_api_v1.py @@ -1,18 +1,18 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock -import pytest - from textwrap import dedent +from unittest import mock + +import pytest from greenwave.app_factory import create_app from greenwave.policies import Policy DEFAULT_DECISION_DATA = dict( - decision_context='test_policies', - product_version='fedora-rawhide', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.f31', + decision_context="test_policies", + product_version="fedora-rawhide", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.f31", ) DEFAULT_DECISION_POLICIES = """ @@ -29,92 +29,105 @@ def make_result(outcome): return { - 'id': 123, - 'data': { - 'item': [DEFAULT_DECISION_DATA['subject_identifier']], - 'type': [DEFAULT_DECISION_DATA['subject_type']], + "id": 123, + "data": { + "item": [DEFAULT_DECISION_DATA["subject_identifier"]], + "type": [DEFAULT_DECISION_DATA["subject_type"]], }, - 'testcase': {'name': 'sometest'}, - 'outcome': outcome, + "testcase": {"name": "sometest"}, + "outcome": outcome, } @pytest.fixture def mock_results(): - with mock.patch('greenwave.resources.ResultsRetriever.retrieve') as mocked: + with mock.patch("greenwave.resources.ResultsRetriever.retrieve") as mocked: mocked.return_value = [] yield mocked @pytest.fixture def mock_waivers(): - with mock.patch('greenwave.resources.WaiversRetriever.retrieve') as mocked: + with mock.patch("greenwave.resources.WaiversRetriever.retrieve") as mocked: mocked.return_value = [] yield mocked @pytest.fixture def make_decision(): - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") def make_decision(policies=DEFAULT_DECISION_POLICIES, **kwargs): - app.config['policies'] = Policy.safe_load_all(dedent(policies)) + app.config["policies"] = Policy.safe_load_all(dedent(policies)) client = app.test_client() data = DEFAULT_DECISION_DATA.copy() data.update(kwargs) - return client.post('/api/v1.0/decision', json=data) + return client.post("/api/v1.0/decision", json=data) yield make_decision -def test_make_decision_retrieves_waivers_on_missing(mock_results, mock_waivers, make_decision): +def test_make_decision_retrieves_waivers_on_missing( + mock_results, mock_waivers, make_decision +): mock_results.return_value = [] mock_waivers.return_value = [] response = make_decision() assert 200 == response.status_code - assert 'Of 1 required test, 1 result missing' == response.json['summary'] + assert "Of 1 required test, 1 result missing" == response.json["summary"] mock_waivers.assert_called_once() -def test_make_decision_retrieves_waivers_on_failed(mock_results, mock_waivers, make_decision): - mock_results.return_value = [make_result(outcome='FAILED')] +def test_make_decision_retrieves_waivers_on_failed( + mock_results, mock_waivers, make_decision +): + mock_results.return_value = [make_result(outcome="FAILED")] mock_waivers.return_value = [] response = make_decision() assert 200 == response.status_code - assert 'Of 1 required test, 1 test failed' == response.json['summary'] + assert "Of 1 required test, 1 test failed" == response.json["summary"] mock_waivers.assert_called_once() def test_make_decision_retrieves_waivers_omitted_on_passed( - mock_results, mock_waivers, make_decision): - mock_results.return_value = [make_result(outcome='PASSED')] + mock_results, mock_waivers, make_decision +): + mock_results.return_value = [make_result(outcome="PASSED")] mock_waivers.return_value = [] response = make_decision() assert 200 == response.status_code - assert 'All required tests (1 total) have passed or been waived' == response.json['summary'] + assert ( + "All required tests (1 total) have passed or been waived" + == response.json["summary"] + ) mock_waivers.assert_not_called() -def test_make_decision_retrieves_waivers_on_errored(mock_results, mock_waivers, make_decision): - mock_results.return_value = [make_result(outcome='ERROR')] +def test_make_decision_retrieves_waivers_on_errored( + mock_results, mock_waivers, make_decision +): + mock_results.return_value = [make_result(outcome="ERROR")] mock_waivers.return_value = [] response = make_decision() assert 200 == response.status_code - assert 'Of 1 required test, 1 test errored' == response.json['summary'] + assert "Of 1 required test, 1 test errored" == response.json["summary"] mock_waivers.assert_called_once() def test_make_decision_retrieves_waivers_once_on_verbose_and_missing( - mock_results, mock_waivers, make_decision): + mock_results, mock_waivers, make_decision +): mock_results.return_value = [] mock_waivers.return_value = [] response = make_decision(verbose=True) assert 200 == response.status_code - assert 'Of 1 required test, 1 result missing' == response.json['summary'] + assert "Of 1 required test, 1 result missing" == response.json["summary"] mock_waivers.assert_called_once() -def test_make_decision_with_no_tests_required(mock_results, mock_waivers, make_decision): +def test_make_decision_with_no_tests_required( + mock_results, mock_waivers, make_decision +): mock_results.return_value = [] mock_waivers.return_value = [] policies = """ @@ -128,12 +141,13 @@ def test_make_decision_with_no_tests_required(mock_results, mock_waivers, make_d """ response = make_decision(policies=policies) assert 200 == response.status_code - assert 'No tests are required' == response.json['summary'] + assert "No tests are required" == response.json["summary"] mock_waivers.assert_not_called() def test_make_decision_with_no_tests_required_and_missing_gating_yaml( - mock_results, mock_waivers, make_decision): + mock_results, mock_waivers, make_decision +): mock_results.return_value = [] mock_waivers.return_value = [] policies = """ @@ -146,13 +160,17 @@ def test_make_decision_with_no_tests_required_and_missing_gating_yaml( rules: - !RemoteRule {} """ - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = None response = make_decision(policies=policies) assert 200 == response.status_code - assert 'No tests are required' == response.json['summary'] + assert "No tests are required" == response.json["summary"] mock_waivers.assert_not_called() @@ -173,10 +191,11 @@ def test_make_decision_with_no_tests_required_and_missing_gating_yaml( - bar rules: [ ] """), - ) + ), ) def test_make_decision_with_no_tests_required_and_empty_remote_rules( - mock_results, mock_waivers, make_decision, remote_gating_yaml): + mock_results, mock_waivers, make_decision, remote_gating_yaml +): mock_results.return_value = [] mock_waivers.return_value = [] policies = """ @@ -192,17 +211,23 @@ def test_make_decision_with_no_tests_required_and_empty_remote_rules( - !RemoteRule {} """ - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_gating_yaml response = make_decision(policies=policies) assert 200 == response.status_code - assert 'No tests are required' == response.json['summary'] + assert "No tests are required" == response.json["summary"] mock_waivers.assert_not_called() -def test_make_decision_no_applicable_policies(mock_results, mock_waivers, make_decision): +def test_make_decision_no_applicable_policies( + mock_results, mock_waivers, make_decision +): mock_results.return_value = [] mock_waivers.return_value = [] policies = """ @@ -218,14 +243,16 @@ def test_make_decision_no_applicable_policies(mock_results, mock_waivers, make_d """ response = make_decision(policies=policies) assert 404 == response.status_code - assert response.json['message'] == ( - 'Found no applicable policies for koji_build subjects at gating ' - 'point(s) test_policies in fedora-rawhide' + assert response.json["message"] == ( + "Found no applicable policies for koji_build subjects at gating " + "point(s) test_policies in fedora-rawhide" ) mock_waivers.assert_not_called() -def test_make_decision_with_missing_required_gating_yaml(mock_results, mock_waivers, make_decision): +def test_make_decision_with_missing_required_gating_yaml( + mock_results, mock_waivers, make_decision +): mock_results.return_value = [] mock_waivers.return_value = [] policies = """ @@ -238,20 +265,24 @@ def test_make_decision_with_missing_required_gating_yaml(mock_results, mock_waiv rules: - !RemoteRule {required: true} """ - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = None response = make_decision(policies=policies) assert 200 == response.status_code - assert not response.json['policies_satisfied'] - exp = '1 error due to missing remote rule file' - assert exp == response.json['summary'] + assert not response.json["policies_satisfied"] + exp = "1 error due to missing remote rule file" + assert exp == response.json["summary"] mock_waivers.assert_called_once() def test_make_decision_multiple_contexts(mock_results, mock_waivers, make_decision): - mock_results.return_value = [make_result(outcome='FAILED')] + mock_results.return_value = [make_result(outcome="FAILED")] mock_waivers.return_value = [] policies = """ --- !Policy @@ -281,22 +312,24 @@ def test_make_decision_multiple_contexts(mock_results, mock_waivers, make_decisi rules: - !PassingTestCaseRule {test_case_name: sometest_3} """ - response = make_decision(policies=policies, decision_context=["test_policies", "test_2"]) + response = make_decision( + policies=policies, decision_context=["test_policies", "test_2"] + ) assert 200 == response.status_code - assert 'Of 2 required tests, 2 tests failed' == response.json['summary'] - assert ['test_policy', 'test_policy_2'] == response.json['applicable_policies'] + assert "Of 2 required tests, 2 tests failed" == response.json["summary"] + assert ["test_policy", "test_policy_2"] == response.json["applicable_policies"] mock_waivers.assert_called_once() def test_subject_types(client): - response = client.get('/api/v1.0/subject_types') + response = client.get("/api/v1.0/subject_types") assert response.status_code == 200 data = response.json - assert len(data['subject_types']) - assert [x['id'] for x in data['subject_types']] == [ - 'bodhi_update', - 'compose', - 'koji_build', - 'redhat-container-image', - 'redhat-module', + assert len(data["subject_types"]) + assert [x["id"] for x in data["subject_types"]] == [ + "bodhi_update", + "compose", + "koji_build", + "redhat-container-image", + "redhat-module", ] diff --git a/greenwave/tests/test_app_factory.py b/greenwave/tests/test_app_factory.py index 64bc65e1..dbd071df 100644 --- a/greenwave/tests/test_app_factory.py +++ b/greenwave/tests/test_app_factory.py @@ -1,21 +1,22 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock - from textwrap import dedent +from unittest import mock + from greenwave.app_factory import create_app -from greenwave.policies import Policy from greenwave.config import TestingConfig +from greenwave.policies import Policy -@mock.patch('greenwave.policies.load_policies') +@mock.patch("greenwave.policies.load_policies") def test_remote_rules_base_url(mock_load_policies): """ The application shouldn't start if RemoteRule is in policy configuration but if cannot be used because dist-git or koji URL is not configured. """ - policies = Policy.safe_load_all(dedent(""" + policies = Policy.safe_load_all( + dedent(""" --- !Policy id: test_policy product_versions: [fedora-rawhide] @@ -23,16 +24,19 @@ def test_remote_rules_base_url(mock_load_policies): subject_type: koji_build rules: - !RemoteRule {} - """)) + """) + ) mock_load_policies.return_value = policies config = TestingConfig() - config.DIST_GIT_BASE_URL = 'http://localhost.localdomain/' - config.DIST_GIT_URL_TEMPLATE = '{DIST_GIT_BASE_URL}{other_params}/blablabla/gating.yaml' + config.DIST_GIT_BASE_URL = "http://localhost.localdomain/" + config.DIST_GIT_URL_TEMPLATE = ( + "{DIST_GIT_BASE_URL}{other_params}/blablabla/gating.yaml" + ) config.REMOTE_RULE_POLICIES = {} app = create_app(config) - assert app.config['DIST_GIT_URL_TEMPLATE'] == ( - 'http://localhost.localdomain/{other_params}/blablabla/gating.yaml' + assert app.config["DIST_GIT_URL_TEMPLATE"] == ( + "http://localhost.localdomain/{other_params}/blablabla/gating.yaml" ) diff --git a/greenwave/tests/test_listeners.py b/greenwave/tests/test_listeners.py index 73e71f94..a83eaa09 100644 --- a/greenwave/tests/test_listeners.py +++ b/greenwave/tests/test_listeners.py @@ -1,15 +1,15 @@ # SPDX-License-Identifier: GPL-2.0+ import json from textwrap import dedent +from unittest import mock -import mock import pytest import stomp from requests.exceptions import HTTPError from greenwave.app_factory import create_app -from greenwave.listeners.waiverdb import WaiverDBListener from greenwave.listeners.resultsdb import ResultsDBListener +from greenwave.listeners.waiverdb import WaiverDBListener from greenwave.monitor import ( messaging_rx_counter, messaging_rx_ignored_counter, @@ -65,7 +65,7 @@ def __init__( nvr=DUMMY_NVR, type_="koji_build", testcase="dist.rpmdeplint", - **kwargs + **kwargs, ): if "message" in kwargs: self.message = kwargs["message"] @@ -368,7 +368,8 @@ def retrieve_decision(data, _config): expected_body = {"msg": expected_message, "topic": DECISION_UPDATE_TOPIC} assert json.loads(mock_call["body"]) == expected_body expected_headers = { - k: expected_message[k] for k in ( + k: expected_message[k] + for k in ( "subject_type", "subject_identifier", "product_version", @@ -376,7 +377,9 @@ def retrieve_decision(data, _config): "summary", ) } - expected_headers["policies_satisfied"] = str(expected_message["policies_satisfied"]).lower() + expected_headers["policies_satisfied"] = str( + expected_message["policies_satisfied"] + ).lower() assert mock_call["headers"] == expected_headers @@ -552,6 +555,7 @@ def test_remote_rule_decision_change_not_matching( Test not publishing decision change message for test cases not mentioned in gating.yaml. """ + def retrieve_decision(data, _config): return { "policies_satisfied": True, @@ -611,7 +615,9 @@ def test_guess_product_version_with_koji(koji_proxy, app): ] subject = create_subject("container-build", "fake_koji_build") - product_versions = subject_product_versions(subject, "http://localhost:5006/kojihub") + product_versions = subject_product_versions( + subject, "http://localhost:5006/kojihub" + ) koji_proxy.getBuild.assert_called_once_with("fake_koji_build") koji_proxy.getTaskRequest.assert_called_once_with(666) @@ -622,7 +628,9 @@ def test_guess_product_version_with_koji_without_task_id(koji_proxy, app): koji_proxy.getBuild.return_value = {"task_id": None} subject = create_subject("container-build", "fake_koji_build") - product_versions = subject_product_versions(subject, "http://localhost:5006/kojihub") + product_versions = subject_product_versions( + subject, "http://localhost:5006/kojihub" + ) koji_proxy.getBuild.assert_called_once_with("fake_koji_build") koji_proxy.getTaskRequest.assert_not_called() @@ -644,7 +652,9 @@ def test_guess_product_version_with_koji_and_unexpected_task_type( koji_proxy.getTaskRequest.return_value = task_request subject = create_subject("container-build", "fake_koji_build") - product_versions = subject_product_versions(subject, "http://localhost:5006/kojihub") + product_versions = subject_product_versions( + subject, "http://localhost:5006/kojihub" + ) koji_proxy.getBuild.assert_called_once_with("fake_koji_build") koji_proxy.getTaskRequest.assert_called_once_with(666) diff --git a/greenwave/tests/test_monitor.py b/greenwave/tests/test_monitor.py index c65adc6f..1b8ede2b 100644 --- a/greenwave/tests/test_monitor.py +++ b/greenwave/tests/test_monitor.py @@ -1,74 +1,75 @@ # SPDX-License-Identifier: GPL-2.0+ -from mock import Mock, patch +from unittest.mock import Mock, patch + from pytest import raises from greenwave.monitor import Counter, Histogram, stats_client def test_counter_to_str(): - assert str(Counter('total_decisions')) == 'total_decisions' + assert str(Counter("total_decisions")) == "total_decisions" def test_counter_to_str_with_labels(): - counter = Counter('total_decisions').labels(handler='test') - assert str(counter) == 'total_decisions[handler=test]' + counter = Counter("total_decisions").labels(handler="test") + assert str(counter) == "total_decisions[handler=test]" - counter2 = counter.labels(decision_context='context') - assert str(counter2) == 'total_decisions[handler=test,decision_context=context]' + counter2 = counter.labels(decision_context="context") + assert str(counter2) == "total_decisions[handler=test,decision_context=context]" def test_counter_no_host_set(monkeypatch): - with patch('greenwave.monitor.StatsClient') as client: - monkeypatch.delenv('GREENWAVE_STATSD_HOST', raising=False) + with patch("greenwave.monitor.StatsClient") as client: + monkeypatch.delenv("GREENWAVE_STATSD_HOST", raising=False) stats_client.cache_clear() - Counter('total_decisions').inc() + Counter("total_decisions").inc() client.assert_not_called() def test_counter_empty_host_set(monkeypatch): - with patch('greenwave.monitor.StatsClient') as client: - monkeypatch.setenv('GREENWAVE_STATSD_HOST', '') + with patch("greenwave.monitor.StatsClient") as client: + monkeypatch.setenv("GREENWAVE_STATSD_HOST", "") stats_client.cache_clear() - Counter('total_decisions').inc() + Counter("total_decisions").inc() client.assert_not_called() def test_counter_inc(monkeypatch): - with patch('greenwave.monitor.StatsClient') as client: - monkeypatch.setenv('GREENWAVE_STATSD_HOST', 'localhost:99999') + with patch("greenwave.monitor.StatsClient") as client: + monkeypatch.setenv("GREENWAVE_STATSD_HOST", "localhost:99999") stats_client.cache_clear() - Counter('total_decisions').inc() + Counter("total_decisions").inc() client.assert_called_once() - stats_client().incr.assert_called_once_with('total_decisions') + stats_client().incr.assert_called_once_with("total_decisions") def test_counter_count_exceptions(monkeypatch): - with patch('greenwave.monitor.StatsClient') as client: - monkeypatch.setenv('GREENWAVE_STATSD_HOST', 'localhost:99999') + with patch("greenwave.monitor.StatsClient") as client: + monkeypatch.setenv("GREENWAVE_STATSD_HOST", "localhost:99999") stats_client.cache_clear() - tested_function = Mock(side_effect=RuntimeError('some exception')) - decorator = Counter('decision_exceptions').count_exceptions() + tested_function = Mock(side_effect=RuntimeError("some exception")) + decorator = Counter("decision_exceptions").count_exceptions() wrapper = decorator(tested_function) - with raises(RuntimeError, match='some exception'): + with raises(RuntimeError, match="some exception"): wrapper(1, a=2, b=3) tested_function.assert_called_once_with(1, a=2, b=3) client.assert_called_once() - stats_client().incr.assert_called_once_with('decision_exceptions') + stats_client().incr.assert_called_once_with("decision_exceptions") def test_histogram_time(monkeypatch): - with patch('greenwave.monitor.StatsClient') as client: - monkeypatch.setenv('GREENWAVE_STATSD_HOST', 'localhost:99999') + with patch("greenwave.monitor.StatsClient") as client: + monkeypatch.setenv("GREENWAVE_STATSD_HOST", "localhost:99999") stats_client.cache_clear() timed_function = Mock() - decorator = Histogram('decision_duration').time() + decorator = Histogram("decision_duration").time() wrapper = decorator(timed_function) wrapper(1, a=2, b=3) timed_function.assert_called_once_with(1, a=2, b=3) client.assert_called_once() - stats_client().timer.assert_called_once_with('decision_duration') + stats_client().timer.assert_called_once_with("decision_duration") diff --git a/greenwave/tests/test_policies.py b/greenwave/tests/test_policies.py index 974af347..419eb3f4 100644 --- a/greenwave/tests/test_policies.py +++ b/greenwave/tests/test_policies.py @@ -1,51 +1,47 @@ - # SPDX-License-Identifier: GPL-2.0+ -import pytest -import mock import time - from textwrap import dedent +from unittest import mock + +import pytest from greenwave.app_factory import create_app +from greenwave.config import Config, TestingConfig from greenwave.decision import Decision from greenwave.policies import ( - applicable_decision_context_product_version_pairs, - load_policies, - summarize_answers, + OnDemandPolicy, Policy, RemotePolicy, RemoteRule, RuleSatisfied, - TestResultMissing, TestResultFailed, - OnDemandPolicy + TestResultMissing, + applicable_decision_context_product_version_pairs, + load_policies, + summarize_answers, ) -from greenwave.resources import ResultsRetriever, KojiScmUrlParseError +from greenwave.resources import KojiScmUrlParseError, ResultsRetriever from greenwave.safe_yaml import SafeYAMLError from greenwave.subjects.factory import create_subject -from greenwave.waivers import waive_answers -from greenwave.config import TestingConfig, Config from greenwave.utils import add_to_timestamp +from greenwave.waivers import waive_answers @pytest.fixture(autouse=True) def app(): - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): yield def answer_types(answers): - return [x.to_json()['type'] for x in answers] + return [x.to_json()["type"] for x in answers] class DummyResultsRetriever(ResultsRetriever): - def __init__(self, subject=None, testcase=None, outcome='PASSED', when=''): - super(DummyResultsRetriever, self).__init__( - ignore_ids=[], - when=when, - url='') + def __init__(self, subject=None, testcase=None, outcome="PASSED", when=""): + super().__init__(ignore_ids=[], when=when, url="") self.subject = subject self.testcase = testcase self.outcome = outcome @@ -54,20 +50,30 @@ def __init__(self, subject=None, testcase=None, outcome='PASSED', when=''): def _retrieve_data(self, params): self.retrieve_data_called += 1 - if (self.subject and (params.get('item') == self.subject.identifier or - params.get('nvr') == self.subject.identifier) and - ('type' not in params or self.subject.type in params['type'].split(',')) and - (params.get('testcases') is None or params.get('testcases') == self.testcase)): - return [{ - 'id': 123, - 'data': { - 'item': [self.subject.identifier], - 'type': [self.subject.type], - }, - 'testcase': {'name': self.testcase}, - 'outcome': self.outcome, - 'submit_time': '2021-03-25T07:26:56.191741', - }] + if ( + self.subject + and ( + params.get("item") == self.subject.identifier + or params.get("nvr") == self.subject.identifier + ) + and ("type" not in params or self.subject.type in params["type"].split(",")) + and ( + params.get("testcases") is None + or params.get("testcases") == self.testcase + ) + ): + return [ + { + "id": 123, + "data": { + "item": [self.subject.identifier], + "type": [self.subject.type], + }, + "testcase": {"name": self.testcase}, + "outcome": self.outcome, + "submit_time": "2021-03-25T07:26:56.191741", + } + ] return [] def get_external_cache(self, key): @@ -78,28 +84,40 @@ def set_external_cache(self, key, value): def test_summarize_answers(): - testSubject = create_subject('koji_build', 'nvr') + testSubject = create_subject("koji_build", "nvr") testResultPassed = RuleSatisfied() - testResultFailed = TestResultFailed(testSubject, 'test', None, 1, {}) - testResultMissing = TestResultMissing(testSubject, 'test', None, None) - - assert summarize_answers([testResultPassed]) == \ - 'All required tests (1 total) have passed or been waived' - assert summarize_answers([testResultFailed, testResultPassed]) == \ - 'Of 2 required tests, 1 test failed' - assert summarize_answers([testResultMissing]) == \ - 'Of 1 required test, 1 result missing' - assert summarize_answers([testResultFailed, testResultMissing]) == \ - 'Of 2 required tests, 1 result missing, 1 test failed' - assert summarize_answers([testResultFailed, testResultMissing, testResultMissing]) == \ - 'Of 3 required tests, 2 results missing, 1 test failed' - assert summarize_answers([testResultMissing, testResultPassed]) == \ - 'Of 2 required tests, 1 result missing' + testResultFailed = TestResultFailed(testSubject, "test", None, 1, {}) + testResultMissing = TestResultMissing(testSubject, "test", None, None) + + assert ( + summarize_answers([testResultPassed]) + == "All required tests (1 total) have passed or been waived" + ) + assert ( + summarize_answers([testResultFailed, testResultPassed]) + == "Of 2 required tests, 1 test failed" + ) + assert ( + summarize_answers([testResultMissing]) == "Of 1 required test, 1 result missing" + ) + assert ( + summarize_answers([testResultFailed, testResultMissing]) + == "Of 2 required tests, 1 result missing, 1 test failed" + ) + assert ( + summarize_answers([testResultFailed, testResultMissing, testResultMissing]) + == "Of 3 required tests, 2 results missing, 1 test failed" + ) + assert ( + summarize_answers([testResultMissing, testResultPassed]) + == "Of 2 required tests, 1 result missing" + ) def test_decision_with_missing_result(tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "rawhide_compose_sync_to_mirrors" product_versions: @@ -108,27 +126,29 @@ def test_decision_with_missing_result(tmpdir): subject_type: compose rules: - !PassingTestCaseRule {test_case_name: sometest} - """)) + """) + ) policies = load_policies(tmpdir.strpath) - subject = create_subject('compose', 'some_nevr') + subject = create_subject("compose", "some_nevr") results = DummyResultsRetriever() - decision = Decision('rawhide_compose_sync_to_mirrors', 'fedora-rawhide') + decision = Decision("rawhide_compose_sync_to_mirrors", "fedora-rawhide") # Ensure that absence of a result is failure. decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['test-result-missing'] + assert answer_types(decision.answers) == ["test-result-missing"] def test_waive_brew_koji_mismatch(tmpdir): - """ Ensure that a koji_build waiver can match a brew-build result + """Ensure that a koji_build waiver can match a brew-build result Note that 'brew-build' in the result does not match 'koji_build' in the waiver. Even though these are different strings, this should work. """ - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: some_id product_versions: @@ -137,41 +157,45 @@ def test_waive_brew_koji_mismatch(tmpdir): subject_type: koji_build rules: - !PassingTestCaseRule {test_case_name: sometest} - """)) + """) + ) policies = load_policies(tmpdir.strpath) - subject = create_subject('koji_build', 'some_nevr') - results = DummyResultsRetriever(subject, 'sometest', 'FAILED') + subject = create_subject("koji_build", "some_nevr") + results = DummyResultsRetriever(subject, "sometest", "FAILED") # Ensure that absence of a result is failure. - decision = Decision('test', 'fedora-rawhide') + decision = Decision("test", "fedora-rawhide") decision.check(subject, policies, results) answers = waive_answers(decision.answers, []) - assert answer_types(answers) == ['test-result-failed'] - - waivers = [{ - 'id': 1, - 'subject_identifier': subject.identifier, - 'subject_type': subject.type, - 'testcase': 'sometest', - 'product_version': 'fedora-rawhide', - 'waived': True, - }] - decision = Decision('test', 'fedora-rawhide') + assert answer_types(answers) == ["test-result-failed"] + + waivers = [ + { + "id": 1, + "subject_identifier": subject.identifier, + "subject_type": subject.type, + "testcase": "sometest", + "product_version": "fedora-rawhide", + "waived": True, + } + ] + decision = Decision("test", "fedora-rawhide") decision.check(subject, policies, results) answers = waive_answers(decision.answers, waivers) - assert answer_types(answers) == ['test-result-failed-waived'] + assert answer_types(answers) == ["test-result-failed-waived"] def test_waive_bodhi_update(tmpdir): - """ Ensure that a koji_build waiver can match a brew-build result + """Ensure that a koji_build waiver can match a brew-build result Note that 'brew-build' in the result does not match 'koji_build' in the waiver. Even though these are different strings, this should work. """ - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: some_id product_versions: @@ -180,46 +204,59 @@ def test_waive_bodhi_update(tmpdir): subject_type: bodhi_update rules: - !PassingTestCaseRule {test_case_name: sometest} - """)) + """) + ) policies = load_policies(tmpdir.strpath) - subject = create_subject('bodhi_update', 'some_bodhi_update') - results = DummyResultsRetriever(subject, 'sometest', 'FAILED') + subject = create_subject("bodhi_update", "some_bodhi_update") + results = DummyResultsRetriever(subject, "sometest", "FAILED") - decision = Decision('test', 'fedora-rawhide') + decision = Decision("test", "fedora-rawhide") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['test-result-failed'] - - waivers = [{ - 'id': 1, - 'subject_identifier': subject.identifier, - 'subject_type': subject.type, - 'testcase': 'sometest', - 'product_version': 'fedora-rawhide', - 'waived': True, - }] - decision = Decision('test', 'fedora-rawhide') + assert answer_types(decision.answers) == ["test-result-failed"] + + waivers = [ + { + "id": 1, + "subject_identifier": subject.identifier, + "subject_type": subject.type, + "testcase": "sometest", + "product_version": "fedora-rawhide", + "waived": True, + } + ] + decision = Decision("test", "fedora-rawhide") decision.check(subject, policies, results) answers = waive_answers(decision.answers, waivers) - assert answer_types(answers) == ['test-result-failed-waived'] + assert answer_types(answers) == ["test-result-failed-waived"] def test_load_policies(): - app = create_app('greenwave.config.TestingConfig') - assert len(app.config['policies']) > 0 - assert any(policy.id == 'taskotron_release_critical_tasks' - for policy in app.config['policies']) - assert any(policy.decision_context == 'bodhi_update_push_stable' - for policy in app.config['policies']) - assert any(policy.all_decision_contexts == ['bodhi_update_push_stable'] - for policy in app.config['policies']) - assert any(getattr(rule, 'test_case_name', None) == 'dist.rpmdeplint' - for policy in app.config['policies'] for rule in policy.rules) + app = create_app("greenwave.config.TestingConfig") + assert len(app.config["policies"]) > 0 + assert any( + policy.id == "taskotron_release_critical_tasks" + for policy in app.config["policies"] + ) + assert any( + policy.decision_context == "bodhi_update_push_stable" + for policy in app.config["policies"] + ) + assert any( + policy.all_decision_contexts == ["bodhi_update_push_stable"] + for policy in app.config["policies"] + ) + assert any( + getattr(rule, "test_case_name", None) == "dist.rpmdeplint" + for policy in app.config["policies"] + for rule in policy.rules + ) def test_misconfigured_policy_rules(tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "taskotron_release_critical_tasks" product_versions: @@ -228,7 +265,8 @@ def test_misconfigured_policy_rules(tmpdir): subject_type: bodhi_update rules: - {test_case_name: dist.abicheck} - """)) + """) + ) expected_error = ( "Policy 'taskotron_release_critical_tasks': " "Attribute 'rules': " @@ -239,8 +277,9 @@ def test_misconfigured_policy_rules(tmpdir): def test_passing_testcasename_with_scenario(tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "rawhide_compose_sync_to_mirrors" product_versions: @@ -250,18 +289,23 @@ def test_passing_testcasename_with_scenario(tmpdir): rules: - !PassingTestCaseRule {test_case_name: compose.install_default_upload, scenario: somescenario} - """)) + """) + ) load_policies(tmpdir.strpath) -@pytest.mark.parametrize(('product_version', 'applies'), [ - ('fedora-27', True), - ('fedora-28', True), - ('epel-7', False), -]) +@pytest.mark.parametrize( + ("product_version", "applies"), + [ + ("fedora-27", True), + ("fedora-28", True), + ("epel-7", False), + ], +) def test_product_versions_pattern(product_version, applies, tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: dummy_policy product_versions: @@ -270,22 +314,24 @@ def test_product_versions_pattern(product_version, applies, tmpdir): subject_type: bodhi_update rules: - !PassingTestCaseRule {test_case_name: test} - """)) + """) + ) policies = load_policies(tmpdir.strpath) policy = policies[0] assert applies == policy.matches( - decision_context='dummy_context', + decision_context="dummy_context", product_version=product_version, - subject_type='bodhi_update') + subject_type="bodhi_update", + ) -@pytest.mark.parametrize('namespace', ["rpms", ""]) +@pytest.mark.parametrize("namespace", ["rpms", ""]) def test_remote_rule_policy(tmpdir, namespace): - """ Testing the RemoteRule with the koji interaction. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction. + In this case we are just mocking koji""" - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") serverside_fragment = dedent(""" --- !Policy @@ -308,43 +354,57 @@ def test_remote_rule_policy(tmpdir, namespace): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = (namespace, 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + namespace, + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'dist.upgradepath') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath") + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] # Ensure that absence of a result is failure. results = DummyResultsRetriever() - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] # And that a result with a failure, is a failure. - results = DummyResultsRetriever(subject, 'dist.upgradepath', 'FAILED') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath", "FAILED") + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-failed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-failed", + ] f.assert_called_with( - 'https://src.fedoraproject.org/{0}'.format( - '' if not namespace else namespace + '/' - ) + 'nethack/raw/c3c47a08a66451cb9686c49f040776ed35a0d1bb/f/gating.yaml' + "https://src.fedoraproject.org/{}".format( + "" if not namespace else namespace + "/" + ) + + "nethack/raw/c3c47a08a66451cb9686c49f040776ed35a0d1bb/f/gating.yaml" ) def test_remote_rule_policy_old_config(tmpdir): - """ Testing the RemoteRule with the koji interaction. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction. + In this case we are just mocking koji""" - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") serverside_fragment = dedent(""" --- !Policy @@ -367,39 +427,45 @@ def test_remote_rule_policy_old_config(tmpdir): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) config_remote_rules_backup = Config.REMOTE_RULE_POLICIES try: - delattr(Config, 'REMOTE_RULE_POLICIES') + delattr(Config, "REMOTE_RULE_POLICIES") config = TestingConfig() - config.DIST_GIT_BASE_URL = 'http://localhost.localdomain/' - config.DIST_GIT_URL_TEMPLATE = '{DIST_GIT_BASE_URL}{pkg_name}/{rev}/gating.yaml' + config.DIST_GIT_BASE_URL = "http://localhost.localdomain/" + config.DIST_GIT_URL_TEMPLATE = "{DIST_GIT_BASE_URL}{pkg_name}/{rev}/gating.yaml" app = create_app(config) with app.app_context(): - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: scm.return_value = ( - 'rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb' + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", ) - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'dist.upgradepath') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath") + decision = Decision( + "bodhi_update_push_stable_with_remoterule", "fedora-26" + ) decision.check(subject, policies, results) assert answer_types(decision.answers) == [ - 'fetched-gating-yaml', 'test-result-passed'] + "fetched-gating-yaml", + "test-result-passed", + ] call = mock.call( - 'http://localhost.localdomain/nethack/' - 'c3c47a08a66451cb9686c49f040776ed35a0d1bb/gating.yaml' + "http://localhost.localdomain/nethack/" + "c3c47a08a66451cb9686c49f040776ed35a0d1bb/gating.yaml" ) assert f.mock_calls == [call] finally: @@ -407,14 +473,14 @@ def test_remote_rule_policy_old_config(tmpdir): def test_remote_rule_policy_brew_build_group(tmpdir): - """ Testing the RemoteRule with the koji interaction. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction. + In this case we are just mocking koji""" subject = create_subject( - 'brew-build-group', - 'sha256:0f41e56a1c32519e189ddbcb01d2551e861bd74e603d01769ef5f70d4b30a2dd' + "brew-build-group", + "sha256:0f41e56a1c32519e189ddbcb01d2551e861bd74e603d01769ef5f70d4b30a2dd", ) - namespace = 'rpms' + namespace = "rpms" serverside_fragment = dedent(""" --- !Policy @@ -438,43 +504,56 @@ def test_remote_rule_policy_brew_build_group(tmpdir): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = (namespace, 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + namespace, + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'dist.upgradepath') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath") + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] # Ensure that absence of a result is failure. results = DummyResultsRetriever() - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] # And that a result with a failure, is a failure. - results = DummyResultsRetriever(subject, 'dist.upgradepath', 'FAILED') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath", "FAILED") + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-failed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-failed", + ] f.assert_called_with( - 'https://git.example.com/devops/greenwave-policies/side-tags/raw/' - 'master/0f41e56a1c32519e189ddbcb01d2551e861bd74e603d01769ef5f70d4b30a2dd.yaml' + "https://git.example.com/devops/greenwave-policies/side-tags/raw/" + "master/0f41e56a1c32519e189ddbcb01d2551e861bd74e603d01769ef5f70d4b30a2dd.yaml" ) scm.assert_not_called() def test_remote_rule_policy_with_no_remote_rule_policies_param_defined(tmpdir): - """ Testing the RemoteRule with the koji interaction. - But this time let's assume that REMOTE_RULE_POLICIES is not defined. """ + """Testing the RemoteRule with the koji interaction. + But this time let's assume that REMOTE_RULE_POLICIES is not defined.""" - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") serverside_fragment = dedent(""" --- !Policy @@ -497,36 +576,44 @@ def test_remote_rule_policy_with_no_remote_rule_policies_param_defined(tmpdir): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - app = create_app('greenwave.config.FedoraTestingConfig') + app = create_app("greenwave.config.FedoraTestingConfig") with app.app_context(): - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'dist.upgradepath') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath") + decision = Decision( + "bodhi_update_push_stable_with_remoterule", "fedora-26" + ) decision.check(subject, policies, results) assert answer_types(decision.answers) == [ - 'fetched-gating-yaml', 'test-result-passed'] + "fetched-gating-yaml", + "test-result-passed", + ] f.assert_called_with( - 'https://src.fedoraproject.org/rpms/nethack/raw/' - 'c3c47a08a66451cb9686c49f040776ed35a0d1bb/f/gating.yaml' + "https://src.fedoraproject.org/rpms/nethack/raw/" + "c3c47a08a66451cb9686c49f040776ed35a0d1bb/f/gating.yaml" ) -@pytest.mark.parametrize('namespace', ["modules", ""]) +@pytest.mark.parametrize("namespace", ["modules", ""]) def test_remote_rule_policy_redhat_module(tmpdir, namespace): - """ Testing the RemoteRule with the koji interaction. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction. + In this case we are just mocking koji""" - nvr = '389-ds-1.4-820181127205924.9edba152' - subject = create_subject('redhat-module', nvr) + nvr = "389-ds-1.4-820181127205924.9edba152" + subject = create_subject("redhat-module", nvr) serverside_fragment = dedent(""" --- !Policy @@ -550,40 +637,56 @@ def test_remote_rule_policy_redhat_module(tmpdir, namespace): """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = (namespace, '389-ds', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + namespace, + "389-ds", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'baseos-ci.redhat-module.tier0.functional') - decision = Decision('osci_compose_gate', 'rhel-8') + results = DummyResultsRetriever( + subject, "baseos-ci.redhat-module.tier0.functional" + ) + decision = Decision("osci_compose_gate", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] # Ensure that absence of a result is failure. results = DummyResultsRetriever(subject) - decision = Decision('osci_compose_gate', 'rhel-8') + decision = Decision("osci_compose_gate", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] # And that a result with a failure, is a failure. results = DummyResultsRetriever( - subject, 'baseos-ci.redhat-module.tier0.functional', 'FAILED') - decision = Decision('osci_compose_gate', 'rhel-8') + subject, "baseos-ci.redhat-module.tier0.functional", "FAILED" + ) + decision = Decision("osci_compose_gate", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-failed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-failed", + ] def test_remote_rule_policy_redhat_container_image(tmpdir): - """ Testing the RemoteRule with the koji interaction. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction. + In this case we are just mocking koji""" - nvr = '389-ds-1.4-820181127205924.9edba152' - subject = create_subject('redhat-container-image', nvr) + nvr = "389-ds-1.4-820181127205924.9edba152" + subject = create_subject("redhat-container-image", nvr) serverside_fragment = dedent(""" --- !Policy @@ -607,41 +710,56 @@ def test_remote_rule_policy_redhat_container_image(tmpdir): """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('containers', '389-ds', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "containers", + "389-ds", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. results = DummyResultsRetriever( - subject, 'baseos-ci.redhat-container-image.tier0.functional') - decision = Decision('osci_compose_gate', 'rhel-8') + subject, "baseos-ci.redhat-container-image.tier0.functional" + ) + decision = Decision("osci_compose_gate", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] # Ensure that absence of a result is failure. results = DummyResultsRetriever(subject) - decision = Decision('osci_compose_gate', 'rhel-8') + decision = Decision("osci_compose_gate", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] # And that a result with a failure, is a failure. results = DummyResultsRetriever( - subject, 'baseos-ci.redhat-container-image.tier0.functional', 'FAILED') - decision = Decision('osci_compose_gate', 'rhel-8') + subject, "baseos-ci.redhat-container-image.tier0.functional", "FAILED" + ) + decision = Decision("osci_compose_gate", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-failed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-failed", + ] def test_remote_rule_with_multiple_contexts(tmpdir): - """ Testing the RemoteRule with the koji interaction. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction. + In this case we are just mocking koji""" - nvr = '389-ds-1.4-820181127205924.9edba152' - subject = create_subject('redhat-container-image', nvr) + nvr = "389-ds-1.4-820181127205924.9edba152" + subject = create_subject("redhat-container-image", nvr) serverside_fragment = dedent(""" --- !Policy @@ -675,52 +793,65 @@ def test_remote_rule_with_multiple_contexts(tmpdir): """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('containers', '389-ds', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "containers", + "389-ds", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) results = DummyResultsRetriever( - subject, 'baseos-ci.redhat-container-image.tier0.functional') - decision = Decision('osci_compose_gate1', 'rhel-8') + subject, "baseos-ci.redhat-container-image.tier0.functional" + ) + decision = Decision("osci_compose_gate1", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] def test_get_sub_policies_multiple_urls(tmpdir, requests_mock): - """ Testing the RemoteRule with the koji interaction when on_demand policy is given. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction when on_demand policy is given. + In this case we are just mocking koji""" config = TestingConfig() - config.REMOTE_RULE_POLICIES = {'*': [ - 'https://src1.fp.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml', - 'https://src2.fp.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml' - ]} + config.REMOTE_RULE_POLICIES = { + "*": [ + "https://src1.fp.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml", + "https://src2.fp.org/{pkg_namespace}{pkg_name}/raw/{rev}/f/gating.yaml", + ] + } app = create_app(config) - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) serverside_json = { - 'product_version': 'fedora-26', - 'id': 'taskotron_release_critical_tasks_with_remoterule', - 'subject': [{'item': nvr, 'type': 'koji_build'}], - 'rules': [ - { - 'type': 'RemoteRule', - 'required': True - }, + "product_version": "fedora-26", + "id": "taskotron_release_critical_tasks_with_remoterule", + "subject": [{"item": nvr, "type": "koji_build"}], + "rules": [ + {"type": "RemoteRule", "required": True}, ], } with app.app_context(): - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) urls = [ - 'https://src{0}.fp.org/{1}/{2}/raw/{3}/f/gating.yaml'.format(i, *scm.return_value) + "https://src{}.fp.org/{}/{}/raw/{}/f/gating.yaml".format( + i, *scm.return_value + ) for i in range(1, 3) ] for url in urls: @@ -731,11 +862,11 @@ def test_get_sub_policies_multiple_urls(tmpdir, requests_mock): assert policy.rules[0].required results = DummyResultsRetriever() - decision = Decision(None, 'fedora-26') + decision = Decision(None, "fedora-26") decision.check(subject, [policy], results) request_history = [(r.method, r.url) for r in requests_mock.request_history] - assert request_history == [('GET', urls[0]), ('GET', urls[1])] - assert answer_types(decision.answers) == ['missing-gating-yaml'] + assert request_history == [("GET", urls[0]), ("GET", urls[1])] + assert answer_types(decision.answers) == ["missing-gating-yaml"] assert not decision.answers[0].is_satisfied assert decision.answers[0].subject.identifier == subject.identifier @@ -747,8 +878,8 @@ def test_get_sub_policies_scm_error(tmpdir): fails. """ - nvr = '389-ds-1.4-820181127205924.9edba152' - subject = create_subject('redhat-container-image', nvr) + nvr = "389-ds-1.4-820181127205924.9edba152" + subject = create_subject("redhat-container-image", nvr) serverside_fragment = dedent(""" --- !Policy @@ -763,51 +894,56 @@ def test_get_sub_policies_scm_error(tmpdir): - !RemoteRule {} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: scm.side_effect = KojiScmUrlParseError("Failed to parse SCM URL") policies = load_policies(tmpdir.strpath) results = DummyResultsRetriever( - subject, 'baseos-ci.redhat-container-image.tier0.functional') - decision = Decision('osci_compose_gate1', 'rhel-8') + subject, "baseos-ci.redhat-container-image.tier0.functional" + ) + decision = Decision("osci_compose_gate1", "rhel-8") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['failed-fetch-gating-yaml'] + assert answer_types(decision.answers) == ["failed-fetch-gating-yaml"] assert not decision.answers[0].is_satisfied assert decision.answers[0].subject.identifier == subject.identifier def test_redhat_container_image_subject_type(): - nvr = '389-ds-1.4-820181127205924.9edba152' - rdb_url = 'http://results.db' - cur_time = time.strftime('%Y-%m-%dT%H:%M:%S.00') - testcase_name = 'testcase1' - rh_img_subject = create_subject('redhat-container-image', nvr) - retriever = ResultsRetriever(ignore_ids=list(), when=cur_time, url=rdb_url) - with mock.patch('requests.Session.get') as req_get: - req_get.json.return_value = {'data': {'item': [nvr]}} + nvr = "389-ds-1.4-820181127205924.9edba152" + rdb_url = "http://results.db" + cur_time = time.strftime("%Y-%m-%dT%H:%M:%S.00") + testcase_name = "testcase1" + rh_img_subject = create_subject("redhat-container-image", nvr) + retriever = ResultsRetriever(ignore_ids=[], when=cur_time, url=rdb_url) + with mock.patch("requests.Session.get") as req_get: + req_get.json.return_value = {"data": {"item": [nvr]}} retriever._retrieve_all(rh_img_subject, testcase_name) # pylint: disable=W0212 assert req_get.call_count == 2 assert req_get.call_args_list[0] == mock.call( - f'{rdb_url}/results/latest', - params={'nvr': nvr, - 'type': 'redhat-container-image', - '_distinct_on': 'scenario,system_architecture,system_variant', - 'since': f'1900-01-01T00:00:00.000000,{cur_time}', - 'testcases': testcase_name} + f"{rdb_url}/results/latest", + params={ + "nvr": nvr, + "type": "redhat-container-image", + "_distinct_on": "scenario,system_architecture,system_variant", + "since": f"1900-01-01T00:00:00.000000,{cur_time}", + "testcases": testcase_name, + }, ) assert req_get.call_args_list[1] == mock.call( - f'{rdb_url}/results/latest', - params={'item': nvr, - 'type': 'koji_build', - '_distinct_on': 'scenario,system_architecture,system_variant', - 'since': f'1900-01-01T00:00:00.000000,{cur_time}', - 'testcases': testcase_name} + f"{rdb_url}/results/latest", + params={ + "item": nvr, + "type": "koji_build", + "_distinct_on": "scenario,system_architecture,system_variant", + "since": f"1900-01-01T00:00:00.000000,{cur_time}", + "testcases": testcase_name, + }, ) def test_remote_rule_policy_optional_id(tmpdir): - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") serverside_fragment = dedent(""" --- !Policy @@ -827,25 +963,32 @@ def test_remote_rule_policy_optional_id(tmpdir): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) results = DummyResultsRetriever() - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-26") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] assert decision.answers[1].is_satisfied is False def test_remote_rule_malformed_yaml(tmpdir): - """ Testing the RemoteRule with a malformed gating.yaml file """ + """Testing the RemoteRule with a malformed gating.yaml file""" - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") serverside_fragment = dedent(""" --- !Policy @@ -858,7 +1001,8 @@ def test_remote_rule_malformed_yaml(tmpdir): - !RemoteRule {} """) - remote_fragments = [dedent(""" + remote_fragments = [ + dedent(""" --- !Policy : "some-policy-from-a-random-packager" product_versions: @@ -867,7 +1011,8 @@ def test_remote_rule_malformed_yaml(tmpdir): excluded_packages: [] rules: - !PassingTestCaseRule {test_case_name: dist.upgradepath} - """), dedent(""" + """), + dedent(""" --- !Policy id: "some-policy-from-a-random-packager" product_versions: @@ -875,31 +1020,40 @@ def test_remote_rule_malformed_yaml(tmpdir): decision_context: bodhi_update_push_stable_with_remoterule rules: - !RemoteRule {test_case_name: dist.upgradepath} - """)] + """), + ] for remote_fragment in remote_fragments: - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) results = DummyResultsRetriever() - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + decision = Decision( + "bodhi_update_push_stable_with_remoterule", "fedora-26" + ) decision.check(subject, policies, results) assert answer_types(decision.answers) == [ - 'fetched-gating-yaml', 'invalid-gating-yaml'] + "fetched-gating-yaml", + "invalid-gating-yaml", + ] assert decision.answers[0].is_satisfied is True assert decision.answers[1].is_satisfied is False def test_remote_rule_malformed_yaml_with_waiver(tmpdir): - """ Testing the RemoteRule with a malformed gating.yaml file - But this time waiving the error """ + """Testing the RemoteRule with a malformed gating.yaml file + But this time waiving the error""" - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") serverside_fragment = dedent(""" --- !Policy @@ -912,7 +1066,8 @@ def test_remote_rule_malformed_yaml_with_waiver(tmpdir): - !RemoteRule {} """) - remote_fragments = [dedent(""" + remote_fragments = [ + dedent(""" --- !Policy : "some-policy-from-a-random-packager" product_versions: @@ -921,7 +1076,8 @@ def test_remote_rule_malformed_yaml_with_waiver(tmpdir): excluded_packages: [] rules: - !PassingTestCaseRule {test_case_name: dist.upgradepath} - """), dedent(""" + """), + dedent(""" --- !Policy id: "some-policy-from-a-random-packager" product_versions: @@ -929,45 +1085,65 @@ def test_remote_rule_malformed_yaml_with_waiver(tmpdir): decision_context: bodhi_update_push_stable_with_remoterule rules: - !RemoteRule {test_case_name: dist.upgradepath} - """)] + """), + ] for remote_fragment in remote_fragments: - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) results = DummyResultsRetriever() - waivers = [{ - 'id': 1, - 'subject_type': 'koji_build', - 'subject_identifier': 'nethack-1.2.3-1.el9000', - 'subject': {'type': 'koji_build', 'item': 'nethack-1.2.3-1.el9000'}, - 'testcase': 'invalid-gating-yaml', - 'product_version': 'fedora-26', - 'comment': 'Waiving the invalid gating.yaml file', - 'waived': True, - }] - - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-26') + waivers = [ + { + "id": 1, + "subject_type": "koji_build", + "subject_identifier": "nethack-1.2.3-1.el9000", + "subject": { + "type": "koji_build", + "item": "nethack-1.2.3-1.el9000", + }, + "testcase": "invalid-gating-yaml", + "product_version": "fedora-26", + "comment": "Waiving the invalid gating.yaml file", + "waived": True, + } + ] + + decision = Decision( + "bodhi_update_push_stable_with_remoterule", "fedora-26" + ) decision.check(subject, policies, results) answers = decision.answers - assert answer_types(answers) == ['fetched-gating-yaml', 'invalid-gating-yaml'] + assert answer_types(answers) == [ + "fetched-gating-yaml", + "invalid-gating-yaml", + ] answers = waive_answers(answers, waivers) - assert answer_types(answers) == ['fetched-gating-yaml'] + assert answer_types(answers) == ["fetched-gating-yaml"] def test_remote_rule_required(): - """ Testing the RemoteRule with required flag set """ - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + """Testing the RemoteRule with required flag set""" + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = None - policies = Policy.safe_load_all(dedent(""" + policies = Policy.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -975,11 +1151,12 @@ def test_remote_rule_required(): subject_type: koji_build rules: - !RemoteRule {required: true} - """)) + """) + ) results = DummyResultsRetriever() - decision = Decision('test', 'fedora-rawhide') + decision = Decision("test", "fedora-rawhide") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['missing-gating-yaml'] + assert answer_types(decision.answers) == ["missing-gating-yaml"] assert not decision.answers[0].is_satisfied assert decision.answers[0].subject.identifier == subject.identifier @@ -997,7 +1174,8 @@ def test_parse_policies_unexpected_type(): def test_parse_policies_missing_id(): expected_error = "Policy 'untitled': Attribute 'id' is required" with pytest.raises(SafeYAMLError, match=expected_error): - Policy.safe_load_all(dedent(""" + Policy.safe_load_all( + dedent(""" --- !Policy product_versions: [fedora-rawhide] decision_context: test @@ -1005,13 +1183,15 @@ def test_parse_policies_missing_id(): excluded_packages: [] rules: - !PassingTestCaseRule {test_case_name: compose.cloud.all} - """)) + """) + ) def test_parse_policies_missing_product_versions(): expected_error = "Policy 'test': Attribute 'product_versions' is required" with pytest.raises(SafeYAMLError, match=expected_error): - Policy.safe_load_all(dedent(""" + Policy.safe_load_all( + dedent(""" --- !Policy id: test decision_context: test @@ -1019,14 +1199,16 @@ def test_parse_policies_missing_product_versions(): excluded_packages: [] rules: - !PassingTestCaseRule {test_case_name: compose.cloud.all} - """)) + """) + ) -@pytest.mark.parametrize('policy_class', [Policy, RemotePolicy]) +@pytest.mark.parametrize("policy_class", [Policy, RemotePolicy]) def test_parse_policies_missing_decision_context(policy_class): expected_error = "No decision contexts provided" with pytest.raises(SafeYAMLError, match=expected_error): - policy_class.safe_load_all(dedent(""" + policy_class.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -1034,14 +1216,18 @@ def test_parse_policies_missing_decision_context(policy_class): excluded_packages: [] rules: - !PassingTestCaseRule {test_case_name: compose.cloud.all} - """)) + """) + ) -@pytest.mark.parametrize('policy_class', [Policy, RemotePolicy]) +@pytest.mark.parametrize("policy_class", [Policy, RemotePolicy]) def test_parse_policies_both_decision_contexts_set(policy_class): - expected_error = 'Both properties "decision_contexts" and "decision_context" were set' + expected_error = ( + 'Both properties "decision_contexts" and "decision_context" were set' + ) with pytest.raises(SafeYAMLError, match=expected_error): - policy_class.safe_load_all(dedent(""" + policy_class.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -1053,12 +1239,14 @@ def test_parse_policies_both_decision_contexts_set(policy_class): - test2 rules: - !PassingTestCaseRule {test_case_name: compose.cloud.all} - """)) + """) + ) def test_policy_with_arbitrary_subject_type(tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "some_policy" product_versions: @@ -1067,18 +1255,20 @@ def test_policy_with_arbitrary_subject_type(tmpdir): subject_type: kind-of-magic rules: - !PassingTestCaseRule {test_case_name: sometest} - """)) + """) + ) policies = load_policies(tmpdir.strpath) - subject = create_subject('kind-of-magic', 'nethack-1.2.3-1.el9000') - results = DummyResultsRetriever(subject, 'sometest', 'PASSED') - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + subject = create_subject("kind-of-magic", "nethack-1.2.3-1.el9000") + results = DummyResultsRetriever(subject, "sometest", "PASSED") + decision = Decision("bodhi_update_push_stable", "rhel-9000") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['test-result-passed'] + assert answer_types(decision.answers) == ["test-result-passed"] def test_policy_all_decision_contexts(tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "some_policy1" product_versions: @@ -1099,19 +1289,21 @@ def test_policy_all_decision_contexts(tmpdir): subject_type: kind-of-magic rules: - !PassingTestCaseRule {test_case_name: sometest} - """)) + """) + ) policies = load_policies(tmpdir.strpath) policy = policies[0] assert len(policy.all_decision_contexts) == 3 - assert set(policy.all_decision_contexts) == {'test1', 'test2', 'test3'} + assert set(policy.all_decision_contexts) == {"test1", "test2", "test3"} policy = policies[1] assert len(policy.all_decision_contexts) == 1 - assert policy.all_decision_contexts == ['test4'] + assert policy.all_decision_contexts == ["test4"] def test_decision_multiple_contexts(tmpdir): - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "some_policy" product_versions: @@ -1129,23 +1321,32 @@ def test_decision_multiple_contexts(tmpdir): subject_type: kind-of-magic rules: - !PassingTestCaseRule {test_case_name: someothertest} - """)) + """) + ) policies = load_policies(tmpdir.strpath) - subject = create_subject('kind-of-magic', 'nethack-1.2.3-1.el9000') - results = DummyResultsRetriever(subject, 'sometest', 'PASSED') - decision = Decision(['bodhi_update_push_stable', 'some_other_context'], 'rhel-9000') + subject = create_subject("kind-of-magic", "nethack-1.2.3-1.el9000") + results = DummyResultsRetriever(subject, "sometest", "PASSED") + decision = Decision(["bodhi_update_push_stable", "some_other_context"], "rhel-9000") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['test-result-passed', 'test-result-missing'] - - -@pytest.mark.parametrize(('package', 'expected_answers'), [ - ('nethack', ['test-result-passed']), - ('net*', ['test-result-passed']), - ('python-requests', []), -]) + assert answer_types(decision.answers) == [ + "test-result-passed", + "test-result-missing", + ] + + +@pytest.mark.parametrize( + ("package", "expected_answers"), + [ + ("nethack", ["test-result-passed"]), + ("net*", ["test-result-passed"]), + ("python-requests", []), + ], +) def test_policy_with_packages_allowlist(tmpdir, package, expected_answers): - p = tmpdir.join('temp.yaml') - p.write(dedent(""" + p = tmpdir.join("temp.yaml") + p.write( + dedent( + """ --- !Policy id: "some_policy" product_versions: @@ -1156,11 +1357,13 @@ def test_policy_with_packages_allowlist(tmpdir, package, expected_answers): - {} rules: - !PassingTestCaseRule {{test_case_name: sometest}} - """.format(package))) + """.format(package) + ) + ) policies = load_policies(tmpdir.strpath) - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') - results = DummyResultsRetriever(subject, 'sometest', 'PASSED') - decision = Decision('test', 'rhel-9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") + results = DummyResultsRetriever(subject, "sometest", "PASSED") + decision = Decision("test", "rhel-9000") decision.check(subject, policies, results) assert answer_types(decision.answers) == expected_answers @@ -1168,7 +1371,8 @@ def test_policy_with_packages_allowlist(tmpdir, package, expected_answers): def test_parse_policies_invalid_rule(): expected_error = "Policy 'test': Attribute 'rules': Expected list of Rule objects" with pytest.raises(SafeYAMLError, match=expected_error): - Policy.safe_load_all(dedent(""" + Policy.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -1178,51 +1382,59 @@ def test_parse_policies_invalid_rule(): rules: - !PassingTestCaseRule {test_case_name: compose.cloud.all} - bad_rule - """)) + """) + ) def test_parse_policies_remote_missing_id_is_ok(): - policies = RemotePolicy.safe_load_all(dedent(""" + policies = RemotePolicy.safe_load_all( + dedent(""" --- !Policy product_versions: [fedora-rawhide] decision_context: test subject_type: koji_build rules: - !PassingTestCaseRule {test_case_name: test.case.name} - """)) + """) + ) assert len(policies) == 1 assert policies[0].id is None def test_parse_policies_remote_decision_contexts(): - policies = RemotePolicy.safe_load_all(dedent(""" + policies = RemotePolicy.safe_load_all( + dedent(""" --- !Policy product_versions: [fedora-rawhide] decision_contexts: [test1, test2] subject_type: koji_build rules: - !PassingTestCaseRule {test_case_name: test.case.name} - """)) + """) + ) assert len(policies) == 1 assert policies[0].all_decision_contexts == ["test1", "test2"] def test_parse_policies_remote_missing_subject_type_is_ok(): - policies = RemotePolicy.safe_load_all(dedent(""" + policies = RemotePolicy.safe_load_all( + dedent(""" --- !Policy product_versions: [fedora-rawhide] decision_context: test rules: - !PassingTestCaseRule {test_case_name: test.case.name} - """)) + """) + ) assert len(policies) == 1 - assert policies[0].subject_type == 'koji_build' + assert policies[0].subject_type == "koji_build" def test_parse_policies_remote_recursive(): expected_error = "Policy 'test': RemoteRule is not allowed in remote policies" with pytest.raises(SafeYAMLError, match=expected_error): - RemotePolicy.safe_load_all(dedent(""" + RemotePolicy.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -1230,11 +1442,13 @@ def test_parse_policies_remote_recursive(): subject_type: koji_build rules: - !RemoteRule {} - """)) + """) + ) def test_parse_policies_remote_multiple(): - policies = RemotePolicy.safe_load_all(dedent(""" + policies = RemotePolicy.safe_load_all( + dedent(""" --- !Policy id: test1 product_versions: [fedora-rawhide] @@ -1248,14 +1462,16 @@ def test_parse_policies_remote_multiple(): decision_context: test rules: - !PassingTestCaseRule {test_case_name: test.case.name} - """)) + """) + ) assert len(policies) == 2 - assert policies[0].id == 'test1' - assert policies[1].id == 'test2' + assert policies[0].id == "test1" + assert policies[1].id == "test2" def test_parse_policies_remote_subject_types(): - policies = RemotePolicy.safe_load_all(dedent(""" + policies = RemotePolicy.safe_load_all( + dedent(""" --- !Policy id: test1 product_versions: [fedora-rawhide] @@ -1271,22 +1487,25 @@ def test_parse_policies_remote_subject_types(): subject_type: redhat-module rules: - !PassingTestCaseRule {test_case_name: test.case.name} - """)) + """) + ) assert len(policies) == 2 - assert policies[0].id == 'test1' - assert policies[0].subject_type == 'koji_build' - assert policies[1].id == 'test2' - assert policies[1].subject_type == 'redhat-module' + assert policies[0].id == "test1" + assert policies[0].subject_type == "koji_build" + assert policies[1].id == "test2" + assert policies[1].subject_type == "redhat-module" def test_parse_policies_remote_minimal(): - policies = RemotePolicy.safe_load_all(dedent(""" + policies = RemotePolicy.safe_load_all( + dedent(""" id: test1 decision_context: test rules: [] - """)) + """) + ) assert len(policies) == 1 - assert policies[0].id == 'test1' + assert policies[0].id == "test1" assert policies[0].rules == [] @@ -1298,18 +1517,21 @@ def test_parse_policies_remote_missing_rule_attribute(): "Attribute 'test_case_name' is required" ) with pytest.raises(SafeYAMLError, match=expected_error): - RemotePolicy.safe_load_all(dedent(""" + RemotePolicy.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] decision_context: test rules: - !PassingTestCaseRule {test_case: test.case.name} - """)) + """) + ) def test_policies_to_json(): - policies = Policy.safe_load_all(dedent(""" + policies = Policy.safe_load_all( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -1317,27 +1539,29 @@ def test_policies_to_json(): subject_type: compose excluded_packages: [] rules: [] - """)) + """) + ) assert len(policies) == 1 assert policies[0].to_json() == { - 'id': 'test', - 'product_versions': ['fedora-rawhide'], - 'decision_context': 'test', - 'decision_contexts': [], - 'subject_type': 'compose', - 'excluded_packages': [], - 'packages': [], - 'rules': [], - 'relevance_key': None, - 'relevance_value': None, + "id": "test", + "product_versions": ["fedora-rawhide"], + "decision_context": "test", + "decision_contexts": [], + "subject_type": "compose", + "excluded_packages": [], + "packages": [], + "rules": [], + "relevance_key": None, + "relevance_value": None, } def test_policy_with_subject_type_component_version(tmpdir): - nv = '389-ds-base-1.4.0.10' - subject = create_subject('component-version', nv) - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + nv = "389-ds-base-1.4.0.10" + subject = create_subject("component-version", nv) + p = tmpdir.join("fedora.yaml") + p.write( + dedent(""" --- !Policy id: "test-new-subject-type" product_versions: @@ -1347,54 +1571,57 @@ def test_policy_with_subject_type_component_version(tmpdir): excluded_packages: [] rules: - !PassingTestCaseRule {test_case_name: test_for_new_type} - """)) + """) + ) policies = load_policies(tmpdir.strpath) - results = DummyResultsRetriever(subject, 'test_for_new_type', 'PASSED') - decision = Decision('decision_context_test_component_version', 'fedora-29') + results = DummyResultsRetriever(subject, "test_for_new_type", "PASSED") + decision = Decision("decision_context_test_component_version", "fedora-29") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['test-result-passed'] + assert answer_types(decision.answers) == ["test-result-passed"] -@pytest.mark.parametrize('subject_type', ["redhat-module", "redhat-container-image"]) +@pytest.mark.parametrize("subject_type", ["redhat-module", "redhat-container-image"]) def test_policy_with_subject_type_redhat_module(tmpdir, subject_type): - nsvc = 'httpd:2.4:20181018085700:9edba152' + nsvc = "httpd:2.4:20181018085700:9edba152" subject = create_subject(subject_type, nsvc) - p = tmpdir.join('fedora.yaml') - p.write(dedent(""" + p = tmpdir.join("fedora.yaml") + p.write( + dedent( + f""" --- !Policy id: "test-new-subject-type" product_versions: - fedora-29 decision_context: decision_context_test_redhat_module - subject_type: %s + subject_type: {subject_type} excluded_packages: [] rules: - - !PassingTestCaseRule {test_case_name: test_for_redhat_module_type} - """ % subject_type)) + - !PassingTestCaseRule {{test_case_name: test_for_redhat_module_type}} + """ + ) + ) policies = load_policies(tmpdir.strpath) - results = DummyResultsRetriever(subject, 'test_for_redhat_module_type', 'PASSED') - decision = Decision('decision_context_test_redhat_module', 'fedora-29') + results = DummyResultsRetriever(subject, "test_for_redhat_module_type", "PASSED") + decision = Decision("decision_context_test_redhat_module", "fedora-29") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['test-result-passed'] + assert answer_types(decision.answers) == ["test-result-passed"] -@pytest.mark.parametrize('namespace', ["rpms", ""]) +@pytest.mark.parametrize("namespace", ["rpms", ""]) def test_remote_rule_policy_on_demand_policy(namespace): - """ Testing the RemoteRule with the koji interaction when on_demand policy is given. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction when on_demand policy is given. + In this case we are just mocking koji""" - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) serverside_json = { - 'product_version': 'fedora-26', - 'id': 'taskotron_release_critical_tasks_with_remoterule', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'rules': [ - { - 'type': 'RemoteRule' - }, + "product_version": "fedora-26", + "id": "taskotron_release_critical_tasks_with_remoterule", + "subject_type": "koji_build", + "subject_identifier": nvr, + "rules": [ + {"type": "RemoteRule"}, ], } @@ -1406,57 +1633,68 @@ def test_remote_rule_policy_on_demand_policy(namespace): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = (namespace, 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + namespace, + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policy = OnDemandPolicy.create_from_json(serverside_json) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'dist.upgradepath') - decision = Decision(None, 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath") + decision = Decision(None, "fedora-26") decision.check(subject, [policy], results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] # Ensure that absence of a result is failure. results = DummyResultsRetriever() - decision = Decision(None, 'fedora-26') + decision = Decision(None, "fedora-26") decision.check(subject, [policy], results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] # And that a result with a failure, is a failure. - results = DummyResultsRetriever(subject, 'dist.upgradepath', 'FAILED') - decision = Decision(None, 'fedora-26') + results = DummyResultsRetriever(subject, "dist.upgradepath", "FAILED") + decision = Decision(None, "fedora-26") decision.check(subject, [policy], results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-failed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-failed", + ] -@pytest.mark.parametrize('two_rules', (True, False)) +@pytest.mark.parametrize("two_rules", (True, False)) def test_on_demand_policy_match(two_rules, koji_proxy): - """ Proceed other rules when there's no source URL in Koji build """ + """Proceed other rules when there's no source URL in Koji build""" - nvr = 'httpd-2.4.el9000' - subject = create_subject('koji_build', nvr) + nvr = "httpd-2.4.el9000" + subject = create_subject("koji_build", nvr) serverside_json = { - 'product_version': 'fedora-30', - 'id': 'taskotron_release_critical_tasks_with_remoterule', - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'rules': [ - { - 'type': 'RemoteRule' - } - ], + "product_version": "fedora-30", + "id": "taskotron_release_critical_tasks_with_remoterule", + "subject_type": "koji_build", + "subject_identifier": nvr, + "rules": [{"type": "RemoteRule"}], } if two_rules: - serverside_json['rules'].append({ - "type": "PassingTestCaseRule", - "test_case_name": "fake.testcase.tier0.validation" - }) + serverside_json["rules"].append( + { + "type": "PassingTestCaseRule", + "test_case_name": "fake.testcase.tier0.validation", + } + ) - koji_proxy.getBuild.return_value = {'extra': {'source': None}} + koji_proxy.getBuild.return_value = {"extra": {"source": None}} policy = OnDemandPolicy.create_from_json(serverside_json) policy_matches = policy.matches(subject=subject) @@ -1464,37 +1702,36 @@ def test_on_demand_policy_match(two_rules, koji_proxy): koji_proxy.getBuild.assert_called_once() assert policy_matches - results = DummyResultsRetriever( - subject, 'fake.testcase.tier0.validation', 'PASSED' - ) - decision = Decision(None, 'fedora-30') + results = DummyResultsRetriever(subject, "fake.testcase.tier0.validation", "PASSED") + decision = Decision(None, "fedora-30") decision.check(subject, [policy], results) if two_rules: - assert answer_types(decision.answers) == ['test-result-passed'] + assert answer_types(decision.answers) == ["test-result-passed"] def test_remote_rule_policy_on_demand_policy_required(): - """ Testing the RemoteRule with the koji interaction when on_demand policy is given. - In this case we are just mocking koji """ + """Testing the RemoteRule with the koji interaction when on_demand policy is given. + In this case we are just mocking koji""" - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) serverside_json = { - 'product_version': 'fedora-26', - 'id': 'taskotron_release_critical_tasks_with_remoterule', - 'subject': [{'item': nvr, 'type': 'koji_build'}], - 'rules': [ - { - 'type': 'RemoteRule', - 'required': True - }, + "product_version": "fedora-26", + "id": "taskotron_release_critical_tasks_with_remoterule", + "subject": [{"item": nvr, "type": "koji_build"}], + "rules": [ + {"type": "RemoteRule", "required": True}, ], } - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rpms', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = None policy = OnDemandPolicy.create_from_json(serverside_json) @@ -1503,16 +1740,16 @@ def test_remote_rule_policy_on_demand_policy_required(): assert policy.rules[0].required results = DummyResultsRetriever() - decision = Decision(None, 'fedora-26') + decision = Decision(None, "fedora-26") decision.check(subject, [policy], results) - assert answer_types(decision.answers) == ['missing-gating-yaml'] + assert answer_types(decision.answers) == ["missing-gating-yaml"] assert not decision.answers[0].is_satisfied assert decision.answers[0].subject.identifier == subject.identifier def test_two_rules_no_duplicate(tmpdir): - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) serverside_fragment = dedent(""" --- !Policy @@ -1536,45 +1773,58 @@ def test_two_rules_no_duplicate(tmpdir): - !PassingTestCaseRule {test_case_name: dist.upgradepath} """) - p = tmpdir.join('gating.yaml') + p = tmpdir.join("gating.yaml") p.write(serverside_fragment) - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as scm: - scm.return_value = ('rmps', 'nethack', 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as f: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: + scm.return_value = ( + "rmps", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment policies = load_policies(tmpdir.strpath) # Ensure that presence of a result is success. - results = DummyResultsRetriever(subject, 'dist.upgradepath') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-31') + results = DummyResultsRetriever(subject, "dist.upgradepath") + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-31") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-passed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-passed", + ] # Ensure that absence of a result is failure. results = DummyResultsRetriever() - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-31') + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-31") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-missing'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-missing", + ] # And that a result with a failure, is a failure. - results = DummyResultsRetriever(subject, 'dist.upgradepath', 'FAILED') - decision = Decision('bodhi_update_push_stable_with_remoterule', 'fedora-31') + results = DummyResultsRetriever(subject, "dist.upgradepath", "FAILED") + decision = Decision("bodhi_update_push_stable_with_remoterule", "fedora-31") decision.check(subject, policies, results) - assert answer_types(decision.answers) == ['fetched-gating-yaml', 'test-result-failed'] + assert answer_types(decision.answers) == [ + "fetched-gating-yaml", + "test-result-failed", + ] def test_cache_all_results_temporarily(): """ All results are stored in temporary cache (valid during single request). """ - subject = create_subject('bodhi_update', 'update-1') - results = DummyResultsRetriever(subject, 'sometest', 'FAILED') + subject = create_subject("bodhi_update", "update-1") + results = DummyResultsRetriever(subject, "sometest", "FAILED") retrieved = results.retrieve(subject, testcase=None) assert results.retrieve_data_called == 1 assert retrieved - cached = results.retrieve(subject, testcase='sometest') + cached = results.retrieve(subject, testcase="sometest") assert results.retrieve_data_called == 1 assert cached == retrieved @@ -1584,46 +1834,47 @@ def test_cache_passing_results(): Passing results are stored in external cache because it's not expected that the outcome changes once they passed. """ - subject = create_subject('bodhi_update', 'update-1') - results = DummyResultsRetriever(subject, 'sometest', 'FAILED') + subject = create_subject("bodhi_update", "update-1") + results = DummyResultsRetriever(subject, "sometest", "FAILED") retrieved = results.retrieve(subject, testcase=None) assert results.retrieve_data_called == 1 assert retrieved - results2 = DummyResultsRetriever(subject, 'sometest', 'PASSED') + results2 = DummyResultsRetriever(subject, "sometest", "PASSED") results2.external_cache = results.external_cache - retrieved2 = results2.retrieve(subject, testcase='sometest') + retrieved2 = results2.retrieve(subject, testcase="sometest") assert results2.retrieve_data_called == 1 assert retrieved2 assert retrieved2 != retrieved # Result stays PASSED even if the latest is now FAILED. - results3 = DummyResultsRetriever(subject, 'sometest', 'FAILED') + results3 = DummyResultsRetriever(subject, "sometest", "FAILED") results3.external_cache = results.external_cache - cached = results3.retrieve(subject, testcase='sometest') + cached = results3.retrieve(subject, testcase="sometest") assert results3.retrieve_data_called == 0 assert cached == retrieved2 # Match submit_time with "since" parameter. - when1 = retrieved2[0]['submit_time'] + when1 = retrieved2[0]["submit_time"] when2 = add_to_timestamp(when1, microseconds=1) - results3 = DummyResultsRetriever(subject, 'sometest', 'FAILED', when=when1) + results3 = DummyResultsRetriever(subject, "sometest", "FAILED", when=when1) results3.external_cache = results.external_cache - not_cached = results3.retrieve(subject, testcase='sometest') + not_cached = results3.retrieve(subject, testcase="sometest") assert results3.retrieve_data_called == 1 assert not_cached == retrieved - results4 = DummyResultsRetriever(subject, 'sometest', 'FAILED', when=when2) + results4 = DummyResultsRetriever(subject, "sometest", "FAILED", when=when2) results4.external_cache = results.external_cache - cached = results4.retrieve(subject, testcase='sometest') + cached = results4.retrieve(subject, testcase="sometest") assert results4.retrieve_data_called == 0 assert cached == retrieved2 def test_applicable_policies(): - policies = Policy.safe_load_all(dedent(""" + policies = Policy.safe_load_all( + dedent(""" --- !Policy id: test_policy product_versions: [fedora-rawhide] @@ -1632,7 +1883,8 @@ def test_applicable_policies(): rules: - !RemoteRule {required: false} - !PassingTestCaseRule {test_case_name: common.test.case} - """)) + """) + ) remote_fragment = dedent(""" --- !Policy product_versions: @@ -1643,11 +1895,14 @@ def test_applicable_policies(): - !PassingTestCaseRule {test_case_name: remote.test.case} """) - subject = create_subject('koji_build', 'nethack-1.2.3-1.el9000') + subject = create_subject("koji_build", "nethack-1.2.3-1.el9000") with mock.patch("greenwave.resources.retrieve_scm_from_koji") as scm: scm.return_value = ( - "rpms", "nethack", "c3c47a08a66451cb9686c49f040776ed35a0d1bb") + "rpms", + "nethack", + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as f: f.return_value = remote_fragment result = applicable_decision_context_product_version_pairs( diff --git a/greenwave/tests/test_product_versions.py b/greenwave/tests/test_product_versions.py index 61e9ad74..ea8e9536 100644 --- a/greenwave/tests/test_product_versions.py +++ b/greenwave/tests/test_product_versions.py @@ -9,33 +9,39 @@ def mock_subject(): - return create_subject('koji_build', 'nethack-1.2.3-1.rawhide') - - -@pytest.mark.parametrize('brew_pv, expected_pvs', ( - ('rawhide', ['fedora-rawhide']), - ('f35-candidate', ['fedora-35']), - ('epel7-candidate', ['epel-7']), - ('rhel-8.5.0-candidate', ['rhel-8', 'rhel-8.5']), - ('rhel-9.2.0-beta-candidate', ['rhel-9', 'rhel-9.2']), -)) + return create_subject("koji_build", "nethack-1.2.3-1.rawhide") + + +@pytest.mark.parametrize( + "brew_pv, expected_pvs", + ( + ("rawhide", ["fedora-rawhide"]), + ("f35-candidate", ["fedora-35"]), + ("epel7-candidate", ["epel-7"]), + ("rhel-8.5.0-candidate", ["rhel-8", "rhel-8.5"]), + ("rhel-9.2.0-beta-candidate", ["rhel-9", "rhel-9.2"]), + ), +) def test_guess_koji_build_product_version(brew_pv, expected_pvs, koji_proxy, app): koji_proxy.getTaskRequest.return_value = [ - 'git://pkgs.devel.example.com/rpms/nethack#12345abcde', + "git://pkgs.devel.example.com/rpms/nethack#12345abcde", brew_pv, - {'scratch': False}] + {"scratch": False}, + ] pvs = product_versions._guess_koji_build_product_versions( - mock_subject(), 'http://localhost:5006/kojihub', koji_task_id=1) + mock_subject(), "http://localhost:5006/kojihub", koji_task_id=1 + ) assert pvs == expected_pvs -@pytest.mark.parametrize('task_id', (None, 3)) +@pytest.mark.parametrize("task_id", (None, 3)) def test_guess_koji_build_product_version_socket_error(task_id, koji_proxy, app): koji_proxy.getBuild.side_effect = koji_proxy.getTaskRequest.side_effect = ( - socket.timeout('timed out') + socket.timeout("timed out") ) - expected = 'Could not reach Koji: timed out' + expected = "Could not reach Koji: timed out" with pytest.raises(ConnectionError, match=expected): # pylint: disable=protected-access product_versions._guess_koji_build_product_versions( - mock_subject(), 'http://localhost:5006/kojihub', task_id) + mock_subject(), "http://localhost:5006/kojihub", task_id + ) diff --git a/greenwave/tests/test_request_session.py b/greenwave/tests/test_request_session.py index dde05eb1..966cfbda 100644 --- a/greenwave/tests/test_request_session.py +++ b/greenwave/tests/test_request_session.py @@ -1,16 +1,18 @@ # SPDX-License-Identifier: GPL-2.0+ -from mock import patch from json import loads -from greenwave.request_session import get_requests_session +from unittest.mock import patch + from requests.exceptions import ConnectionError +from greenwave.request_session import get_requests_session + -@patch('requests.adapters.HTTPAdapter.send') +@patch("requests.adapters.HTTPAdapter.send") def test_retry_handler(mocked_request): - msg_text = 'It happens...' + msg_text = "It happens..." mocked_request.side_effect = ConnectionError(msg_text) session = get_requests_session() - resp = session.get('http://localhost.localdomain') + resp = session.get("http://localhost.localdomain") assert resp.status_code == 502 - assert loads(resp.content) == {'message': msg_text} + assert loads(resp.content) == {"message": msg_text} diff --git a/greenwave/tests/test_resultsdb_consumer.py b/greenwave/tests/test_resultsdb_consumer.py index d076204d..edde55d7 100644 --- a/greenwave/tests/test_resultsdb_consumer.py +++ b/greenwave/tests/test_resultsdb_consumer.py @@ -1,44 +1,46 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock -import pytest - from textwrap import dedent +from unittest import mock + +import pytest import greenwave.consumers.resultsdb from greenwave.app_factory import create_app -from greenwave.product_versions import subject_product_versions from greenwave.policies import Policy +from greenwave.product_versions import subject_product_versions from greenwave.subjects.factory import create_subject @pytest.fixture(autouse=True) def mock_retrieve_decision(): - with mock.patch('greenwave.decision.make_decision') as mocked: + with mock.patch("greenwave.decision.make_decision") as mocked: + def retrieve_decision(data, config): - #pylint: disable=unused-argument - if 'when' in data: + # pylint: disable=unused-argument + if "when" in data: return None return {} + mocked.side_effect = retrieve_decision yield mocked @pytest.fixture def mock_retrieve_results(): - with mock.patch('greenwave.resources.ResultsRetriever.retrieve') as mocked: + with mock.patch("greenwave.resources.ResultsRetriever.retrieve") as mocked: yield mocked @pytest.fixture def mock_retrieve_scm_from_koji(): - with mock.patch('greenwave.resources.retrieve_scm_from_koji') as mocked: + with mock.patch("greenwave.resources.retrieve_scm_from_koji") as mocked: yield mocked @pytest.fixture def mock_retrieve_yaml_remote_rule(): - with mock.patch('greenwave.resources.retrieve_yaml_remote_rule') as mocked: + with mock.patch("greenwave.resources.retrieve_yaml_remote_rule") as mocked: yield mocked @@ -53,10 +55,17 @@ def announcement_subject(message): def test_announcement_keys_decode_with_list(): - message = {'msg': {'data': { - 'original_spec_nvr': ['glibc-1.0-1.fc27'], - }}} - assert announcement_subject(message) == {'type': 'koji_build', 'item': 'glibc-1.0-1.fc27'} + message = { + "msg": { + "data": { + "original_spec_nvr": ["glibc-1.0-1.fc27"], + } + } + } + assert announcement_subject(message) == { + "type": "koji_build", + "item": "glibc-1.0-1.fc27", + } def test_no_announcement_subjects_for_empty_nvr(): @@ -66,9 +75,13 @@ def test_no_announcement_subjects_for_empty_nvr(): empty string. To avoid unpredictable consequences, we should not return any announcement subjects for such a message. """ - message = {'msg': {'data': { - 'original_spec_nvr': [""], - }}} + message = { + "msg": { + "data": { + "original_spec_nvr": [""], + } + } + } assert announcement_subject(message) is None @@ -76,12 +89,19 @@ def test_no_announcement_subjects_for_empty_nvr(): def test_announcement_subjects_for_brew_build(): # The 'brew-build' type appears internally within Red Hat. We treat it as an # alias of 'koji_build'. - message = {'msg': {'data': { - 'type': 'brew-build', - 'item': ['glibc-1.0-3.fc27'], - }}} + message = { + "msg": { + "data": { + "type": "brew-build", + "item": ["glibc-1.0-3.fc27"], + } + } + } - assert announcement_subject(message) == {'type': 'koji_build', 'item': 'glibc-1.0-3.fc27'} + assert announcement_subject(message) == { + "type": "koji_build", + "item": "glibc-1.0-3.fc27", + } def test_announcement_subjects_for_new_compose_message(): @@ -92,8 +112,8 @@ def test_announcement_subjects_for_new_compose_message(): possible with new-style 'resultsdb' message, like this one. """ message = { - 'msg': { - 'data': { + "msg": { + "data": { "scenario": ["fedora.universal.x86_64.64bit"], "source": ["openqa"], "productmd.compose.name": ["Fedora"], @@ -112,8 +132,9 @@ def test_announcement_subjects_for_new_compose_message(): } } - assert announcement_subject(message) == \ - {'productmd.compose.id': 'Fedora-Rawhide-20181205.n.0'} + assert announcement_subject(message) == { + "productmd.compose.id": "Fedora-Rawhide-20181205.n.0" + } def test_no_announcement_subjects_for_old_compose_message(): @@ -123,19 +144,19 @@ def test_no_announcement_subjects_for_old_compose_message(): produce any subjects for this kind of message. """ message = { - 'msg': { - 'task': { - 'item': 'Fedora-AtomicHost-28_Update-20180723.1839.x86_64.qcow2', - 'type': 'compose', - 'name': 'compose.install_no_user' + "msg": { + "task": { + "item": "Fedora-AtomicHost-28_Update-20180723.1839.x86_64.qcow2", + "type": "compose", + "name": "compose.install_no_user", + }, + "result": { + "prev_outcome": None, + "outcome": "PASSED", + "id": 23004689, + "submit_time": "2018-07-23 21:07:38 UTC", + "log_url": "https://apps.fedoraproject.org/autocloud/jobs/9238/output", }, - 'result': { - 'prev_outcome': None, - 'outcome': 'PASSED', - 'id': 23004689, - 'submit_time': '2018-07-23 21:07:38 UTC', - 'log_url': 'https://apps.fedoraproject.org/autocloud/jobs/9238/output' - } } } @@ -143,15 +164,16 @@ def test_no_announcement_subjects_for_old_compose_message(): def test_remote_rule_decision_change( - mock_retrieve_yaml_remote_rule, - mock_retrieve_scm_from_koji, - mock_retrieve_results, - koji_proxy): + mock_retrieve_yaml_remote_rule, + mock_retrieve_scm_from_koji, + mock_retrieve_results, + koji_proxy, +): """ Test publishing decision change message for test cases mentioned in gating.yaml. """ - publish = 'greenwave.consumers.consumer.fedora_messaging.api.publish' + publish = "greenwave.consumers.consumer.fedora_messaging.api.publish" with mock.patch(publish) as mock_fedora_messaging: # gating.yaml gating_yaml = dedent(""" @@ -173,74 +195,76 @@ def test_remote_rule_decision_change( - !RemoteRule {} """) - nvr = 'nethack-1.2.3-1.rawhide' + nvr = "nethack-1.2.3-1.rawhide" result = { - 'id': 1, - 'testcase': {'name': 'dist.rpmdeplint'}, - 'outcome': 'PASSED', - 'data': {'item': nvr, 'type': 'koji_build'}, - 'submit_time': '2019-03-25T16:34:41.882620' + "id": 1, + "testcase": {"name": "dist.rpmdeplint"}, + "outcome": "PASSED", + "data": {"item": nvr, "type": "koji_build"}, + "submit_time": "2019-03-25T16:34:41.882620", } mock_retrieve_results.return_value = [result] - mock_retrieve_scm_from_koji.return_value = ('rpms', nvr, - 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') + mock_retrieve_scm_from_koji.return_value = ( + "rpms", + nvr, + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'dist.rpmdeplint', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": result["id"], + "outcome": "PASSED", + "testcase": { + "name": "dist.rpmdeplint", }, - 'data': { - 'item': [nvr], - 'type': ['koji_build'], + "data": { + "item": [nvr], + "type": ["koji_build"], }, - 'submit_time': '2019-03-25T16:34:41.882620' - } + "submit_time": "2019-03-25T16:34:41.882620", + }, } } hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) - handler.flask_app.config['policies'] = Policy.safe_load_all(policies) + handler.flask_app.config["policies"] = Policy.safe_load_all(policies) with handler.flask_app.app_context(): handler.consume(message) assert len(mock_fedora_messaging.mock_calls) == 1 mock_call = mock_fedora_messaging.mock_calls[0][1][0] - assert mock_call.topic == 'greenwave.decision.update' + assert mock_call.topic == "greenwave.decision.update" actual_msgs_sent = mock_call.body assert actual_msgs_sent == { - 'decision_context': 'test_context', - 'product_version': 'fedora-rawhide', - 'subject': [ - {'item': nvr, 'type': 'koji_build'}, + "decision_context": "test_context", + "product_version": "fedora-rawhide", + "subject": [ + {"item": nvr, "type": "koji_build"}, ], - 'subject_type': 'koji_build', - 'subject_identifier': nvr, - 'previous': None, + "subject_type": "koji_build", + "subject_identifier": nvr, + "previous": None, } def test_remote_rule_decision_change_not_matching( - mock_retrieve_yaml_remote_rule, - mock_retrieve_scm_from_koji, - mock_retrieve_results): + mock_retrieve_yaml_remote_rule, mock_retrieve_scm_from_koji, mock_retrieve_results +): """ Test publishing decision change message for test cases mentioned in gating.yaml. """ - publish = 'greenwave.consumers.consumer.fedora_messaging.api.publish' + publish = "greenwave.consumers.consumer.fedora_messaging.api.publish" with mock.patch(publish) as mock_fedora_messaging: # gating.yaml gating_yaml = dedent(""" @@ -262,44 +286,47 @@ def test_remote_rule_decision_change_not_matching( - !RemoteRule {} """) - nvr = 'nethack-1.2.3-1.rawhide' + nvr = "nethack-1.2.3-1.rawhide" result = { - 'id': 1, - 'testcase': {'name': 'dist.rpmdeplint'}, - 'outcome': 'PASSED', - 'data': {'item': nvr, 'type': 'koji_build'}, - 'submit_time': '2019-03-25T16:34:41.882620' + "id": 1, + "testcase": {"name": "dist.rpmdeplint"}, + "outcome": "PASSED", + "data": {"item": nvr, "type": "koji_build"}, + "submit_time": "2019-03-25T16:34:41.882620", } mock_retrieve_results.return_value = [result] - mock_retrieve_scm_from_koji.return_value = ('rpms', nvr, - 'c3c47a08a66451cb9686c49f040776ed35a0d1bb') + mock_retrieve_scm_from_koji.return_value = ( + "rpms", + nvr, + "c3c47a08a66451cb9686c49f040776ed35a0d1bb", + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'dist.rpmdeplint', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": result["id"], + "outcome": "PASSED", + "testcase": { + "name": "dist.rpmdeplint", }, - 'data': { - 'item': [nvr], - 'type': ['koji_build'], + "data": { + "item": [nvr], + "type": ["koji_build"], }, - 'submit_time': '2019-03-25T16:34:41.882620' - } + "submit_time": "2019-03-25T16:34:41.882620", + }, } } hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) - handler.flask_app.config['policies'] = Policy.safe_load_all(policies) + handler.flask_app.config["policies"] = Policy.safe_load_all(policies) with handler.flask_app.app_context(): handler.consume(message) @@ -309,82 +336,101 @@ def test_remote_rule_decision_change_not_matching( def test_guess_product_version(): hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) with handler.flask_app.app_context(): - subject = create_subject('koji_build', 'release-e2e-test-1.0.1685-1.el5') + subject = create_subject("koji_build", "release-e2e-test-1.0.1685-1.el5") product_versions = subject_product_versions(subject) - assert product_versions == ['rhel-5'] + assert product_versions == ["rhel-5"] - subject = create_subject('redhat-module', 'rust-toolset-rhel8-20181010170614.b09eea91') + subject = create_subject( + "redhat-module", "rust-toolset-rhel8-20181010170614.b09eea91" + ) product_versions = subject_product_versions(subject) - assert product_versions == ['rhel-8'] + assert product_versions == ["rhel-8"] def test_guess_product_version_with_koji(koji_proxy, app): - koji_proxy.getBuild.return_value = {'task_id': 666} - koji_proxy.getTaskRequest.return_value = ['git://example.com/project', 'rawhide', {}] - - subject = create_subject('container-build', 'fake_koji_build') - product_versions = subject_product_versions(subject, 'http://localhost:5006/kojihub') - - koji_proxy.getBuild.assert_called_once_with('fake_koji_build') + koji_proxy.getBuild.return_value = {"task_id": 666} + koji_proxy.getTaskRequest.return_value = [ + "git://example.com/project", + "rawhide", + {}, + ] + + subject = create_subject("container-build", "fake_koji_build") + product_versions = subject_product_versions( + subject, "http://localhost:5006/kojihub" + ) + + koji_proxy.getBuild.assert_called_once_with("fake_koji_build") koji_proxy.getTaskRequest.assert_called_once_with(666) - assert product_versions == ['fedora-rawhide'] + assert product_versions == ["fedora-rawhide"] def test_guess_product_version_with_koji_without_task_id(koji_proxy, app): - koji_proxy.getBuild.return_value = {'task_id': None} + koji_proxy.getBuild.return_value = {"task_id": None} - subject = create_subject('container-build', 'fake_koji_build') - product_versions = subject_product_versions(subject, 'http://localhost:5006/kojihub') + subject = create_subject("container-build", "fake_koji_build") + product_versions = subject_product_versions( + subject, "http://localhost:5006/kojihub" + ) - koji_proxy.getBuild.assert_called_once_with('fake_koji_build') + koji_proxy.getBuild.assert_called_once_with("fake_koji_build") koji_proxy.getTaskRequest.assert_not_called() assert product_versions == [] -@pytest.mark.parametrize('task_request', ( - ['git://example.com/project', 7777, {}], - [], - None, -)) -def test_guess_product_version_with_koji_and_unexpected_task_type(task_request, koji_proxy, app): - koji_proxy.getBuild.return_value = {'task_id': 666} +@pytest.mark.parametrize( + "task_request", + ( + ["git://example.com/project", 7777, {}], + [], + None, + ), +) +def test_guess_product_version_with_koji_and_unexpected_task_type( + task_request, koji_proxy, app +): + koji_proxy.getBuild.return_value = {"task_id": 666} koji_proxy.getTaskRequest.return_value = task_request - subject = create_subject('container-build', 'fake_koji_build') - product_versions = subject_product_versions(subject, 'http://localhost:5006/kojihub') + subject = create_subject("container-build", "fake_koji_build") + product_versions = subject_product_versions( + subject, "http://localhost:5006/kojihub" + ) - koji_proxy.getBuild.assert_called_once_with('fake_koji_build') + koji_proxy.getBuild.assert_called_once_with("fake_koji_build") koji_proxy.getTaskRequest.assert_called_once_with(666) assert product_versions == [] -@pytest.mark.parametrize('nvr', ( - 'badnvr.elastic-1-228', - 'badnvr-1.2-1.elastic8', - 'el99', - 'badnvr-1.2.f30', -)) +@pytest.mark.parametrize( + "nvr", + ( + "badnvr.elastic-1-228", + "badnvr-1.2-1.elastic8", + "el99", + "badnvr-1.2.f30", + ), +) def test_guess_product_version_failure(nvr): app = create_app() with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) product_versions = subject_product_versions(subject) assert product_versions == [] def test_decision_change_for_modules( - mock_retrieve_yaml_remote_rule, - mock_retrieve_scm_from_koji, - mock_retrieve_results): + mock_retrieve_yaml_remote_rule, mock_retrieve_scm_from_koji, mock_retrieve_results +): """ Test publishing decision change message for a module. """ - publish = 'greenwave.consumers.consumer.fedora_messaging.api.publish' + publish = "greenwave.consumers.consumer.fedora_messaging.api.publish" with mock.patch(publish) as mock_fedora_messaging: # gating.yaml gating_yaml = dedent(""" @@ -410,72 +456,73 @@ def test_decision_change_for_modules( - !RemoteRule {} """) - nsvc = 'python36-3.6-820181204160430.17efdbc7' + nsvc = "python36-3.6-820181204160430.17efdbc7" result = { - 'id': 1, - 'testcase': {'name': 'baseos-ci.redhat-module.tier1.functional'}, - 'outcome': 'PASSED', - 'data': {'item': nsvc, 'type': 'redhat-module'}, - 'submit_time': '2019-03-25T16:34:41.882620' + "id": 1, + "testcase": {"name": "baseos-ci.redhat-module.tier1.functional"}, + "outcome": "PASSED", + "data": {"item": nsvc, "type": "redhat-module"}, + "submit_time": "2019-03-25T16:34:41.882620", } mock_retrieve_results.return_value = [result] - mock_retrieve_scm_from_koji.return_value = ('modules', nsvc, - '97273b80dd568bd15f9636b695f6001ecadb65e0') + mock_retrieve_scm_from_koji.return_value = ( + "modules", + nsvc, + "97273b80dd568bd15f9636b695f6001ecadb65e0", + ) message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': { - 'id': result['id'], - 'outcome': 'PASSED', - 'testcase': { - 'name': 'baseos-ci.redhat-module.tier1.functional', + "body": { + "topic": "resultsdb.result.new", + "msg": { + "id": result["id"], + "outcome": "PASSED", + "testcase": { + "name": "baseos-ci.redhat-module.tier1.functional", }, - 'data': { - 'item': [nsvc], - 'type': ['redhat-module'], + "data": { + "item": [nsvc], + "type": ["redhat-module"], }, - 'submit_time': '2019-03-25T16:34:41.882620' - } + "submit_time": "2019-03-25T16:34:41.882620", + }, } } hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) - handler.flask_app.config['policies'] = Policy.safe_load_all(policies) + handler.flask_app.config["policies"] = Policy.safe_load_all(policies) with handler.flask_app.app_context(): handler.consume(message) assert len(mock_fedora_messaging.mock_calls) == 1 mock_call = mock_fedora_messaging.mock_calls[0][1][0] - assert mock_call.topic == 'greenwave.decision.update' + assert mock_call.topic == "greenwave.decision.update" actual_msgs_sent = mock_call.body assert actual_msgs_sent == { - 'decision_context': 'osci_compose_gate_modules', - 'product_version': 'rhel-8', - 'subject': [ - {'item': nsvc, 'type': 'redhat-module'}, + "decision_context": "osci_compose_gate_modules", + "product_version": "rhel-8", + "subject": [ + {"item": nsvc, "type": "redhat-module"}, ], - 'subject_type': 'redhat-module', - 'subject_identifier': nsvc, - 'previous': None, + "subject_type": "redhat-module", + "subject_identifier": nsvc, + "previous": None, } -def test_decision_change_for_composes( - koji_proxy, - mock_retrieve_results): +def test_decision_change_for_composes(koji_proxy, mock_retrieve_results): """ Test publishing decision change message for a compose. """ - publish = 'greenwave.consumers.consumer.fedora_messaging.api.publish' + publish = "greenwave.consumers.consumer.fedora_messaging.api.publish" with mock.patch(publish) as mock_fedora_messaging: policies = dedent(""" --- !Policy @@ -492,98 +539,84 @@ def test_decision_change_for_composes( result_data = { "item": ["RHEL-9000/unknown/x86_64"], "productmd.compose.id": ["RHEL-9000"], - "type": ["compose"] + "type": ["compose"], } result = { - 'id': 1, - 'testcase': {'name': 'rtt.installability.validation'}, - 'outcome': 'PASSED', - 'data': result_data, - "submit_time": "2021-02-15T13:31:35.000001" + "id": 1, + "testcase": {"name": "rtt.installability.validation"}, + "outcome": "PASSED", + "data": result_data, + "submit_time": "2021-02-15T13:31:35.000001", } mock_retrieve_results.return_value = [result] koji_proxy.getBuild.return_value = None message = { - 'body': { - 'topic': 'resultsdb.result.new', - 'msg': result, + "body": { + "topic": "resultsdb.result.new", + "msg": result, } } hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) - handler.flask_app.config['policies'] = Policy.safe_load_all(policies) + handler.flask_app.config["policies"] = Policy.safe_load_all(policies) with handler.flask_app.app_context(): handler.consume(message) assert len(mock_fedora_messaging.mock_calls) == 1 mock_call = mock_fedora_messaging.mock_calls[0][1][0] - assert mock_call.topic == 'greenwave.decision.update' + assert mock_call.topic == "greenwave.decision.update" actual_msgs_sent = mock_call.body assert actual_msgs_sent == { - 'decision_context': 'osci_rhel8_development_nightly_compose_gate', - 'product_version': 'rhel-8', - 'subject': [{'productmd.compose.id': 'RHEL-9000'}], - 'subject_type': 'compose', - 'subject_identifier': 'RHEL-9000', - 'previous': None, + "decision_context": "osci_rhel8_development_nightly_compose_gate", + "product_version": "rhel-8", + "subject": [{"productmd.compose.id": "RHEL-9000"}], + "subject_type": "compose", + "subject_identifier": "RHEL-9000", + "previous": None, } def test_real_fedora_messaging_msg(mock_retrieve_results): message = { - 'msg': { - 'task': { - 'type': 'bodhi_update', - 'item': 'FEDORA-2019-9244c8b209', - 'name': 'update.advisory_boot' + "msg": { + "task": { + "type": "bodhi_update", + "item": "FEDORA-2019-9244c8b209", + "name": "update.advisory_boot", + }, + "result": { + "id": 23523568, + "submit_time": "2019-04-24 13:06:12 UTC", + "prev_outcome": None, + "outcome": "PASSED", + "log_url": "https://openqa.stg.fedoraproject.org/tests/528801", }, - 'result': { - 'id': 23523568, - 'submit_time': '2019-04-24 13:06:12 UTC', - 'prev_outcome': None, - 'outcome': 'PASSED', - 'log_url': 'https://openqa.stg.fedoraproject.org/tests/528801' - } } } result = { "data": { - "arch": [ - "x86_64" - ], - "firmware": [ - "bios" - ], - "item": [ - "FEDORA-2019-9244c8b209" - ], - "meta.conventions": [ - "result fedora.bodhi" - ], - "scenario": [ - "fedora.updates-server.x86_64.64bit" - ], - "source": [ - "openqa" - ], - "type": [ - "bodhi_update" - ] + "arch": ["x86_64"], + "firmware": ["bios"], + "item": ["FEDORA-2019-9244c8b209"], + "meta.conventions": ["result fedora.bodhi"], + "scenario": ["fedora.updates-server.x86_64.64bit"], + "source": ["openqa"], + "type": ["bodhi_update"], }, "groups": [ "61d11797-79cd-579f-b150-349cc77e0941", "222c442c-5d94-528f-9b9e-3fb379edf657", - "0f3309ea-6d4c-59b2-b422-d73e9b8511f3" + "0f3309ea-6d4c-59b2-b422-d73e9b8511f3", ], "href": "https://taskotron.stg.fedoraproject.org/resultsdb_api/api/v2.0/results/23523568", "id": 23523568, @@ -592,10 +625,10 @@ def test_real_fedora_messaging_msg(mock_retrieve_results): "ref_url": "https://openqa.stg.fedoraproject.org/tests/528801", "submit_time": "2019-04-24T13:06:12.135146", "testcase": { - "href": "https://taskotron.stg.fedoraproject.org/resultsdb_api/api/v2.0/testcases/update.advisory_boot", # noqa + "href": "https://taskotron.stg.fedoraproject.org/resultsdb_api/api/v2.0/testcases/update.advisory_boot", # noqa "name": "update.advisory_boot", - "ref_url": "https://openqa.stg.fedoraproject.org/tests/546627" - } + "ref_url": "https://openqa.stg.fedoraproject.org/tests/546627", + }, } policies = dedent(""" @@ -608,60 +641,60 @@ def test_real_fedora_messaging_msg(mock_retrieve_results): - !PassingTestCaseRule {test_case_name: update.advisory_boot} """) - publish = 'greenwave.consumers.consumer.fedora_messaging.api.publish' + publish = "greenwave.consumers.consumer.fedora_messaging.api.publish" with mock.patch(publish) as mock_fedora_messaging: result = { - 'id': 1, - 'testcase': {'name': 'dist.rpmdeplint'}, - 'outcome': 'PASSED', - 'data': {'item': 'FEDORA-2019-9244c8b209', 'type': 'bodhi_update'}, - 'submit_time': '2019-04-24 13:06:12.135146' + "id": 1, + "testcase": {"name": "dist.rpmdeplint"}, + "outcome": "PASSED", + "data": {"item": "FEDORA-2019-9244c8b209", "type": "bodhi_update"}, + "submit_time": "2019-04-24 13:06:12.135146", } mock_retrieve_results.return_value = [result] hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) handler.koji_base_url = None - handler.flask_app.config['policies'] = Policy.safe_load_all(policies) + handler.flask_app.config["policies"] = Policy.safe_load_all(policies) with handler.flask_app.app_context(): handler.consume(message) assert len(mock_fedora_messaging.mock_calls) == 1 mock_call = mock_fedora_messaging.mock_calls[0][1][0] - assert mock_call.topic == 'greenwave.decision.update' + assert mock_call.topic == "greenwave.decision.update" actual_msgs_sent = mock_call.body assert actual_msgs_sent == { - 'decision_context': 'test_context', - 'product_version': 'fedora-rawhide', - 'subject': [ - {'item': 'FEDORA-2019-9244c8b209', 'type': 'bodhi_update'}, + "decision_context": "test_context", + "product_version": "fedora-rawhide", + "subject": [ + {"item": "FEDORA-2019-9244c8b209", "type": "bodhi_update"}, ], - 'subject_type': 'bodhi_update', - 'subject_identifier': 'FEDORA-2019-9244c8b209', - 'previous': None, + "subject_type": "bodhi_update", + "subject_identifier": "FEDORA-2019-9244c8b209", + "previous": None, } def test_container_brew_build(mock_retrieve_results, koji_proxy): message = { - 'msg': { - 'submit_time': '2019-08-27T13:57:53.490376', - 'testcase': { - 'name': 'example_test', + "msg": { + "submit_time": "2019-08-27T13:57:53.490376", + "testcase": { + "name": "example_test", + }, + "data": { + "brew_task_id": ["666"], + "type": ["brew-build"], + "item": ["example-container"], }, - 'data': { - 'brew_task_id': ['666'], - 'type': ['brew-build'], - 'item': ['example-container'], - } } } @@ -675,30 +708,33 @@ def test_container_brew_build(mock_retrieve_results, koji_proxy): - !PassingTestCaseRule {test_case_name: example_test} """) - publish = 'greenwave.consumers.consumer.fedora_messaging.api.publish' + publish = "greenwave.consumers.consumer.fedora_messaging.api.publish" with mock.patch(publish) as mock_fedora_messaging: result = { - 'id': 1, - 'testcase': {'name': 'example_test'}, - 'outcome': 'PASSED', - 'data': {'item': 'example-container', 'type': 'koji_build'}, - 'submit_time': '2019-04-24 13:06:12.135146' + "id": 1, + "testcase": {"name": "example_test"}, + "outcome": "PASSED", + "data": {"item": "example-container", "type": "koji_build"}, + "submit_time": "2019-04-24 13:06:12.135146", } mock_retrieve_results.return_value = [result] hub = mock.MagicMock() hub.config = { - 'environment': 'environment', - 'topic_prefix': 'topic_prefix', + "environment": "environment", + "topic_prefix": "topic_prefix", } handler = greenwave.consumers.resultsdb.ResultsDBHandler(hub) koji_proxy.getBuild.return_value = None koji_proxy.getTaskRequest.return_value = [ - 'git://example.com/project', 'example_product_version', {}] + "git://example.com/project", + "example_product_version", + {}, + ] - handler.flask_app.config['policies'] = Policy.safe_load_all(policies) + handler.flask_app.config["policies"] = Policy.safe_load_all(policies) with handler.flask_app.app_context(): handler.consume(message) @@ -708,16 +744,16 @@ def test_container_brew_build(mock_retrieve_results, koji_proxy): assert len(mock_fedora_messaging.mock_calls) == 1 mock_call = mock_fedora_messaging.mock_calls[0][1][0] - assert mock_call.topic == 'greenwave.decision.update' + assert mock_call.topic == "greenwave.decision.update" actual_msgs_sent = mock_call.body assert actual_msgs_sent == { - 'decision_context': 'test_context', - 'product_version': 'example_product_version', - 'subject': [ - {'item': 'example-container', 'type': 'koji_build'}, + "decision_context": "test_context", + "product_version": "example_product_version", + "subject": [ + {"item": "example-container", "type": "koji_build"}, ], - 'subject_type': 'koji_build', - 'subject_identifier': 'example-container', - 'previous': None, + "subject_type": "koji_build", + "subject_identifier": "example-container", + "previous": None, } diff --git a/greenwave/tests/test_retrieve_gating_yaml.py b/greenwave/tests/test_retrieve_gating_yaml.py index 287d4462..f8c52674 100644 --- a/greenwave/tests/test_retrieve_gating_yaml.py +++ b/greenwave/tests/test_retrieve_gating_yaml.py @@ -2,66 +2,66 @@ import re import socket -from requests.exceptions import ConnectionError import pytest +from requests.exceptions import ConnectionError from werkzeug.exceptions import BadGateway, NotFound from greenwave.resources import ( - NoSourceException, KojiScmUrlParseError, + NoSourceException, retrieve_scm_from_koji, retrieve_yaml_remote_rule, ) def test_retrieve_scm_from_rpm_build(app, koji_proxy): - nvr = 'nethack-3.6.1-3.fc29' + nvr = "nethack-3.6.1-3.fc29" koji_proxy.getBuild.return_value = { - 'nvr': nvr, - 'extra': { - 'source': { - 'original_url': 'git+https://src.fedoraproject.org/rpms/nethack.git#master' + "nvr": nvr, + "extra": { + "source": { + "original_url": "git+https://src.fedoraproject.org/rpms/nethack.git#master" } }, - 'source': 'git+https://src.fedoraproject.org/rpms/nethack.git#' - '0c1a84e0e8a152897003bd7e27b3f407ff6ba040' + "source": "git+https://src.fedoraproject.org/rpms/nethack.git#" + "0c1a84e0e8a152897003bd7e27b3f407ff6ba040", } namespace, pkg_name, rev = retrieve_scm_from_koji(nvr) - assert namespace == 'rpms' - assert rev == '0c1a84e0e8a152897003bd7e27b3f407ff6ba040' - assert pkg_name == 'nethack' + assert namespace == "rpms" + assert rev == "0c1a84e0e8a152897003bd7e27b3f407ff6ba040" + assert pkg_name == "nethack" def test_retrieve_scm_from_rpm_build_fallback_to_source(app, koji_proxy): - nvr = 'nethack-3.6.1-3.fc29' + nvr = "nethack-3.6.1-3.fc29" koji_proxy.getBuild.return_value = { - 'nvr': nvr, - 'source': 'git+https://src.fedoraproject.org/rpms/nethack.git#' - '0c1a84e0e8a152897003bd7e27b3f407ff6ba040' + "nvr": nvr, + "source": "git+https://src.fedoraproject.org/rpms/nethack.git#" + "0c1a84e0e8a152897003bd7e27b3f407ff6ba040", } namespace, pkg_name, rev = retrieve_scm_from_koji(nvr) - assert namespace == 'rpms' - assert rev == '0c1a84e0e8a152897003bd7e27b3f407ff6ba040' - assert pkg_name == 'nethack' + assert namespace == "rpms" + assert rev == "0c1a84e0e8a152897003bd7e27b3f407ff6ba040" + assert pkg_name == "nethack" def test_retrieve_scm_from_container_build(app, koji_proxy): - nvr = 'golang-github-openshift-prometheus-alert-buffer-container-v3.10.0-0.34.0.0' + nvr = "golang-github-openshift-prometheus-alert-buffer-container-v3.10.0-0.34.0.0" koji_proxy.getBuild.return_value = { - 'nvr': nvr, - 'source': 'git://pkgs.devel.redhat.com/containers/' - 'golang-github-openshift-prometheus-alert-buffer#' - '46af2f8efbfb0a4e7e7d5676f4efb997f72d4b8c' + "nvr": nvr, + "source": "git://pkgs.devel.redhat.com/containers/" + "golang-github-openshift-prometheus-alert-buffer#" + "46af2f8efbfb0a4e7e7d5676f4efb997f72d4b8c", } namespace, pkg_name, rev = retrieve_scm_from_koji(nvr) - assert namespace == 'containers' - assert rev == '46af2f8efbfb0a4e7e7d5676f4efb997f72d4b8c' - assert pkg_name == 'golang-github-openshift-prometheus-alert-buffer' + assert namespace == "containers" + assert rev == "46af2f8efbfb0a4e7e7d5676f4efb997f72d4b8c" + assert pkg_name == "golang-github-openshift-prometheus-alert-buffer" def test_retrieve_scm_from_nonexistent_build(app, koji_proxy): - nvr = 'foo-1.2.3-1.fc29' + nvr = "foo-1.2.3-1.fc29" koji_proxy.getBuild.return_value = {} expected_error = 'Failed to find Koji build for "{}" at "{}"'.format( nvr, app.config["KOJI_BASE_URL"] @@ -79,25 +79,25 @@ def test_retrieve_scm_from_build_with_missing_source(app, koji_proxy): def test_retrieve_scm_from_build_without_namespace(app, koji_proxy): - nvr = 'foo-1.2.3-1.fc29' + nvr = "foo-1.2.3-1.fc29" koji_proxy.getBuild.return_value = { - 'nvr': nvr, - 'extra': { - 'source': { - 'original_url': 'git+https://src.fedoraproject.org/foo.git#deadbeef' + "nvr": nvr, + "extra": { + "source": { + "original_url": "git+https://src.fedoraproject.org/foo.git#deadbeef" } - } + }, } namespace, pkg_name, rev = retrieve_scm_from_koji(nvr) - assert namespace == '' - assert rev == 'deadbeef' - assert pkg_name == 'foo' + assert namespace == "" + assert rev == "deadbeef" + assert pkg_name == "foo" def test_retrieve_scm_from_koji_build_not_found(app, koji_proxy): - nvr = 'foo-1.2.3-1.fc29' + nvr = "foo-1.2.3-1.fc29" expected_error = '404 Not Found: Failed to find Koji build for "{}" at "{}"'.format( - nvr, app.config['KOJI_BASE_URL'] + nvr, app.config["KOJI_BASE_URL"] ) koji_proxy.getBuild.return_value = {} with pytest.raises(NotFound, match=expected_error): @@ -105,57 +105,58 @@ def test_retrieve_scm_from_koji_build_not_found(app, koji_proxy): def test_retrieve_scm_from_build_with_missing_rev(app, koji_proxy): - nvr = 'foo-1.2.3-1.fc29' + nvr = "foo-1.2.3-1.fc29" koji_proxy.getBuild.return_value = { - 'nvr': nvr, - 'extra': { - 'source': { - 'original_url': 'git+https://src.fedoraproject.org/rpms/foo.git' - } - } + "nvr": nvr, + "extra": { + "source": {"original_url": "git+https://src.fedoraproject.org/rpms/foo.git"} + }, } - expected_error = 'missing URL fragment with SCM revision information' + expected_error = "missing URL fragment with SCM revision information" with pytest.raises(KojiScmUrlParseError, match=expected_error): retrieve_scm_from_koji(nvr) def test_retrieve_yaml_remote_rule_no_namespace(app, requests_mock): requests_mock.get( - 'https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml', status_code=404) + "https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml", status_code=404 + ) # Return 404, because we are only interested in the URL in the request # and whether it is correct even with empty namespace. returned_file = retrieve_yaml_remote_rule( - app.config['REMOTE_RULE_POLICIES']['*'].format( - rev='deadbeaf', pkg_name='pkg', pkg_namespace='' + app.config["REMOTE_RULE_POLICIES"]["*"].format( + rev="deadbeaf", pkg_name="pkg", pkg_namespace="" ) ) request_history = [(r.method, r.url) for r in requests_mock.request_history] - assert request_history == [( - 'GET', 'https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml' - )] + assert request_history == [ + ("GET", "https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml") + ] assert returned_file is None def test_retrieve_yaml_remote_rule_connection_error(app, requests_mock): - exc = ConnectionError('Something went terribly wrong...') - requests_mock.get('https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml', exc=exc) + exc = ConnectionError("Something went terribly wrong...") + requests_mock.get( + "https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml", exc=exc + ) expected_error = re.escape( - 'Got unexpected status code 502 for' - ' https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml' + "Got unexpected status code 502 for" + " https://src.fedoraproject.org/pkg/raw/deadbeaf/f/gating.yaml" ) with pytest.raises(BadGateway, match=expected_error): retrieve_yaml_remote_rule( - app.config['REMOTE_RULE_POLICIES']['*'].format( - rev='deadbeaf', pkg_name='pkg', pkg_namespace='' + app.config["REMOTE_RULE_POLICIES"]["*"].format( + rev="deadbeaf", pkg_name="pkg", pkg_namespace="" ) ) def test_retrieve_scm_from_koji_build_socket_error(app, koji_proxy): - koji_proxy.getBuild.side_effect = socket.error('Socket is closed') - nvr = 'nethack-3.6.1-3.fc29' - expected_error = 'Could not reach Koji: Socket is closed' + koji_proxy.getBuild.side_effect = socket.error("Socket is closed") + nvr = "nethack-3.6.1-3.fc29" + expected_error = "Could not reach Koji: Socket is closed" with pytest.raises(socket.error, match=expected_error): retrieve_scm_from_koji(nvr) diff --git a/greenwave/tests/test_rules.py b/greenwave/tests/test_rules.py index a5a57194..eb1557d8 100644 --- a/greenwave/tests/test_rules.py +++ b/greenwave/tests/test_rules.py @@ -1,9 +1,8 @@ -import mock -import pytest import re - from textwrap import dedent +from unittest import mock +import pytest from defusedxml.xmlrpc import xmlrpc_client from werkzeug.exceptions import BadGateway, NotFound @@ -33,12 +32,12 @@ def test_match_passing_test_case_rule(): rule = policy.rules[0] assert rule.matches(policy) - assert rule.matches(policy, testcase='some_test_case') - assert not rule.matches(policy, testcase='other_test_case') + assert rule.matches(policy, testcase="some_test_case") + assert not rule.matches(policy, testcase="other_test_case") -@mock.patch('greenwave.resources.retrieve_yaml_remote_rule') -@mock.patch('greenwave.resources.retrieve_scm_from_koji') +@mock.patch("greenwave.resources.retrieve_yaml_remote_rule") +@mock.patch("greenwave.resources.retrieve_scm_from_koji") def test_match_remote_rule(mock_retrieve_scm_from_koji, mock_retrieve_yaml_remote_rule): policy_yaml = dedent(""" --- !Policy @@ -56,12 +55,12 @@ def test_match_remote_rule(mock_retrieve_scm_from_koji, mock_retrieve_yaml_remot rules: - !PassingTestCaseRule {test_case_name: some_test_case} """) - nvr = 'nethack-1.2.3-1.el9000' - mock_retrieve_scm_from_koji.return_value = ('rpms', nvr, '123') + nvr = "nethack-1.2.3-1.el9000" + mock_retrieve_scm_from_koji.return_value = ("rpms", nvr, "123") - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 @@ -71,18 +70,18 @@ def test_match_remote_rule(mock_retrieve_scm_from_koji, mock_retrieve_yaml_remot rule = policy.rules[0] assert rule.matches(policy) assert rule.matches(policy, subject=subject) - assert rule.matches(policy, subject=subject, testcase='some_test_case') - assert not rule.matches(policy, subject=subject, testcase='other_test_case') + assert rule.matches(policy, subject=subject, testcase="some_test_case") + assert not rule.matches(policy, subject=subject, testcase="other_test_case") assert rule.matches( policy, subject=subject, - testcase='other_test_case', + testcase="other_test_case", match_any_remote_rule=True, ) -@mock.patch('greenwave.resources.retrieve_yaml_remote_rule') -@mock.patch('greenwave.resources._koji') +@mock.patch("greenwave.resources.retrieve_yaml_remote_rule") +@mock.patch("greenwave.resources._koji") def test_invalid_nvr_iden(koji, mock_retrieve_yaml_remote_rule): policy_yaml = dedent(""" --- !Policy @@ -93,25 +92,27 @@ def test_invalid_nvr_iden(koji, mock_retrieve_yaml_remote_rule): rules: - !RemoteRule {} """) - nvr = 'nieco' - koji().getBuild.side_effect = xmlrpc_client.Fault(1000, 'invalid format') + nvr = "nieco" + koji().getBuild.side_effect = xmlrpc_client.Fault(1000, "invalid format") - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) policies = Policy.safe_load_all(policy_yaml) policy = policies[0] rule = policy.rules[0] expected_error = re.escape( - f'Failed to get Koji build for "{nvr}": invalid format (code: 1000)') + f'Failed to get Koji build for "{nvr}": invalid format (code: 1000)' + ) with pytest.raises(BadGateway, match=expected_error): sub_policies, answers = rule._get_sub_policies(policy, subject) -@mock.patch('greenwave.resources.retrieve_yaml_remote_rule') -@mock.patch('greenwave.resources.retrieve_scm_from_koji') +@mock.patch("greenwave.resources.retrieve_yaml_remote_rule") +@mock.patch("greenwave.resources.retrieve_scm_from_koji") def test_remote_rule_include_failures( - mock_retrieve_scm_from_koji, mock_retrieve_yaml_remote_rule): + mock_retrieve_scm_from_koji, mock_retrieve_yaml_remote_rule +): policy_yaml = dedent(""" --- !Policy id: "some_policy" @@ -121,12 +122,12 @@ def test_remote_rule_include_failures( rules: - !RemoteRule {} """) - nvr = 'nethack-1.2.3-1.el9000' - mock_retrieve_scm_from_koji.return_value = ('rpms', nvr, '123') + nvr = "nethack-1.2.3-1.el9000" + mock_retrieve_scm_from_koji.return_value = ("rpms", nvr, "123") - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 @@ -138,23 +139,25 @@ def test_remote_rule_include_failures( # Include any failure fetching/parsing remote rule file in the # decision. mock_retrieve_yaml_remote_rule.return_value = "--- !Policy" - assert rule.matches(policy, subject=subject, testcase='other_test_case') - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + assert rule.matches(policy, subject=subject, testcase="other_test_case") + decision = Decision("bodhi_update_push_stable", "rhel-9000") decision.check(subject, policies, results_retriever=None) assert len(decision.answers) == 2 - assert decision.answers[1].test_case_name == 'invalid-gating-yaml' + assert decision.answers[1].test_case_name == "invalid-gating-yaml" # Reload rules to clear cache. policies = Policy.safe_load_all(policy_yaml) mock_retrieve_scm_from_koji.side_effect = NotFound - assert rule.matches(policy, subject=subject, testcase='other_test_case') - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + assert rule.matches(policy, subject=subject, testcase="other_test_case") + decision = Decision("bodhi_update_push_stable", "rhel-9000") decision.check(subject, policies, results_retriever=None) - assert [x.to_json()['type'] for x in decision.answers] == ['failed-fetch-gating-yaml'] - assert decision.answers[0].error == f'Koji build not found for {subject}' + assert [x.to_json()["type"] for x in decision.answers] == [ + "failed-fetch-gating-yaml" + ] + assert decision.answers[0].error == f"Koji build not found for {subject}" -@mock.patch('greenwave.resources.retrieve_scm_from_koji') +@mock.patch("greenwave.resources.retrieve_scm_from_koji") def test_remote_rule_exclude_no_source(mock_retrieve_scm_from_koji): policy_yaml = dedent(""" --- !Policy @@ -165,11 +168,11 @@ def test_remote_rule_exclude_no_source(mock_retrieve_scm_from_koji): rules: - !RemoteRule {} """) - nvr = 'nethack-1.2.3-1.el9000' + nvr = "nethack-1.2.3-1.el9000" - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 @@ -180,35 +183,38 @@ def test_remote_rule_exclude_no_source(mock_retrieve_scm_from_koji): mock_retrieve_scm_from_koji.side_effect = NoSourceException assert rule.matches(policy, subject=subject) - assert rule.matches(policy, subject=subject, testcase='some_test_case') - assert rule.matches(policy, subject=subject, testcase='other_test_case') + assert rule.matches(policy, subject=subject, testcase="some_test_case") + assert rule.matches(policy, subject=subject, testcase="other_test_case") - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + decision = Decision("bodhi_update_push_stable", "rhel-9000") decision.check(subject, policies, results_retriever=None) assert decision.answers == [] -@pytest.mark.parametrize(('required_flag', 'required_value'), ( - ('true', True), - ('True', True), - ('on', True), - ('On', True), - ('ON', True), - ('yes', True), - ('Yes', True), - ('YES', True), - - ('false', False), - ('False', False), - ('off', False), - ('Off', False), - ('OFF', False), - ('no', False), - ('No', False), - ('NO', False), -)) +@pytest.mark.parametrize( + ("required_flag", "required_value"), + ( + ("true", True), + ("True", True), + ("on", True), + ("On", True), + ("ON", True), + ("yes", True), + ("Yes", True), + ("YES", True), + ("false", False), + ("False", False), + ("off", False), + ("Off", False), + ("OFF", False), + ("no", False), + ("No", False), + ("NO", False), + ), +) def test_remote_rule_requiered_flag(required_flag, required_value): - policy_yaml = dedent(""" + policy_yaml = ( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -216,7 +222,9 @@ def test_remote_rule_requiered_flag(required_flag, required_value): subject_type: koji_build rules: - !RemoteRule {required: %s} - """) % required_flag + """) + % required_flag + ) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 assert len(policies[0].rules) == 1 @@ -224,11 +232,10 @@ def test_remote_rule_requiered_flag(required_flag, required_value): assert policies[0].rules[0].required == required_value -@pytest.mark.parametrize('required_flag', ( - '', '0', '1', 'nope', 'TRUe', 'oN' -)) +@pytest.mark.parametrize("required_flag", ("", "0", "1", "nope", "TRUe", "oN")) def test_remote_rule_requiered_flag_bad(required_flag): - policy_yaml = dedent(""" + policy_yaml = ( + dedent(""" --- !Policy id: test product_versions: [fedora-rawhide] @@ -236,17 +243,22 @@ def test_remote_rule_requiered_flag_bad(required_flag): subject_type: koji_build rules: - !RemoteRule {required: %s} - """) % required_flag - error = 'Expected a boolean value, got: {}'.format(required_flag) + """) + % required_flag + ) + error = f"Expected a boolean value, got: {required_flag}" with pytest.raises(SafeYAMLError, match=error): Policy.safe_load_all(policy_yaml) -@pytest.mark.parametrize(('prop', 'value'), ( - ('valid_since', False), - ('valid_since', ''), - ('valid_since', 'x'), -)) +@pytest.mark.parametrize( + ("prop", "value"), + ( + ("valid_since", False), + ("valid_since", ""), + ("valid_since", "x"), + ), +) def test_remote_rule_valid_date_bad(prop, value): policy_yaml = dedent(""" --- !Policy @@ -257,22 +269,26 @@ def test_remote_rule_valid_date_bad(prop, value): rules: - !PassingTestCaseRule {test_case_name: some_test_case, %s: %s} """) % (prop, value) - error = 'Could not parse string as date/time, got: {}'.format(value) + error = f"Could not parse string as date/time, got: {value}" with pytest.raises(SafeYAMLError, match=error): Policy.safe_load_all(policy_yaml) -@pytest.mark.parametrize(('properties', 'is_valid'), ( - ('valid_since: 2021-10-06', True), - ('valid_since: 2021-10-07', False), - ('valid_until: 2021-10-07', True), - ('valid_until: 2021-10-06', False), - ('valid_since: 2021-10-06, valid_until: 2021-10-07', True), - ('valid_since: 2021-10-07, valid_until: 2021-10-08', False), - ('valid_since: 2021-10-05, valid_until: 2021-10-06', False), -)) +@pytest.mark.parametrize( + ("properties", "is_valid"), + ( + ("valid_since: 2021-10-06", True), + ("valid_since: 2021-10-07", False), + ("valid_until: 2021-10-07", True), + ("valid_until: 2021-10-06", False), + ("valid_since: 2021-10-06, valid_until: 2021-10-07", True), + ("valid_since: 2021-10-07, valid_until: 2021-10-08", False), + ("valid_since: 2021-10-05, valid_until: 2021-10-06", False), + ), +) def test_passing_test_case_rule_valid_times(koji_proxy, properties, is_valid): - policy_yaml = dedent(""" + policy_yaml = ( + dedent(""" --- !Policy id: "some_policy" product_versions: [rhel-9000] @@ -280,13 +296,15 @@ def test_passing_test_case_rule_valid_times(koji_proxy, properties, is_valid): subject_type: koji_build rules: - !PassingTestCaseRule {test_case_name: some_test_case, %s} - """) % properties + """) + % properties + ) - nvr = 'nethack-1.2.3-1.el9000' + nvr = "nethack-1.2.3-1.el9000" - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 @@ -296,25 +314,30 @@ def test_passing_test_case_rule_valid_times(koji_proxy, properties, is_valid): rule = policy.rules[0] assert rule.matches(policy, subject=subject) - assert rule.matches(policy, subject=subject, testcase='some_test_case') + assert rule.matches(policy, subject=subject, testcase="some_test_case") koji_proxy.getBuild.return_value = { - 'creation_time': '2021-10-06 06:00:00.000000+00:00'} - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + "creation_time": "2021-10-06 06:00:00.000000+00:00" + } + decision = Decision("bodhi_update_push_stable", "rhel-9000") results_retriever = mock.MagicMock() results_retriever.retrieve.return_value = [] decision.check(subject, policies, results_retriever=results_retriever) - answers = ['test-result-missing'] if is_valid else [] - assert [x.to_json()['type'] for x in decision.answers] == answers + answers = ["test-result-missing"] if is_valid else [] + assert [x.to_json()["type"] for x in decision.answers] == answers -@pytest.mark.parametrize(('creation_time', 'test_case_name'), ( - ('2021-10-06 05:59:59.999999+00:00', 'old_test_case'), - ('2021-10-06 06:00:00.000000+00:00', 'new_test_case'), -)) +@pytest.mark.parametrize( + ("creation_time", "test_case_name"), + ( + ("2021-10-06 05:59:59.999999+00:00", "old_test_case"), + ("2021-10-06 06:00:00.000000+00:00", "new_test_case"), + ), +) def test_passing_test_case_rule_replace_using_valid_times( - koji_proxy, creation_time, test_case_name): + koji_proxy, creation_time, test_case_name +): policy_yaml = dedent(""" --- !Policy id: "some_policy" @@ -330,28 +353,30 @@ def test_passing_test_case_rule_replace_using_valid_times( test_case_name: new_test_case """) - nvr = 'nethack-1.2.3-1.el9000' + nvr = "nethack-1.2.3-1.el9000" - app = create_app('greenwave.config.TestingConfig') + app = create_app("greenwave.config.TestingConfig") with app.app_context(): - subject = create_subject('koji_build', nvr) + subject = create_subject("koji_build", nvr) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 policy = policies[0] assert len(policy.rules) == 2 - koji_proxy.getBuild.return_value = {'creation_time': creation_time} - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + koji_proxy.getBuild.return_value = {"creation_time": creation_time} + decision = Decision("bodhi_update_push_stable", "rhel-9000") results_retriever = mock.MagicMock() results_retriever.retrieve.return_value = [] decision.check(subject, policies, results_retriever=results_retriever) - assert [x.to_json()['type'] for x in decision.answers] == ['test-result-missing'] + assert [x.to_json()["type"] for x in decision.answers] == [ + "test-result-missing" + ] assert decision.answers[0].test_case_name == test_case_name -@mock.patch('greenwave.resources.retrieve_yaml_remote_rule') +@mock.patch("greenwave.resources.retrieve_yaml_remote_rule") def test_remote_rule_custom_sources(mock_retrieve_yaml_remote_rule, app): url1 = "http://gating.example.com/gating1.yml" url2 = "http://gating.example.com/gating2.yml" @@ -373,15 +398,17 @@ def test_remote_rule_custom_sources(mock_retrieve_yaml_remote_rule, app): rules: - !PassingTestCaseRule {test_case_name: some_test_case} """) - mock_retrieve_yaml_remote_rule.side_effect = lambda url: gating_yaml if url == url2 else None + mock_retrieve_yaml_remote_rule.side_effect = ( + lambda url: gating_yaml if url == url2 else None + ) policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + decision = Decision("bodhi_update_push_stable", "rhel-9000") results_retriever = mock.MagicMock() results_retriever.retrieve.return_value = [] decision.check(subject, policies, results_retriever=results_retriever) @@ -397,10 +424,11 @@ def test_remote_rule_custom_sources(mock_retrieve_yaml_remote_rule, app): assert len(decision.answers) == 2 -@mock.patch('greenwave.resources.retrieve_yaml_remote_rule') -@mock.patch('greenwave.resources.retrieve_scm_from_koji') +@mock.patch("greenwave.resources.retrieve_yaml_remote_rule") +@mock.patch("greenwave.resources.retrieve_scm_from_koji") def test_remote_rule_custom_sources_multiple_rules( - mock_retrieve_scm_from_koji, mock_retrieve_yaml_remote_rule, app): + mock_retrieve_scm_from_koji, mock_retrieve_yaml_remote_rule, app +): url1 = "http://gating.example.com/gating1.yml" url2 = "http://gating.example.com/gating2.yml" policy_yaml = dedent(f""" @@ -424,17 +452,19 @@ def test_remote_rule_custom_sources_multiple_rules( rules: - !PassingTestCaseRule {test_case_name: some_test_case} """) - mock_retrieve_yaml_remote_rule.side_effect = lambda url: gating_yaml if url == url2 else None + mock_retrieve_yaml_remote_rule.side_effect = ( + lambda url: gating_yaml if url == url2 else None + ) mock_retrieve_scm_from_koji.side_effect = NotFound policies = Policy.safe_load_all(policy_yaml) assert len(policies) == 1 assert len(policies[0].rules) == 3 - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + decision = Decision("bodhi_update_push_stable", "rhel-9000") results_retriever = mock.MagicMock() results_retriever.retrieve.return_value = [] decision.check(subject, policies, results_retriever=results_retriever) @@ -458,7 +488,7 @@ def test_remote_rule_custom_sources_multiple_rules( assert len(decision.answers) == 3 -@mock.patch('greenwave.resources.retrieve_scm_from_koji') +@mock.patch("greenwave.resources.retrieve_scm_from_koji") def test_koji_failed(mock_retrieve_scm_from_koji, app): policy_yaml = dedent(""" --- @@ -475,11 +505,13 @@ def test_koji_failed(mock_retrieve_scm_from_koji, app): assert len(policies) == 1 assert len(policies[0].rules) == 1 - nvr = 'nethack-1.2.3-1.el9000' - subject = create_subject('koji_build', nvr) + nvr = "nethack-1.2.3-1.el9000" + subject = create_subject("koji_build", nvr) - decision = Decision('bodhi_update_push_stable', 'rhel-9000') + decision = Decision("bodhi_update_push_stable", "rhel-9000") results_retriever = mock.MagicMock() results_retriever.retrieve.return_value = [] - with pytest.raises(BadGateway, match='Unexpected error while fetching remote policies'): + with pytest.raises( + BadGateway, match="Unexpected error while fetching remote policies" + ): decision.check(subject, policies, results_retriever=results_retriever) diff --git a/greenwave/tests/test_subjects.py b/greenwave/tests/test_subjects.py index 9f22f508..b60b0912 100644 --- a/greenwave/tests/test_subjects.py +++ b/greenwave/tests/test_subjects.py @@ -3,21 +3,21 @@ import pytest from greenwave.subjects.factory import ( + UnknownSubjectDataError, create_subject, create_subject_from_data, - UnknownSubjectDataError, ) def test_subject_create_from_data(app): data = { - 'item': 'some_item', - 'type': 'some_type', + "item": "some_item", + "type": "some_type", } subject = create_subject_from_data(data) - assert subject.item == 'some_item' - assert subject.type == 'some_type' + assert subject.item == "some_item" + assert subject.type == "some_type" assert subject.to_dict() == data assert list(subject.result_queries()) == [data] @@ -28,9 +28,9 @@ def test_subject_create_from_data_bad(app): def test_subject_create_generic(app): - subject = create_subject('some_type', 'some_item') - assert subject.item == 'some_item' - assert subject.type == 'some_type' + subject = create_subject("some_type", "some_item") + assert subject.item == "some_item" + assert subject.type == "some_type" assert subject.identifier == subject.item assert subject.package_name is None assert subject.short_product_version is None @@ -39,39 +39,42 @@ def test_subject_create_generic(app): def test_subject_koji_build_result_queries(app): - subject = create_subject('koji_build', 'some_nvr') + subject = create_subject("koji_build", "some_nvr") assert list(subject.result_queries()) == [ - {'type': 'koji_build,brew-build', 'item': 'some_nvr'}, - {'original_spec_nvr': 'some_nvr'}, + {"type": "koji_build,brew-build", "item": "some_nvr"}, + {"original_spec_nvr": "some_nvr"}, ] def test_subject_ignore_missing_policy(app): - subject = create_subject('bodhi_update', 'some_item') + subject = create_subject("bodhi_update", "some_item") assert subject.ignore_missing_policy def test_subject_to_str(app): - subject = create_subject('koji_build', 'some_nvr') + subject = create_subject("koji_build", "some_nvr") assert str(subject) == "subject_type 'koji_build', subject_identifier 'some_nvr'" def test_subject_to_repr(app): - subject = create_subject('koji_build', 'some_nvr') + subject = create_subject("koji_build", "some_nvr") assert repr(subject) == "Subject(, 'some_nvr')" def test_subject_to_repr_generic(app): - subject = create_subject('some_type', 'some_nvr') + subject = create_subject("some_type", "some_nvr") assert repr(subject) == "Subject(, 'some_nvr')" -@pytest.mark.parametrize('compose, expected_product_version', ( - ('RHEL-8.5.0-20210101.d.1', ['rhel-8']), - ('RHEL-9.0.0-20210101.d.2', ['rhel-9']), - ('FEDORA-2021-35759ad8d3', []), -)) +@pytest.mark.parametrize( + "compose, expected_product_version", + ( + ("RHEL-8.5.0-20210101.d.1", ["rhel-8"]), + ("RHEL-9.0.0-20210101.d.2", ["rhel-9"]), + ("FEDORA-2021-35759ad8d3", []), + ), +) def test_subject_product_version_match(compose, expected_product_version, app): - subject = create_subject('compose', compose) + subject = create_subject("compose", compose) assert subject.subject_type.product_version_match assert subject.product_versions == expected_product_version, compose diff --git a/greenwave/tests/test_subjects_type.py b/greenwave/tests/test_subjects_type.py index 145f4807..8641cac6 100644 --- a/greenwave/tests/test_subjects_type.py +++ b/greenwave/tests/test_subjects_type.py @@ -7,32 +7,32 @@ ) -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def subject_types(): yield load_subject_types(TestingConfig.SUBJECT_TYPES_DIR) def test_subject_type_create(subject_types): - subject_type = create_subject_type('koji_build', subject_types) - assert subject_type.id == 'koji_build' - assert subject_type.aliases == ['brew-build'] + subject_type = create_subject_type("koji_build", subject_types) + assert subject_type.id == "koji_build" + assert subject_type.aliases == ["brew-build"] assert subject_type.is_koji_build assert subject_type.is_nvr - assert subject_type.item_key == 'original_spec_nvr' + assert subject_type.item_key == "original_spec_nvr" def test_subject_type_matches_type_id(subject_types): - subject_type = create_subject_type('koji_build', subject_types) - assert subject_type.id == 'koji_build' - assert subject_type.matches('koji_build') + subject_type = create_subject_type("koji_build", subject_types) + assert subject_type.id == "koji_build" + assert subject_type.matches("koji_build") def test_subject_type_matches_alias(subject_types): - subject_type = create_subject_type('koji_build', subject_types) - assert subject_type.aliases == ['brew-build'] - assert subject_type.matches('brew-build') + subject_type = create_subject_type("koji_build", subject_types) + assert subject_type.aliases == ["brew-build"] + assert subject_type.matches("brew-build") def test_subject_type_safe_yaml_label(subject_types): - subject_type = create_subject_type('koji_build', subject_types) + subject_type = create_subject_type("koji_build", subject_types) assert subject_type.safe_yaml_label == "SubjectType 'koji_build'" diff --git a/greenwave/tests/test_summary.py b/greenwave/tests/test_summary.py index b9870245..48c43fb3 100644 --- a/greenwave/tests/test_summary.py +++ b/greenwave/tests/test_summary.py @@ -1,78 +1,79 @@ # SPDX-License-Identifier: GPL-2.0+ from greenwave.policies import ( - summarize_answers, + InvalidRemoteRuleYaml, RuleSatisfied, TestResultErrored, TestResultFailed, - TestResultMissing, TestResultIncomplete, + TestResultMissing, TestResultWaived, - InvalidRemoteRuleYaml, + summarize_answers, ) - from greenwave.subjects.subject import Subject from greenwave.subjects.subject_type import GenericSubjectType - -testSubject = Subject(GenericSubjectType('koji_build'), 'nethack-1.2.3-1.el9000') +testSubject = Subject(GenericSubjectType("koji_build"), "nethack-1.2.3-1.el9000") testResultPassed = RuleSatisfied() -testResultErrored = TestResultErrored( - testSubject, 'test', None, 1, {}, 'some error') -testResultFailed = TestResultFailed( - testSubject, 'test', None, 1, {}) -testResultIncomplete = TestResultIncomplete( - testSubject, 'test', None, 1, {}) -testResultMissing = TestResultMissing( - testSubject, 'test', None, None) +testResultErrored = TestResultErrored(testSubject, "test", None, 1, {}, "some error") +testResultFailed = TestResultFailed(testSubject, "test", None, 1, {}) +testResultIncomplete = TestResultIncomplete(testSubject, "test", None, 1, {}) +testResultMissing = TestResultMissing(testSubject, "test", None, None) testInvalidGatingYaml = InvalidRemoteRuleYaml( - testSubject, 'test', 'Missing !Policy tag', None) + testSubject, "test", "Missing !Policy tag", None +) def test_summary_passed(): answers = [ testResultPassed, ] - assert summarize_answers(answers) == 'All required tests (1 total) have passed or been waived' + assert ( + summarize_answers(answers) + == "All required tests (1 total) have passed or been waived" + ) def test_summary_empty(): answers = [] - assert summarize_answers(answers) == 'No tests are required' + assert summarize_answers(answers) == "No tests are required" def test_summary_failed(): answers = [ testResultFailed, ] - assert summarize_answers(answers) == 'Of 1 required test, 1 test failed' + assert summarize_answers(answers) == "Of 1 required test, 1 test failed" def test_summary_incomplete(): answers = [ testResultIncomplete, ] - assert summarize_answers(answers) == 'Of 1 required test, 1 test incomplete' + assert summarize_answers(answers) == "Of 1 required test, 1 test incomplete" def test_summary_missing(): answers = [ testResultMissing, ] - assert summarize_answers(answers) == 'Of 1 required test, 1 result missing' + assert summarize_answers(answers) == "Of 1 required test, 1 result missing" def test_summary_missing_waived(): answers = [ TestResultWaived(testResultMissing, 123), ] - assert summarize_answers(answers) == 'All required tests (1 total) have passed or been waived' + assert ( + summarize_answers(answers) + == "All required tests (1 total) have passed or been waived" + ) def test_summary_errored(): answers = [ testResultErrored, ] - assert summarize_answers(answers) == 'Of 1 required test, 1 test errored' + assert summarize_answers(answers) == "Of 1 required test, 1 test errored" def test_summary_one_passed_one_failed(): @@ -80,7 +81,7 @@ def test_summary_one_passed_one_failed(): testResultPassed, testResultFailed, ] - assert summarize_answers(answers) == 'Of 2 required tests, 1 test failed' + assert summarize_answers(answers) == "Of 2 required tests, 1 test failed" def test_summary_one_passed_one_missing(): @@ -88,7 +89,7 @@ def test_summary_one_passed_one_missing(): testResultPassed, testResultMissing, ] - assert summarize_answers(answers) == 'Of 2 required tests, 1 result missing' + assert summarize_answers(answers) == "Of 2 required tests, 1 result missing" def test_summary_one_passed_one_missing_waived(): @@ -96,7 +97,10 @@ def test_summary_one_passed_one_missing_waived(): testResultPassed, TestResultWaived(testResultMissing, 123), ] - assert summarize_answers(answers) == 'All required tests (2 total) have passed or been waived' + assert ( + summarize_answers(answers) + == "All required tests (2 total) have passed or been waived" + ) def test_summary_one_failed_one_missing(): @@ -104,7 +108,7 @@ def test_summary_one_failed_one_missing(): testResultFailed, testResultMissing, ] - exp = 'Of 2 required tests, 1 result missing, 1 test failed' + exp = "Of 2 required tests, 1 result missing, 1 test failed" assert summarize_answers(answers) == exp @@ -114,7 +118,7 @@ def test_summary_one_passed_one_failed_one_missing(): testResultFailed, testResultMissing, ] - exp = 'Of 3 required tests, 1 result missing, 1 test failed' + exp = "Of 3 required tests, 1 result missing, 1 test failed" assert summarize_answers(answers) == exp @@ -126,7 +130,7 @@ def test_summary_one_passed_one_failed_one_missing_two_errored(): testResultMissing, testResultErrored, ] - exp = 'Of 5 required tests, 1 result missing, 2 tests errored, 1 test failed' + exp = "Of 5 required tests, 1 result missing, 2 tests errored, 1 test failed" assert summarize_answers(answers) == exp @@ -134,7 +138,7 @@ def test_summary_invalid_gating_yaml(): answers = [ testInvalidGatingYaml, ] - exp = '1 error due to invalid remote rule file' + exp = "1 error due to invalid remote rule file" assert summarize_answers(answers) == exp @@ -144,6 +148,6 @@ def test_summary_one_passed_one_invalid_gating_yaml_one_missing(): testResultMissing, testInvalidGatingYaml, ] - exp = '1 error due to invalid remote rule file. ' - exp += 'Of 2 required tests, 1 result missing' + exp = "1 error due to invalid remote rule file. " + exp += "Of 2 required tests, 1 result missing" assert summarize_answers(answers) == exp diff --git a/greenwave/tests/test_tracing.py b/greenwave/tests/test_tracing.py index 87cbe081..58f193d8 100644 --- a/greenwave/tests/test_tracing.py +++ b/greenwave/tests/test_tracing.py @@ -10,54 +10,24 @@ JSON_MESSAGE = { "data": { - "brew_task_id": [ - "57212843" - ], - "ci_docs": [ - "https://docs.example.com/ci" - ], - "ci_email": [ - "example@email.com" - ], - "ci_name": [ - "Container Test" - ], - "ci_team": [ - "Test" - ], - "ci_url": [ - "https://docs.example.com/ci" - ], - "full_names": [ - "https://docs.example.com/ci" - ], + "brew_task_id": ["57212843"], + "ci_docs": ["https://docs.example.com/ci"], + "ci_email": ["example@email.com"], + "ci_name": ["Container Test"], + "ci_team": ["Test"], + "ci_url": ["https://docs.example.com/ci"], + "full_names": ["https://docs.example.com/ci"], "id": [ "sha256:a7fc01280c6b8173611c75a2cbd5a19f5d2ce42d9578d4efcc944e4bc80b09a0" ], - "issuer": [ - "Test" - ], - "item": [ - "avahi" - ], - "log": [ - "https://docs.example.com/ci" - ], - "msg_id": [ - "ID:jenkins-2-8dcwr-46389-1700226798425-136563:1:1:1:1" - ], - "scratch": [ - "false" - ], - "system_architecture": [ - "x86_64" - ], - "system_provider": [ - "Test" - ], - "type": [ - "redhat-module" - ] + "issuer": ["Test"], + "item": ["avahi"], + "log": ["https://docs.example.com/ci"], + "msg_id": ["ID:jenkins-2-8dcwr-46389-1700226798425-136563:1:1:1:1"], + "scratch": ["false"], + "system_architecture": ["x86_64"], + "system_provider": ["Test"], + "type": ["redhat-module"], }, "groups": [], "href": "https://docs.example.com/ci", @@ -69,24 +39,24 @@ "testcase": { "href": "https://docs.example.com/ci", "name": "baseos-ci.redhat-module.tier0.functional", - "ref_url": None + "ref_url": None, }, - "traceparent": "00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01" + "traceparent": "00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01", } patch_subject = SubjectType() patch_subject.id = "redhat-module" # type: ignore patch_decision = { - 'policies_satisfied': True, - 'summary': "TestSucced", - 'satisfied_requirements': [None], - 'unsatisfied_requirements': [None] + "policies_satisfied": True, + "summary": "TestSucced", + "satisfied_requirements": [None], + "unsatisfied_requirements": [None], } patch_old_decision = { - 'policies_satisfied': True, - 'summary': "TestSucced", - 'satisfied_requirements': [None], - 'unsatisfied_requirements': [None] + "policies_satisfied": True, + "summary": "TestSucced", + "satisfied_requirements": [None], + "unsatisfied_requirements": [None], } real_connection = stomp.connect.StompConnection11() @@ -94,19 +64,23 @@ mock_connection.send = MagicMock(side_effect=[]) -@patch('greenwave.listeners.base._is_decision_unchanged', return_value=False) -@patch.object(ResultsDBListener, '_old_and_new_decisions', - return_value=(patch_old_decision, patch_decision)) -@patch('greenwave.subjects.factory.subject_types', return_value=[patch_subject]) +@patch("greenwave.listeners.base._is_decision_unchanged", return_value=False) +@patch.object( + ResultsDBListener, + "_old_and_new_decisions", + return_value=(patch_old_decision, patch_decision), +) +@patch("greenwave.subjects.factory.subject_types", return_value=[patch_subject]) def test_tracing(mocked_factory, mocked_decision, mocked_decision_unchanged): resultdb_class = ResultsDBListener() - with patch.object(resultdb_class, 'connection', side_effect=mock_connection): + with patch.object(resultdb_class, "connection", side_effect=mock_connection): mock_publish = MagicMock(side_effect=resultdb_class._publish_decision_update) resultdb_class._publish_decision_update = mock_publish resultdb_class._consume_message(JSON_MESSAGE) mock_publish.assert_called_once() - assert mock_publish.call_args.args[0]["traceparent"] == JSON_MESSAGE[ - "traceparent"] + assert ( + mock_publish.call_args.args[0]["traceparent"] == JSON_MESSAGE["traceparent"] + ) @patch("greenwave.tracing.TracerProvider") @@ -114,33 +88,35 @@ def test_tracing(mocked_factory, mocked_decision, mocked_decision_unchanged): @patch("greenwave.tracing.FlaskInstrumentor") @patch("greenwave.tracing.BatchSpanProcessor") @patch("greenwave.tracing.Resource") -def test_init_tracing_with_valid_config(mock_resource, mock_batch, mock_instrumentor, - mock_span_exporter, - mock_provider): +def test_init_tracing_with_valid_config( + mock_resource, mock_batch, mock_instrumentor, mock_span_exporter, mock_provider +): app = MagicMock() - app.config.get.side_effect = lambda key: { + app.config.get.side_effect = { "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://example.com", - "OTEL_EXPORTER_SERVICE_NAME": "example_service" - }.get(key) + "OTEL_EXPORTER_SERVICE_NAME": "example_service", + }.get init_tracing(app) - mock_provider.assert_called_once_with( - resource=mock_resource.create.return_value) + mock_provider.assert_called_once_with(resource=mock_resource.create.return_value) mock_span_exporter.assert_called_once_with(endpoint="http://example.com") mock_provider.return_value.add_span_processor.assert_called_once_with( - mock_batch.return_value) - (mock_instrumentor().instrument_app. - assert_called_once_with(app, - tracer_provider=mock_provider.return_value)) + mock_batch.return_value + ) + ( + mock_instrumentor().instrument_app.assert_called_once_with( + app, tracer_provider=mock_provider.return_value + ) + ) @patch("greenwave.tracing.TracerProvider") def test_init_tracing_with_invalid_config_name(mock_provider): app = MagicMock() - app.config.get.side_effect = lambda key: { + app.config.get.side_effect = { "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://example.com", - }.get(key) + }.get init_tracing(app) mock_provider.assert_not_called() @@ -149,9 +125,9 @@ def test_init_tracing_with_invalid_config_name(mock_provider): @patch("greenwave.tracing.TracerProvider") def test_init_tracing_with_invalid_config_endpoint(mock_provider): app = MagicMock() - app.config.get.side_effect = lambda key: { + app.config.get.side_effect = { "OTEL_EXPORTER_SERVICE_NAME": "example_service", - }.get(key) + }.get init_tracing(app) mock_provider.assert_not_called() diff --git a/greenwave/tests/test_utils.py b/greenwave/tests/test_utils.py index 54912975..03fd8a0b 100644 --- a/greenwave/tests/test_utils.py +++ b/greenwave/tests/test_utils.py @@ -10,52 +10,54 @@ import greenwave.app_factory from greenwave.utils import json_error, load_config - SETTINGS_DIR = os.path.abspath(os.path.dirname(__file__)) -SETTINGS_BASE_NAME = os.path.join(SETTINGS_DIR, 'settings') - - -@pytest.mark.parametrize(('error, expected_status_code,' - 'expected_error_message_part'), [ - (ConnectionError('ERROR'), 502, 'ERROR'), - (requests.ConnectionError('ERROR'), 502, 'ERROR'), - (requests.ConnectTimeout('TIMEOUT'), 502, 'TIMEOUT'), - (requests.Timeout('TIMEOUT'), 504, 'TIMEOUT'), - (InternalServerError(), 500, 'The server encountered an internal error') -]) -def test_json_connection_error(error, expected_status_code, - expected_error_message_part): +SETTINGS_BASE_NAME = os.path.join(SETTINGS_DIR, "settings") + + +@pytest.mark.parametrize( + ("error, expected_status_code, expected_error_message_part"), + [ + (ConnectionError("ERROR"), 502, "ERROR"), + (requests.ConnectionError("ERROR"), 502, "ERROR"), + (requests.ConnectTimeout("TIMEOUT"), 502, "TIMEOUT"), + (requests.Timeout("TIMEOUT"), 504, "TIMEOUT"), + (InternalServerError(), 500, "The server encountered an internal error"), + ], +) +def test_json_connection_error( + error, expected_status_code, expected_error_message_part +): app = greenwave.app_factory.create_app() with app.app_context(): with app.test_request_context(): r = json_error(error) data = json.loads(r.get_data()) assert r.status_code == expected_status_code - assert expected_error_message_part in data['message'] + assert expected_error_message_part in data["message"] def test_load_config_defaults(monkeypatch): - monkeypatch.setenv('GREENWAVE_CONFIG', SETTINGS_BASE_NAME + '_empty.py') - monkeypatch.delenv('GREENWAVE_POLICIES_DIR', raising=False) - monkeypatch.delenv('GREENWAVE_SUBJECT_TYPES_DIR', raising=False) - config = load_config('greenwave.config.ProductionConfig') - assert config['POLICIES_DIR'] == '/etc/greenwave/policies' - assert config['SUBJECT_TYPES_DIR'] == '/etc/greenwave/subject_types' + monkeypatch.setenv("GREENWAVE_CONFIG", SETTINGS_BASE_NAME + "_empty.py") + monkeypatch.delenv("GREENWAVE_POLICIES_DIR", raising=False) + monkeypatch.delenv("GREENWAVE_SUBJECT_TYPES_DIR", raising=False) + config = load_config("greenwave.config.ProductionConfig") + assert config["POLICIES_DIR"] == "/etc/greenwave/policies" + assert config["SUBJECT_TYPES_DIR"] == "/etc/greenwave/subject_types" def test_load_config_override_with_env(monkeypatch): - monkeypatch.setenv('GREENWAVE_CONFIG', SETTINGS_BASE_NAME + '_empty.py') - monkeypatch.setenv('GREENWAVE_POLICIES_DIR', '/policies') - monkeypatch.setenv('GREENWAVE_SUBJECT_TYPES_DIR', '/subject_types') - config = load_config('greenwave.config.ProductionConfig') - assert config['POLICIES_DIR'] == '/policies' - assert config['SUBJECT_TYPES_DIR'] == '/subject_types' + monkeypatch.setenv("GREENWAVE_CONFIG", SETTINGS_BASE_NAME + "_empty.py") + monkeypatch.setenv("GREENWAVE_POLICIES_DIR", "/policies") + monkeypatch.setenv("GREENWAVE_SUBJECT_TYPES_DIR", "/subject_types") + config = load_config("greenwave.config.ProductionConfig") + assert config["POLICIES_DIR"] == "/policies" + assert config["SUBJECT_TYPES_DIR"] == "/subject_types" def test_load_config_override_with_custom_config(monkeypatch): - monkeypatch.setenv('GREENWAVE_CONFIG', SETTINGS_BASE_NAME + '_override.py') - monkeypatch.setenv('GREENWAVE_POLICIES_DIR', '/policies') - monkeypatch.setenv('GREENWAVE_SUBJECT_TYPES_DIR', '/subject_types') - config = load_config('greenwave.config.ProductionConfig') - assert config['POLICIES_DIR'] == '/src/conf/policies' - assert config['SUBJECT_TYPES_DIR'] == '/src/conf/subject_types' + monkeypatch.setenv("GREENWAVE_CONFIG", SETTINGS_BASE_NAME + "_override.py") + monkeypatch.setenv("GREENWAVE_POLICIES_DIR", "/policies") + monkeypatch.setenv("GREENWAVE_SUBJECT_TYPES_DIR", "/subject_types") + config = load_config("greenwave.config.ProductionConfig") + assert config["POLICIES_DIR"] == "/src/conf/policies" + assert config["SUBJECT_TYPES_DIR"] == "/src/conf/subject_types" diff --git a/greenwave/tests/test_waive.py b/greenwave/tests/test_waive.py index dc74d223..688a733e 100644 --- a/greenwave/tests/test_waive.py +++ b/greenwave/tests/test_waive.py @@ -12,17 +12,17 @@ def test_subject(): - return Subject(GenericSubjectType('koji_build'), 'nethack-1.2.3-1.rawhide') + return Subject(GenericSubjectType("koji_build"), "nethack-1.2.3-1.rawhide") def test_waive_failed_result(): answers = [ TestResultFailed( subject=test_subject(), - test_case_name='test1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + source="https://greenwave_tests.example.com", result_id=99, - data={'scenario': 'scenario1'}, + data={"scenario": "scenario1"}, ) ] @@ -32,22 +32,22 @@ def test_waive_failed_result(): waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", ) ] waived = waive_answers(answers, waivers) expected_json = dict( - type='test-result-failed-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-failed-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", result_id=99, waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', + scenario="scenario1", + source="https://greenwave_tests.example.com", ) assert 1 == len(waived) assert expected_json == waived[0].to_json() @@ -57,9 +57,9 @@ def test_waive_missing_result(): answers = [ TestResultMissing( subject=test_subject(), - test_case_name='test1', - scenario='scenario1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + scenario="scenario1", + source="https://greenwave_tests.example.com", ) ] @@ -69,21 +69,21 @@ def test_waive_missing_result(): waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", ) ] waived = waive_answers(answers, waivers) expected_json = dict( - type='test-result-missing-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-missing-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', + scenario="scenario1", + source="https://greenwave_tests.example.com", ) assert 1 == len(waived) assert expected_json == waived[0].to_json() @@ -93,10 +93,10 @@ def test_waive_incomplete_result(): answers = [ TestResultIncomplete( subject=test_subject(), - test_case_name='test1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + source="https://greenwave_tests.example.com", result_id=99, - data={'scenario': 'scenario1'}, + data={"scenario": "scenario1"}, ) ] @@ -106,22 +106,22 @@ def test_waive_incomplete_result(): waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", ) ] waived = waive_answers(answers, waivers) expected_json = dict( - type='test-result-missing-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-missing-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", result_id=99, waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', + scenario="scenario1", + source="https://greenwave_tests.example.com", ) assert 1 == len(waived) assert expected_json == waived[0].to_json() @@ -131,11 +131,11 @@ def test_waive_errored_result(): answers = [ TestResultErrored( subject=test_subject(), - test_case_name='test1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + source="https://greenwave_tests.example.com", result_id=99, - data={'scenario': 'scenario1'}, - error_reason='Failed', + data={"scenario": "scenario1"}, + error_reason="Failed", ) ] @@ -145,23 +145,23 @@ def test_waive_errored_result(): waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", ) ] waived = waive_answers(answers, waivers) expected_json = dict( - type='test-result-errored-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-errored-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", result_id=99, waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', - error_reason='Failed', + scenario="scenario1", + source="https://greenwave_tests.example.com", + error_reason="Failed", ) assert 1 == len(waived) assert expected_json == waived[0].to_json() @@ -171,9 +171,9 @@ def test_waive_invalid_gatin_yaml(): answers = [ InvalidRemoteRuleYaml( subject=test_subject(), - test_case_name='invalid-gating-yaml', - source='https://greenwave_tests.example.com', - details='', + test_case_name="invalid-gating-yaml", + source="https://greenwave_tests.example.com", + details="", ) ] @@ -183,10 +183,10 @@ def test_waive_invalid_gatin_yaml(): waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='invalid-gating-yaml', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="invalid-gating-yaml", ) ] waived = waive_answers(answers, waivers) @@ -197,21 +197,21 @@ def test_waive_scenario(): answers = [ TestResultFailed( subject=test_subject(), - test_case_name='test1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + source="https://greenwave_tests.example.com", result_id=99, - data={'scenario': 'scenario1'}, + data={"scenario": "scenario1"}, ) ] waivers = [ dict( id=8, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', - scenario='scenario2' + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", + scenario="scenario2", ) ] waived = waive_answers(answers, waivers) @@ -220,23 +220,23 @@ def test_waive_scenario(): waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', - scenario='scenario1' + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", + scenario="scenario1", ) ] waived = waive_answers(answers, waivers) expected_json = dict( - type='test-result-failed-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-failed-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", result_id=99, waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', + scenario="scenario1", + source="https://greenwave_tests.example.com", ) assert 1 == len(waived) assert expected_json == waived[0].to_json() @@ -246,51 +246,51 @@ def test_waive_scenarios_all(): answers = [ TestResultFailed( subject=test_subject(), - test_case_name='test1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + source="https://greenwave_tests.example.com", result_id=98, - data={'scenario': 'scenario1'}, + data={"scenario": "scenario1"}, ), TestResultFailed( subject=test_subject(), - test_case_name='test1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + source="https://greenwave_tests.example.com", result_id=99, - data={'scenario': 'scenario2'}, - ) + data={"scenario": "scenario2"}, + ), ] waivers = [ dict( id=9, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', - scenario=None + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", + scenario=None, ) ] waived = waive_answers(answers, waivers) expected_json = [ dict( - type='test-result-failed-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-failed-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", result_id=98, waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', + scenario="scenario1", + source="https://greenwave_tests.example.com", ), dict( - type='test-result-failed-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-failed-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", result_id=99, waiver_id=9, - scenario='scenario2', - source='https://greenwave_tests.example.com', + scenario="scenario2", + source="https://greenwave_tests.example.com", ), ] assert expected_json == [w.to_json() for w in waived] @@ -298,13 +298,13 @@ def test_waive_scenarios_all(): def test_waive_with_subject_type_alias(): subject = test_subject() - subject.subject_type.aliases = ['brew-build'] + subject.subject_type.aliases = ["brew-build"] answers = [ TestResultMissing( subject=subject, - test_case_name='test1', - scenario='scenario1', - source='https://greenwave_tests.example.com', + test_case_name="test1", + scenario="scenario1", + source="https://greenwave_tests.example.com", ) ] @@ -314,21 +314,21 @@ def test_waive_with_subject_type_alias(): waivers = [ dict( id=9, - subject_type='brew-build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="brew-build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", ) ] waived = waive_answers(answers, waivers) expected_json = dict( - type='test-result-missing-waived', - testcase='test1', - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', + type="test-result-missing-waived", + testcase="test1", + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", waiver_id=9, - scenario='scenario1', - source='https://greenwave_tests.example.com', + scenario="scenario1", + source="https://greenwave_tests.example.com", ) assert 1 == len(waived) assert expected_json == waived[0].to_json() diff --git a/greenwave/tests/test_waivers_retriever.py b/greenwave/tests/test_waivers_retriever.py index dfed4342..86964603 100644 --- a/greenwave/tests/test_waivers_retriever.py +++ b/greenwave/tests/test_waivers_retriever.py @@ -1,17 +1,17 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock -from typing import Any, Dict +from typing import Any +from unittest import mock from greenwave.resources import WaiversRetriever -_DUMMY_RETRIEVER_ARGUMENTS: Dict[str, Any] = dict( +_DUMMY_RETRIEVER_ARGUMENTS: dict[str, Any] = dict( ignore_ids=[], when=None, url=None, ) -_DUMMY_FILTERS = ['dummy_filter'] +_DUMMY_FILTERS = ["dummy_filter"] def test_waivers_retriever_retrieves_not_ignored_ids(): @@ -20,10 +20,10 @@ def test_waivers_retriever_retrieves_not_ignored_ids(): retriever.ignore_ids = [100] waiver = dict( id=99, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", waived=True, ) retriever._retrieve_data = mock.MagicMock(return_value=[waiver]) @@ -37,10 +37,10 @@ def test_waivers_retriever_ignores_ids(): retriever.ignore_ids = [99] waiver = dict( id=99, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", waived=True, ) retriever._retrieve_data = mock.MagicMock(return_value=[waiver]) @@ -53,10 +53,10 @@ def test_waivers_retriever_ignores_no_waived(): retriever = WaiversRetriever(**_DUMMY_RETRIEVER_ARGUMENTS) waiver = dict( id=99, - subject_type='koji_build', - subject_identifier='nethack-1.2.3-1.rawhide', - product_version='rawhide', - testcase='test1', + subject_type="koji_build", + subject_identifier="nethack-1.2.3-1.rawhide", + product_version="rawhide", + testcase="test1", waived=False, ) retriever._retrieve_data = mock.MagicMock(return_value=[waiver]) diff --git a/greenwave/tests/test_xmlrpc_server_proxy.py b/greenwave/tests/test_xmlrpc_server_proxy.py index 02acf692..7a0d77b9 100644 --- a/greenwave/tests/test_xmlrpc_server_proxy.py +++ b/greenwave/tests/test_xmlrpc_server_proxy.py @@ -1,21 +1,22 @@ # SPDX-License-Identifier: GPL-2.0+ -import mock +from unittest import mock + import pytest from greenwave import xmlrpc_server_proxy @pytest.mark.parametrize( - 'url, expected_transport, timeout, expected_timeout', + "url, expected_transport, timeout, expected_timeout", ( - ('http://localhost:5000/api', xmlrpc_server_proxy.Transport, 15, 15), - ('https://localhost:5000/api', xmlrpc_server_proxy.SafeTransport, 15, 15), - ('https://localhost:5000/api', xmlrpc_server_proxy.SafeTransport, (3, 12), 12), + ("http://localhost:5000/api", xmlrpc_server_proxy.Transport, 15, 15), + ("https://localhost:5000/api", xmlrpc_server_proxy.SafeTransport, 15, 15), + ("https://localhost:5000/api", xmlrpc_server_proxy.SafeTransport, (3, 12), 12), ), ) -@mock.patch('greenwave.xmlrpc_server_proxy.Transport') -@mock.patch('greenwave.xmlrpc_server_proxy.SafeTransport') +@mock.patch("greenwave.xmlrpc_server_proxy.Transport") +@mock.patch("greenwave.xmlrpc_server_proxy.SafeTransport") def test_get_server_proxy( mock_safe_transport, mock_transport, diff --git a/greenwave/tracing.py b/greenwave/tracing.py index 99fbc958..59446700 100644 --- a/greenwave/tracing.py +++ b/greenwave/tracing.py @@ -1,11 +1,13 @@ # SPDX-License-Identifier: GPL-2.0+ from opentelemetry import trace -from opentelemetry.sdk.resources import Resource, SERVICE_NAME +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.instrumentation.flask import FlaskInstrumentor def init_tracing(app): diff --git a/greenwave/utils.py b/greenwave/utils.py index ecacc478..b3160423 100644 --- a/greenwave/utils.py +++ b/greenwave/utils.py @@ -1,14 +1,14 @@ # SPDX-License-Identifier: GPL-2.0+ +import datetime import functools +import hashlib import logging import os -import hashlib -import datetime -from flask import jsonify, current_app, request -from flask.config import Config import requests +from flask import current_app, jsonify, request +from flask.config import Config from werkzeug.exceptions import HTTPException log = logging.getLogger(__name__) @@ -26,18 +26,18 @@ def json_error(error): if isinstance(error, HTTPException): msg = error.description status_code = error.code - current_app.logger.info('HTTP request failed: %s', error) + current_app.logger.info("HTTP request failed: %s", error) elif isinstance(error, (ConnectionError, requests.ConnectionError)): - current_app.logger.exception('Connection error: %s', error) - msg = 'Error connecting to upstream server: {}'.format(error) + current_app.logger.exception("Connection error: %s", error) + msg = f"Error connecting to upstream server: {error}" status_code = 502 elif isinstance(error, requests.Timeout): - current_app.logger.exception('Timeout error: %s', error) - msg = 'Timeout connecting to upstream server: {}'.format(error) + current_app.logger.exception("Timeout error: %s", error) + msg = f"Timeout connecting to upstream server: {error}" status_code = 504 else: - current_app.logger.exception('Unexpected server error: %s', error) - msg = 'Server encountered unexpected error' + current_app.logger.exception("Unexpected server error: %s", error) + msg = "Server encountered unexpected error" status_code = 500 response = jsonify(message=msg) @@ -50,19 +50,20 @@ def json_error(error): def jsonp(func): """Wraps Jsonified output for JSONP requests.""" + @functools.wraps(func) def wrapped(*args, **kwargs): - callback = request.args.get('callback', False) + callback = request.args.get("callback", False) if callback: resp = func(*args, **kwargs) - resp.set_data('{}({});'.format( - str(callback), - resp.get_data().decode('utf-8') - )) - resp.mimetype = 'application/javascript' + resp.set_data( + "{}({});".format(str(callback), resp.get_data().decode("utf-8")) + ) + resp.mimetype = "application/javascript" return resp else: return func(*args, **kwargs) + return wrapped @@ -75,55 +76,54 @@ def load_config(config_obj=None): # Load default config, then override that with a config file config = Config(__name__) if config_obj is None: - if os.getenv('TEST') == 'true': - config_obj = 'greenwave.config.TestingConfig' - elif os.getenv('DEV') == 'true' or os.getenv('DOCS') == 'true': - config_obj = 'greenwave.config.DevelopmentConfig' + if os.getenv("TEST") == "true": + config_obj = "greenwave.config.TestingConfig" + elif os.getenv("DEV") == "true" or os.getenv("DOCS") == "true": + config_obj = "greenwave.config.DevelopmentConfig" else: - config_obj = 'greenwave.config.ProductionConfig' + config_obj = "greenwave.config.ProductionConfig" - if os.getenv('TEST') == 'true': - default_config_file = os.path.join(os.getcwd(), 'conf', 'settings.py.example') - elif os.getenv('DEV') == 'true': - default_config_file = os.path.join(os.getcwd(), 'conf', 'settings.py') - elif os.getenv('DOCS') == 'true': + if os.getenv("TEST") == "true": + default_config_file = os.path.join(os.getcwd(), "conf", "settings.py.example") + elif os.getenv("DEV") == "true": + default_config_file = os.path.join(os.getcwd(), "conf", "settings.py") + elif os.getenv("DOCS") == "true": default_config_file = os.path.normpath( - os.path.join(os.getcwd(), '..', 'conf', 'settings.py.example') + os.path.join(os.getcwd(), "..", "conf", "settings.py.example") ) else: - default_config_file = '/etc/greenwave/settings.py' + default_config_file = "/etc/greenwave/settings.py" # 1. Load default configuration. log.debug("config: Loading config from %r", config_obj) config.from_object(config_obj) # 2. Override default configuration with environment variables. - if os.environ.get('GREENWAVE_SUBJECT_TYPES_DIR'): - config['SUBJECT_TYPES_DIR'] = os.environ['GREENWAVE_SUBJECT_TYPES_DIR'] + if os.environ.get("GREENWAVE_SUBJECT_TYPES_DIR"): + config["SUBJECT_TYPES_DIR"] = os.environ["GREENWAVE_SUBJECT_TYPES_DIR"] - if os.environ.get('GREENWAVE_POLICIES_DIR'): - config['POLICIES_DIR'] = os.environ['GREENWAVE_POLICIES_DIR'] + if os.environ.get("GREENWAVE_POLICIES_DIR"): + config["POLICIES_DIR"] = os.environ["GREENWAVE_POLICIES_DIR"] # 3. Override default configuration and environment variables with custom config file. - config_file = os.environ.get('GREENWAVE_CONFIG', default_config_file) + config_file = os.environ.get("GREENWAVE_CONFIG", default_config_file) log.debug("config: Extending config with %r", config_file) config.from_pyfile(config_file) - if os.environ.get('SECRET_KEY'): - config['SECRET_KEY'] = os.environ['SECRET_KEY'] + if os.environ.get("SECRET_KEY"): + config["SECRET_KEY"] = os.environ["SECRET_KEY"] return config def insert_headers(response): - """ Insert the CORS headers for the give reponse if there are any + """Insert the CORS headers for the give reponse if there are any configured for the application. """ - if current_app.config.get('CORS_URL'): - response.headers['Access-Control-Allow-Origin'] = \ - current_app.config['CORS_URL'] - response.headers['Access-Control-Allow-Headers'] = 'Content-Type' - response.headers['Access-Control-Allow-Method'] = 'POST, OPTIONS' + if current_app.config.get("CORS_URL"): + response.headers["Access-Control-Allow-Origin"] = current_app.config["CORS_URL"] + response.headers["Access-Control-Allow-Headers"] = "Content-Type" + response.headers["Access-Control-Allow-Method"] = "POST, OPTIONS" return response @@ -133,7 +133,7 @@ def mangle_key(key): Python 3 with str keys (which must be encoded to bytes before passing them to hashlib.sha1()). """ - return hashlib.sha256(key.encode('utf-8')).hexdigest() + return hashlib.sha256(key.encode("utf-8")).hexdigest() def add_to_timestamp(timestamp, **kwargs): @@ -146,17 +146,17 @@ def add_to_timestamp(timestamp, **kwargs): """ delta = datetime.timedelta(**kwargs) - date_format = '%Y-%m-%dT%H:%M:%S.%f' + date_format = "%Y-%m-%dT%H:%M:%S.%f" - if timestamp.endswith(' UTC'): + if timestamp.endswith(" UTC"): # date/time format submitted by resultsdb using fedora-messaging - from_date_format = '%Y-%m-%d %H:%M:%S UTC' + from_date_format = "%Y-%m-%d %H:%M:%S UTC" else: from_date_format = date_format return datetime.datetime.strftime( - datetime.datetime.strptime(timestamp, from_date_format) + delta, - date_format) + datetime.datetime.strptime(timestamp, from_date_format) + delta, date_format + ) def right_before_this_time(timestamp): diff --git a/greenwave/wsgi.py b/greenwave/wsgi.py index 2b75d0e5..07062b28 100644 --- a/greenwave/wsgi.py +++ b/greenwave/wsgi.py @@ -1,8 +1,9 @@ # SPDX-License-Identifier: GPL-2.0+ import logging -from greenwave.logger import init_logging, log_to_stdout + from greenwave.app_factory import create_app +from greenwave.logger import init_logging, log_to_stdout init_logging() log_to_stdout(logging.DEBUG) diff --git a/greenwave/xmlrpc_server_proxy.py b/greenwave/xmlrpc_server_proxy.py index ba9bb1aa..04826b69 100644 --- a/greenwave/xmlrpc_server_proxy.py +++ b/greenwave/xmlrpc_server_proxy.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- # SPDX-License-Identifier: GPL-2.0+ """ Provides an "xmlrpc_client.ServerProxy" object with a timeout on the socket. """ + import urllib.parse from defusedxml.xmlrpc import xmlrpc_client @@ -23,7 +23,7 @@ def get_server_proxy(uri, timeout): a socket timeout set. """ parsed_uri = urllib.parse.urlparse(uri) - if parsed_uri.scheme == 'https': + if parsed_uri.scheme == "https": transport = SafeTransport(timeout=timeout) else: transport = Transport(timeout=timeout) diff --git a/rpmlint-config.py b/rpmlint-config.py index 833daa99..2c9336a4 100644 --- a/rpmlint-config.py +++ b/rpmlint-config.py @@ -1,5 +1,6 @@ from Config import addFilter + # RPMs built from git have no %changelog, the proper ones maintained from dist-git do though -addFilter(r'no-changelogname-tag') +addFilter(r"no-changelogname-tag") # RPMs built from git have no source tarball uploaded to PYPI -addFilter(r'invalid-url') +addFilter(r"invalid-url") diff --git a/run-dev-server.py b/run-dev-server.py index 021b0808..27cc6f0c 100644 --- a/run-dev-server.py +++ b/run-dev-server.py @@ -7,12 +7,12 @@ from greenwave.app_factory import create_app from greenwave.logger import init_logging, log_to_stdout -if __name__ == '__main__': - app = create_app('greenwave.config.DevelopmentConfig') +if __name__ == "__main__": + app = create_app("greenwave.config.DevelopmentConfig") init_logging() log_to_stdout(level=logging.DEBUG) app.run( - host=app.config['HOST'], - port=app.config['PORT'], - debug=app.config['DEBUG'], + host=app.config["HOST"], + port=app.config["PORT"], + debug=app.config["DEBUG"], ) diff --git a/tox.ini b/tox.ini index eb33af76..6cfc5625 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = bandit,lint,mypy,semgrep,py3,docs +envlist = mypy,semgrep,py3,docs requires = poetry @@ -78,23 +78,8 @@ commands= rm -rf _build/ sphinx-build -W -b html -d {envtmpdir}/doctrees . _build/html -[testenv:lint] -deps = - flake8 > 3.0 -commands = - python -m flake8 {posargs} - [testenv:mypy] deps = mypy commands = mypy -p greenwave --install-types --non-interactive --ignore-missing-imports - -[flake8] -show-source = True -max-line-length = 100 -exclude = .git,.tox,dist,*egg,docs,.env,.venv,docker -# E265 block comment should start with '# ' -# W503 line break before binary operator -# W504 line break after binary operator -ignore = E265,W503,W504