diff --git a/cli/objects/failure.py b/cli/objects/failure.py index 82bdb082..e7b12cbb 100644 --- a/cli/objects/failure.py +++ b/cli/objects/failure.py @@ -36,6 +36,7 @@ def __init__( failure_type (str): The failure type failed_test_name (Optional[str], optional): The failed test name. Defaults to None. failed_test_junit_path (Optional[str], optional): The path to the failed test's junit file. Defaults to None. + ignore (bool): Flag to indicate that the failure should be ignored. """ self.logger = get_logger(__name__) diff --git a/cli/objects/failure_rule.py b/cli/objects/failure_rule.py index ff6d8679..41339aae 100644 --- a/cli/objects/failure_rule.py +++ b/cli/objects/failure_rule.py @@ -188,14 +188,9 @@ def _get_ignore(self, rule_dict: dict[Any, Any]) -> bool: ) exit(1) - def matches_failure(self, failure: "Failure") -> bool: - if ( + def matches_failure(self, failure: Failure) -> bool: + return ( hasattr(self, "step") and fnmatch.fnmatch(failure.step, self.step) - and ( - (failure.failure_type == self.failure_type) - or self.failure_type == "all" - ) - ): - return True - return False + and failure.failure_type in (self.failure_type, "all") + ) diff --git a/cli/objects/job.py b/cli/objects/job.py index e4791f9c..93de03f6 100644 --- a/cli/objects/job.py +++ b/cli/objects/job.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import itertools import json import os from typing import Any @@ -324,13 +323,12 @@ def _find_failures(self, logs_dir: str, junit_dir: str) -> Optional[list[Failure unique_steps_with_failures = set() # Combine lists into one list - for failure in itertools.chain(test_failures, pod_failures): + for failure in test_failures + pod_failures: if failure.step not in unique_steps_with_failures: - unique_steps_with_failures.update([failure.step]) - if self.firewatch_config.failure_rules: - for rule in self.firewatch_config.failure_rules: - if rule.matches_failure(failure) and rule.ignore: - failure.ignore = True + unique_steps_with_failures.add(failure.step) + if failure_rules := self.firewatch_config.failure_rules: + for rule in failure_rules: + failure.ignore = rule.matches_failure(failure) and rule.ignore failures_list.append(failure) if len(failures_list) > 0: diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index 58024d04..14da98cb 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Red Hat, Inc. +# Copyright (C) 2024 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ from cli.objects.job import Job from cli.report import Report -_logger = simple_logger.logger.get_logger(__name__) +LOGGER = simple_logger.logger.get_logger(__name__) BUILD_ID_ENV_VAR = "BUILD_ID" FIREWATCH_DEFAULT_JIRA_PROJECT_ENV_VAR = "FIREWATCH_DEFAULT_JIRA_PROJECT" @@ -54,6 +54,8 @@ ["junit_install.xml", "junit_install_status.xml", "junit_symptoms.xml"], ] +DEFAULT_MOCK_ISSUE_KEY = "LPTOCPCI-MOCK" + @pytest.fixture def jira(jira_config_path): @@ -85,18 +87,18 @@ def job(firewatch_config, build_id): def patch_jira(monkeypatch): @dataclass class MockIssue: - key: str = "LPTOCPCI-MOCK" + key: str = DEFAULT_MOCK_ISSUE_KEY def create_jira_issue(*args, **kwargs): - _logger.info("Patching Report.create_jira_issue") - _logger.info( + LOGGER.info("Patching Report.create_jira_issue") + LOGGER.info( f"Attempted call Report.create_issue with the following keywords: \n{pprint.pformat(kwargs)}", ) return MockIssue() def add_duplicate_comment(*args, **kwargs): - _logger.info("Patching Report.add_duplicate_comment") - _logger.info( + LOGGER.info("Patching Report.add_duplicate_comment") + LOGGER.info( f"Attempted to call Report.add_duplicate_comment with the following keywords: \n{pprint.pformat(kwargs)}", ) return @@ -128,12 +130,12 @@ def __post_init__(self): cap = CapJira() def create_jira_issue(*args, **kwargs): - _logger.info("Patching Report.create_jira_issue") + LOGGER.info("Patching Report.create_jira_issue") cap.create_jira_issue.append(CapInputs(args, kwargs)) return MockIssue() def add_duplicate_comment(*args, **kwargs): - _logger.info("Patching Report.add_duplicate_comment") + LOGGER.info("Patching Report.add_duplicate_comment") cap.add_duplicate_comment.append(CapInputs(args, kwargs)) monkeypatch.setattr(Report, "create_jira_issue", create_jira_issue) @@ -143,7 +145,7 @@ def add_duplicate_comment(*args, **kwargs): @pytest.fixture def patch_job_log_dir(monkeypatch, job_log_dir): - _logger.info("Patching Job log dir path") + LOGGER.info("Patching Job log dir path") def _download_logs(*args, **kwargs): return job_log_dir.as_posix() @@ -152,36 +154,98 @@ def _download_logs(*args, **kwargs): @pytest.fixture -def patch_job_junit_dir(monkeypatch, job_junit_dir): - _logger.info("Patching Job junit dir path") +def patch_job_junit_dir(monkeypatch, job_artifacts_dir): + LOGGER.info("Patching Job junit dir path") def _download_junit(*args, **kwargs): - return job_junit_dir.as_posix() + return job_artifacts_dir.as_posix() monkeypatch.setattr(Job, "_download_junit", _download_junit) @pytest.fixture -def assert_job_dir_exists(job_dir): - job_dir.mkdir(exist_ok=True, parents=True) - assert job_dir.is_dir() +def fake_log_secret_path(job_log_dir): + yield job_log_dir.joinpath("fake-step/fake_build_log.txt") @pytest.fixture -def fake_log_secret_path(job_log_dir): - yield job_log_dir.joinpath("fake-step/fake_build_log.txt") +def fake_junit_secret_path(job_artifacts_dir): + yield job_artifacts_dir.joinpath("fake-step/fake_junit.xml") @pytest.fixture -def fake_junit_secret_path(job_junit_dir): - yield job_junit_dir.joinpath("fake-step/fake_junit.xml") +def firewatch_config_json(monkeypatch): + config_json = json.dumps( + { + "failure_rules": [ + { + "step": "exact-step-name", + "failure_type": "pod_failure", + "classification": "Infrastructure", + "jira_project": "!default", + "jira_component": ["some-component"], + "jira_assignee": "some-user@redhat.com", + "jira_security_level": "Restricted", + }, + { + "step": "*partial-name*", + "failure_type": "all", + "classification": "Misc.", + "jira_project": "OTHER", + "jira_component": ["component-1", "component-2", "!default"], + "jira_priority": "major", + "group": {"name": "some-group", "priority": 1}, + }, + { + "step": "*ends-with-this", + "failure_type": "test_failure", + "classification": "Test failures", + "jira_epic": "!default", + "jira_additional_labels": [ + "test-label-1", + "test-label-2", + "!default", + ], + "group": {"name": "some-group", "priority": 2}, + }, + { + "step": "*ignore*", + "failure_type": "test_failure", + "classification": "NONE", + "jira_project": "NONE", + "ignore": "true", + }, + { + "step": "affects-version", + "failure_type": "all", + "classification": "Affects Version", + "jira_project": "TEST", + "jira_epic": "!default", + "jira_affects_version": "4.14", + "jira_assignee": "!default", + }, + { + "step": "affects-version", + "failure_type": "all", + "classification": "Affects Version", + "jira_project": "TEST", + "jira_epic": "!default", + "jira_affects_version": "4.14", + "jira_assignee": "!default", + }, + ], + }, + ) + monkeypatch.setenv(FIREWATCH_CONFIG_ENV_VAR, config_json) + yield config_json @pytest.fixture -def assert_jira_config_file_exists(jira_config_path): - if not jira_config_path.is_file(): - jira_config_path.parent.mkdir(exist_ok=True, parents=True) - jira_config_path.write_text( +def jira_config_path(tmp_path): + config_path = tmp_path.joinpath("jira_config.json") + if not config_path.is_file(): + config_path.parent.mkdir(exist_ok=True, parents=True) + config_path.write_text( json.dumps( { "token": os.getenv(JIRA_TOKEN_ENV_VAR), @@ -193,87 +257,7 @@ def assert_jira_config_file_exists(jira_config_path): }, ), ) - assert jira_config_path.is_file() - - -@pytest.fixture -def assert_firewatch_config_in_env(monkeypatch): - monkeypatch.setenv( - FIREWATCH_CONFIG_ENV_VAR, - json.dumps( - { - "failure_rules": [ - { - "step": "exact-step-name", - "failure_type": "pod_failure", - "classification": "Infrastructure", - "jira_project": "!default", - "jira_component": ["some-component"], - "jira_assignee": "some-user@redhat.com", - "jira_security_level": "Restricted", - }, - { - "step": "*partial-name*", - "failure_type": "all", - "classification": "Misc.", - "jira_project": "OTHER", - "jira_component": ["component-1", "component-2", "!default"], - "jira_priority": "major", - "group": {"name": "some-group", "priority": 1}, - }, - { - "step": "*ends-with-this", - "failure_type": "test_failure", - "classification": "Test failures", - "jira_epic": "!default", - "jira_additional_labels": [ - "test-label-1", - "test-label-2", - "!default", - ], - "group": {"name": "some-group", "priority": 2}, - }, - { - "step": "*ignore*", - "failure_type": "test_failure", - "classification": "NONE", - "jira_project": "NONE", - "ignore": "true", - }, - { - "step": "affects-version", - "failure_type": "all", - "classification": "Affects Version", - "jira_project": "TEST", - "jira_epic": "!default", - "jira_affects_version": "4.14", - "jira_assignee": "!default", - }, - { - "step": "affects-version", - "failure_type": "all", - "classification": "Affects Version", - "jira_project": "TEST", - "jira_epic": "!default", - "jira_affects_version": "4.14", - "jira_assignee": "!default", - }, - ], - }, - ), - ) - assert os.getenv(FIREWATCH_CONFIG_ENV_VAR) - - -@pytest.fixture -def assert_artifact_dir_exists(artifact_dir): - artifact_dir.mkdir(exist_ok=True, parents=True) - assert artifact_dir.is_dir() - - -@pytest.fixture -def jira_config_path(tmp_path): - yield tmp_path.joinpath("jira_config.json") + yield config_path @pytest.fixture(params=JOB_STEP_DIRS_TO_TEST) @@ -282,8 +266,8 @@ def job_log_step_dirs(request, job_log_dir): @pytest.fixture(params=JOB_STEP_DIRS_TO_TEST) -def job_junit_step_dirs(request, job_junit_dir): - yield (job_junit_dir / p for p in request.param) +def job_junit_step_dirs(request, job_artifacts_dir): + yield (job_artifacts_dir / p for p in request.param) @pytest.fixture @@ -292,7 +276,7 @@ def job_log_dir(job_dir): @pytest.fixture -def job_junit_dir(job_dir): +def job_artifacts_dir(job_dir): yield job_dir / "artifacts" @@ -307,14 +291,11 @@ def job_dir_junit_artifact_paths(request, job_junit_step_dirs): @pytest.fixture -def assert_artifact_dir_in_env(monkeypatch, artifact_dir): - monkeypatch.setenv(ARTIFACT_DIR_ENV_VAR, artifact_dir.as_posix()) - assert os.getenv(ARTIFACT_DIR_ENV_VAR) - - -@pytest.fixture -def artifact_dir(tmp_path): - yield tmp_path / "artifacts" +def artifact_dir(monkeypatch, tmp_path): + path = tmp_path / "artifacts" + monkeypatch.setenv(ARTIFACT_DIR_ENV_VAR, path.as_posix()) + path.mkdir(exist_ok=True, parents=True) + yield path @pytest.fixture @@ -323,38 +304,28 @@ def job_dir(tmp_path, build_id): @pytest.fixture(params=BUILD_IDS_TO_TEST, ids=BUILD_IDS_TO_TEST) -def build_id(request): - yield request.param +def build_id(monkeypatch, request): + param = request.param + monkeypatch.setenv(BUILD_ID_ENV_VAR, param) + yield param -@pytest.fixture(params=DEFAULT_JIRA_PROJECTS_TO_TEST, ids=DEFAULT_JIRA_PROJECTS_TO_TEST) -def default_jira_project(request): - yield request.param +@pytest.fixture(params=DEFAULT_JIRA_PROJECTS_TO_TEST) +def default_jira_project(monkeypatch, request): + param = request.param + monkeypatch.setenv(FIREWATCH_DEFAULT_JIRA_PROJECT_ENV_VAR, param) + yield param @pytest.fixture(params=DEFAULT_JIRA_EPICS_TO_TEST, ids=DEFAULT_JIRA_EPICS_TO_TEST) -def default_jira_epic(request): - yield request.param - - -@pytest.fixture -def assert_build_id_in_env(build_id, monkeypatch): - monkeypatch.setenv(BUILD_ID_ENV_VAR, build_id) - assert os.getenv(BUILD_ID_ENV_VAR) - - -@pytest.fixture -def assert_default_jira_project_in_env(default_jira_project, monkeypatch): - monkeypatch.setenv(FIREWATCH_DEFAULT_JIRA_PROJECT_ENV_VAR, default_jira_project) - assert os.getenv(FIREWATCH_DEFAULT_JIRA_PROJECT_ENV_VAR) - - -@pytest.fixture -def assert_default_jira_epic_in_env(default_jira_epic, monkeypatch): - monkeypatch.setenv(FIREWATCH_DEFAULT_JIRA_EPIC_ENV_VAR, default_jira_epic) - assert os.getenv(FIREWATCH_DEFAULT_JIRA_EPIC_ENV_VAR) +def default_jira_epic(monkeypatch, request): + param = request.param + monkeypatch.setenv(FIREWATCH_DEFAULT_JIRA_EPIC_ENV_VAR, param) + yield param @pytest.fixture -def assert_jira_token_in_env(): - assert os.getenv(JIRA_TOKEN_ENV_VAR) +def jira_token(): + token = os.getenv(JIRA_TOKEN_ENV_VAR) + assert token + yield token diff --git a/tests/unittests/objects/failure_rule/test_firewatch_objects_failure_rule_matches_failure.py b/tests/unittests/objects/failure_rule/test_firewatch_objects_failure_rule_matches_failure.py index b8cfdf0d..7bee003a 100644 --- a/tests/unittests/objects/failure_rule/test_firewatch_objects_failure_rule_matches_failure.py +++ b/tests/unittests/objects/failure_rule/test_firewatch_objects_failure_rule_matches_failure.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Red Hat, Inc. +# Copyright (C) 2024 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,29 +19,21 @@ from cli.objects.failure_rule import FailureRule -@pytest.fixture(autouse=True) -def setup_tests(assert_default_jira_project_in_env): - ... - - -@pytest.fixture -def rule_dict(): - yield { - "step": "gather-*", - "failure_type": "test_failure", - "classification": "NONE", - "jira_project": "NONE", - "ignore": "false", - } - - @pytest.fixture -def failure_rule(rule_dict): - yield FailureRule(rule_dict=rule_dict) +def failure_rule(): + yield FailureRule( + rule_dict={ + "step": "gather-*", + "failure_type": "test_failure", + "classification": "NONE", + "jira_project": "NONE", + "ignore": "false", + }, + ) @pytest.fixture -def matching_failure(): +def failure(): yield Failure( failure_type="test_failure", failed_test_name="post", @@ -49,9 +41,33 @@ def matching_failure(): ) -def test_init_failure_rule_from_fixtures(failure_rule): - assert isinstance(failure_rule, FailureRule) +def test_failure_rule_matches_failure_if_rule_pattern_equals_failure_step( + failure, + failure_rule, +): + failure_rule.step = failure.step = "gather-must-gather" + assert failure_rule.matches_failure( + failure, + ), f"'{failure_rule.step}' should match '{failure.step}'" + + +def test_failure_rule_matches_failure_if_glob_style_rule_pattern_matches_failure_step( + failure, + failure_rule, +): + failure.step = "gather-must-gather" + failure_rule.step = "gather-*" + assert failure_rule.matches_failure( + failure, + ), f"'{failure_rule.step}' should match '{failure.step}'" -def test_failure_rule_matches_failure(failure_rule, matching_failure): - assert failure_rule.matches_failure(matching_failure) +def test_failure_rule_does_not_match_non_glob_unequal_failure_step( + failure, + failure_rule, +): + failure.step = "gather-must-gather" + failure_rule.step = "firewatch_report_issues" + assert not failure_rule.matches_failure( + failure, + ), f"'{failure_rule.step}' should NOT match '{failure.step}'" diff --git a/tests/unittests/objects/fixtures/__init__.py b/tests/unittests/objects/fixtures/__init__.py deleted file mode 100644 index b8c15d27..00000000 --- a/tests/unittests/objects/fixtures/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2023 Red Hat, Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# diff --git a/tests/unittests/objects/fixtures/test_fixtures.py b/tests/unittests/objects/fixtures/test_fixtures.py deleted file mode 100644 index fcec2c8b..00000000 --- a/tests/unittests/objects/fixtures/test_fixtures.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2023 Red Hat, Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -import pytest - -from cli.objects.configuration import Configuration -from cli.objects.jira_base import Jira -from cli.objects.job import Job - - -@pytest.fixture(autouse=True) -def setup_test_environment( - assert_jira_token_in_env, - assert_jira_config_file_exists, - assert_default_jira_project_in_env, - assert_firewatch_config_in_env, - patch_job_junit_dir, - patch_job_log_dir, - assert_build_id_in_env, - assert_artifact_dir_exists, - assert_artifact_dir_in_env, - assert_job_dir_exists, -): - ... - - -def test_get_firewatch_config_instance_from_fixture(firewatch_config): - assert isinstance(firewatch_config, Configuration) - - -def test_get_job_instance_from_fixture(job): - assert isinstance(job, Job) - - -def test_get_jira_instance_from_fixture(jira): - assert isinstance(jira, Jira) diff --git a/tests/unittests/objects/job/test_fail_with_test_failures_should_not_cause_failure_for_ignored_step.py b/tests/unittests/objects/job/test_fail_with_test_failures_should_not_cause_failure_for_ignored_step.py index 52133dc1..a3e75653 100644 --- a/tests/unittests/objects/job/test_fail_with_test_failures_should_not_cause_failure_for_ignored_step.py +++ b/tests/unittests/objects/job/test_fail_with_test_failures_should_not_cause_failure_for_ignored_step.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Red Hat, Inc. +# Copyright (C) 2024 Red Hat, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,18 +20,13 @@ from cli.objects.configuration import Configuration from cli.objects.job import Job +from conftest import FIREWATCH_CONFIG_ENV_VAR -@pytest.fixture(autouse=True) -def setup_tests( - monkeypatch, - assert_jira_config_file_exists, - assert_default_jira_project_in_env, - patch_job_log_dir, - patch_job_junit_dir, -): +@pytest.fixture +def firewatch_config(monkeypatch, jira, default_jira_project): monkeypatch.setenv( - "FIREWATCH_CONFIG", + FIREWATCH_CONFIG_ENV_VAR, json.dumps( { "failure_rules": [ @@ -46,10 +41,6 @@ def setup_tests( }, ), ) - - -@pytest.fixture -def config(jira): yield Configuration( jira=jira, fail_with_test_failures=True, @@ -59,8 +50,8 @@ def config(jira): @pytest.fixture -def job(config, job_junit_dir): - gather_must_gather_dir = job_junit_dir / "gather-must-gather" +def job(firewatch_config, patch_job_log_dir, patch_job_junit_dir, job_artifacts_dir): + gather_must_gather_dir = job_artifacts_dir / "gather-must-gather" gather_must_gather_dir.mkdir(exist_ok=True, parents=True) (gather_must_gather_dir / "finished.json").write_text( '{"timestamp":170340000,"passed":false,"result":"FAILURE","revision":"release-v1.11"}', @@ -70,16 +61,16 @@ def job(config, job_junit_dir): name_safe="mtr-interop-aws", build_id="1739165508839673856", gcs_bucket="test-platform-results", - firewatch_config=config, + firewatch_config=firewatch_config, ) -def test_init_job_from_fixtures(job): - assert isinstance(job, Job) - - -def test_fail_with_test_failures_should_not_cause_failure_for_ignored_step(config, job): - rule = config.failure_rules[0] +def test_fail_with_test_failures_should_not_cause_failure_for_ignored_step( + monkeypatch, + firewatch_config, + job, +): + rule = firewatch_config.failure_rules[0] assert rule.step == "gather-*" assert rule.ignore assert re.match(rule.step, "gather-must-gather")