diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index bf8cc64..09461c8 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -19,6 +19,6 @@ jobs: registry_username: ${{ secrets.QUAY_IMAGE_SCLORG_BUILDER_USERNAME }} registry_token: ${{ secrets.QUAY_IMAGE_SCLORG_BUILDER_TOKEN }} dockerfile: Dockerfile.daily-tests - tag: "0.8.7" + tag: "0.8.8" image_name: "upstream-daily-tests" quay_application_token: ${{ secrets.QUAY_IMAGE_SCLORG_UPDATE_DESC }} diff --git a/Dockerfile.daily-tests b/Dockerfile.daily-tests index ac61d5b..e6c451e 100644 --- a/Dockerfile.daily-tests +++ b/Dockerfile.daily-tests @@ -2,7 +2,7 @@ FROM quay.io/fedora/fedora:42 ENV SHARED_DIR="/var/ci-scripts" \ VERSION="42" \ - RELEASE_UPSTREAM="0.8.7" \ + RELEASE_UPSTREAM="0.8.8" \ UPSTREAM_TMT_REPO="https://github.com/sclorg/sclorg-testing-farm" \ UPSTREAM_TMT_DIR="sclorg-testing-farm" \ HOME="/home/nightly" \ diff --git a/Makefile b/Makefile index 14788ad..b10fd97 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ shellcheck: ./run-shellcheck.sh `git ls-files *.sh` build_images: - podman build -t quay.io/sclorg/upstream-daily-tests:0.8.7 -f Dockerfile.daily-tests . + podman build -t quay.io/sclorg/upstream-daily-tests:0.8.8 -f Dockerfile.daily-tests . diff --git a/daily_tests/daily_nightly_tests_report.py b/daily_tests/daily_nightly_tests_report.py index 13abf0e..67e4062 100755 --- a/daily_tests/daily_nightly_tests_report.py +++ b/daily_tests/daily_nightly_tests_report.py @@ -127,6 +127,9 @@ def __init__(self): self.available_test_case = TEST_CASES def parse_args(self): + """ + Parse command line arguments. + """ parser = argparse.ArgumentParser( description="NightlyTestsReport program report all failures" "over all OS and Tests (tests, test-pytest, test-openshift-pytest)." @@ -150,11 +153,17 @@ def parse_args(self): return parser.parse_args() def return_plan_name(self, item) -> str: + """ + Return the plan name for a given item. + """ return "".join( [x[1] for x in self.available_test_case if item.startswith(x[0])] ) def load_mails_from_environment(self): + """ + Load email addresses from environment variables. + """ print(os.environ) if "DB_MAILS" in os.environ: SCLORG_MAILS["mariadb-container"] = os.environ["DB_MAILS"].split(",") @@ -186,6 +195,12 @@ def load_mails_from_environment(self): print(f"Send email: '{self.send_email}'") def send_file_to_pastebin(self, log_path, log_name: Path): + """ + Send file to pastebin using send_to_paste_bin.sh script. + It is used for sending logs from TMT command in case of TMT failures. + + :param log_path: Path to log file to send + :param log_name: Path to file where pastebin link will be stored""" if not os.path.exists(log_path): return cmd = f'{SEND_PASTE_BIN} "{log_path}" "{str(log_name)}"' @@ -200,6 +215,11 @@ def send_file_to_pastebin(self, log_path, log_name: Path): time.sleep(1) def get_pastebin_url(self, log_name: str) -> str: + """ + Get pastebin URL from file where send_to_paste_bin.sh + script stored it after sending logs to pastebin. + :param log_name: Path to file where pastebin link is stored + :return: URL as string or empty string if URL is not found in file""" with open(log_name, "r") as f: lines = f.read() @@ -210,36 +230,51 @@ def get_pastebin_url(self, log_name: str) -> str: return "" def store_tmt_logs_to_dict( - self, path_dir: Path, test_case, is_running=False, not_exists=False + self, + path_dir: Path, + test_case, + is_running=False, + not_exists=False, + is_failed=False, ): + """ + Store TMT logs in a dictionary for reporting purposes. + :param path_dir: Path to the directory containing TMT logs + :param test_case: Name of the test case + :param is_running: Flag indicating if the test is still running + :param not_exists: Flag indicating if the data directory does not exist + :param is_failed: Flag indicating if the test has failed + """ + log_path = self.reports_dir / test_case / "tmt-verbose-log" + log_name = path_dir / "tmt-verbose-log.txt" + self.send_file_to_pastebin(log_path=log_path, log_name=log_name) + + if not (is_running or is_failed or not_exists): + return if not_exists: msg = ( f"Data dir for test case {test_case} does not exist." f"Look at log in attachment called '{test_case}-log.txt'." ) - else: - if is_running: - msg = ( - f"tmt tests for case {test_case} is still running." - f"Look at log in attachment called '{test_case}-log.txt'." - ) - else: - msg = ( - f"tmt command has failed for test case {test_case}." - f"Look at log in attachment called '{test_case}-log.txt'." - ) + if is_running: + msg = ( + f"tmt tests for case {test_case} is still running." + f"Look at log in attachment called '{test_case}-log.txt'." + ) + if is_failed: + msg = ( + f"tmt command has failed for test case {test_case}." + f"Look at log in attachment called '{test_case}-log.txt'." + ) self.data_dict["tmt"]["msg"].append(msg) if is_running: dictionary_key = "tmt_running" - else: + if is_failed: dictionary_key = "tmt_failed" - self.data_dict["tmt"][dictionary_key].append(test_case) - log_path = self.reports_dir / test_case / "tmt-verbose-log" - log_name = path_dir / "tmt-verbose-log.txt" - self.send_file_to_pastebin(log_path=log_path, log_name=log_name) if log_name.exists(): with open(log_name) as f: print(f.readlines()) + self.data_dict["tmt"][dictionary_key].append(test_case) self.data_dict["tmt"]["logs"].append((test_case, log_path, log_name)) def collect_data(self): @@ -277,7 +312,7 @@ def collect_data(self): # /var/tmp/daily_scl_tests//log.txt file if (path_dir / "tmt_failed").exists(): print(f"tmt command has failed for test case {test_case}.") - self.store_tmt_logs_to_dict(path_dir, test_case) + self.store_tmt_logs_to_dict(path_dir, test_case, is_failed=True) failed_tests = True continue data_dir = path_dir / "results" @@ -293,7 +328,7 @@ def collect_data(self): print(success_logs) for suc in success_logs: self.store_tmt_logs_to_dict( - path_dir=path_dir, test_case=test_case + path_dir=path_dir, test_case=test_case, is_failed=False ) self.data_dict["SUCCESS_DATA"].extend( [(test_case, str(f), str(f.name)) for f in success_logs] @@ -313,6 +348,11 @@ def collect_data(self): pprint.pprint(self.data_dict) def generate_email_body(self): + """ + Generate email body based on collected data. + It contains information about failed containers + and logs from TMT command in case of TMT failures. + """ if self.args.upstream_tests: body_failure = "NodeJS upstream tests failures:
" body_success = ( @@ -346,6 +386,9 @@ def generate_email_body(self): print(f"Body to email: {self.body}") def generate_failed_containers(self): + """ + Generate email body for failed containers. + """ print("GENERATE FAILED CONTAINERS") for test_case, plan, msg in self.available_test_case: if test_case not in self.data_dict: @@ -365,6 +408,9 @@ def generate_failed_containers(self): ) def generate_success_containers(self): + """ + Generate email body for successful containers. + """ print("GENERATE SUCCESS CONTAINERS") for test_case, cont_path, log_name in self.data_dict["SUCCESS_DATA"]: if os.path.exists(log_name): @@ -374,6 +420,9 @@ def generate_success_containers(self): ) def generate_tmt_logs_containers(self): + """ + Generate email body for TMT logs. + """ for test_case, cont_path, log_name in self.data_dict["tmt"]["logs"]: print(f"generate_tmt_logs_containers: {test_case}, {cont_path}, {log_name}") if os.path.exists(log_name): @@ -390,6 +439,9 @@ def generate_tmt_logs_containers(self): self.body += "
" def generate_emails(self): + """ + Generate email list based on collected data and predefined email lists for each container. + """ print("generate_emails: ", self.data_dict) for test_case, plan, _ in self.available_test_case: if test_case not in self.data_dict: @@ -405,6 +457,9 @@ def generate_emails(self): print(f"generate_emails: Additional emails: {DEFAULT_MAILS}") def send_emails(self): + """ + Send emails with the test results. + """ if not self.send_email: print("Sending email is not allowed") return diff --git a/daily_tests/tests/test_tf_log_downloader.py b/daily_tests/tests/test_tf_log_downloader.py new file mode 100644 index 0000000..ad40037 --- /dev/null +++ b/daily_tests/tests/test_tf_log_downloader.py @@ -0,0 +1,315 @@ +# pylint: disable=import-error,redefined-outer-name +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +import download_logs + +TEST_DIR = Path(__file__).parent.absolute() +sys.path.insert(0, str(TEST_DIR.parent)) + + +@pytest.fixture +def tmp_log_dir(tmp_path): + """Set up SHARED_DIR and return the path for test isolation.""" + with patch.dict("os.environ", {"SHARED_DIR": str(tmp_path)}): + with patch.object(download_logs, "LOG_DIR", str(tmp_path)): + yield tmp_path + + +@pytest.fixture +def log_file_with_request_id(tmp_path): + """Create a log file containing a Testing Farm request ID.""" + log_file = tmp_path / "test.log" + log_file.write_text( + "some line\n" + "api https://api.dev.testing-farm.io/v0.1/requests/abc-123-def/status\n" + "another line\n" + ) + return str(log_file) + + +@pytest.fixture +def log_file_without_request_id(tmp_path): + """Create a log file without a request ID.""" + log_file = tmp_path / "test.log" + log_file.write_text("some line\nno request id here\n") + return str(log_file) + + +@pytest.fixture +def downloader(tmp_log_dir, log_file_with_request_id): + """Create a TestingFarmLogDownloader instance with valid log file.""" + return download_logs.TestingFarmLogDownloader( + log_file_with_request_id, "fedora", "test-pytest" + ) + + +class TestTestingFarmLogDownloaderInit: + """Tests for TestingFarmLogDownloader.__init__.""" + + def test_init_sets_attributes(self, tmp_log_dir, log_file_with_request_id): + downloader = download_logs.TestingFarmLogDownloader( + log_file_with_request_id, "c9s", "test" + ) + assert downloader.log_file == Path(log_file_with_request_id) + assert downloader.target == "c9s" + assert downloader.test == "test" + assert downloader.request_id is None + assert downloader.xml_dict is None + assert downloader.data_dir_url_link is None + assert "daily_reports_dir" in str(downloader.log_dir) + assert "c9s-test" in str(downloader.log_dir) + + +class TestGetRequestId: + """Tests for TestingFarmLogDownloader.get_request_id.""" + + def test_get_request_id_success(self, downloader, capsys): + result = downloader.get_request_id() + assert result is True + assert downloader.request_id == "abc-123-def" + assert "Request ID: abc-123-def" in capsys.readouterr().out + + def test_get_request_id_failure( + self, tmp_log_dir, log_file_without_request_id, capsys + ): + downloader = download_logs.TestingFarmLogDownloader( + log_file_without_request_id, "fedora", "test" + ) + result = downloader.get_request_id() + assert result is False + assert downloader.request_id is None + assert "Request ID not found" in capsys.readouterr().out + + +class TestDownloadLog: + """Tests for TestingFarmLogDownloader.download_log.""" + + def test_download_log_success(self, downloader, tmp_path): + downloader.log_dir = tmp_path + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b"log content" + + with patch("download_logs.requests.get", return_value=mock_response): + result = downloader.download_log("http://example.com/log.txt", "log.txt") + + assert result is True + assert (tmp_path / "log.txt").read_bytes() == b"log content" + + def test_download_log_success_failed_dir(self, downloader, tmp_path): + (tmp_path / "results").mkdir() + downloader.log_dir = tmp_path + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b"failed log" + + with patch("download_logs.requests.get", return_value=mock_response): + result = downloader.download_log( + "http://example.com/fail.log", "fail.log", is_failed=True + ) + + assert result is True + assert (tmp_path / "results" / "fail.log").read_bytes() == b"failed log" + + def test_download_log_failure_after_retries(self, downloader, tmp_path, capsys): + downloader.log_dir = tmp_path + mock_response = MagicMock() + mock_response.status_code = 404 + + with patch("download_logs.requests.get", return_value=mock_response): + with patch("download_logs.time.sleep"): + result = downloader.download_log( + "http://example.com/missing.log", "missing.log" + ) + + assert result is False + assert "Failed to download log" in capsys.readouterr().out + + +class TestDownloadTmtLogs: + """Tests for TestingFarmLogDownloader.download_tmt_logs.""" + + def test_download_tmt_logs_no_xml_dict(self, downloader, capsys): + downloader.xml_dict = None + result = downloader.download_tmt_logs() + assert result is False + assert "XML report not found" in capsys.readouterr().out + + def test_download_tmt_logs_downloads_logs_and_sets_data_link( + self, downloader, tmp_path + ): + downloader.log_dir = tmp_path + downloader.xml_dict = { + "testsuites": { + "testsuite": { + "logs": { + "log": [ + {"@name": "tmt-log", "@href": "http://example.com/tmt.log"}, + { + "@name": "tmt-verbose-log", + "@href": "http://example.com/tmt-verbose.log", + }, + {"@name": "data", "@href": "http://example.com/data"}, + ] + } + } + } + } + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b"log content" + + with patch("download_logs.requests.get", return_value=mock_response): + downloader.download_tmt_logs() + + assert downloader.data_dir_url_link == "http://example.com/data" + assert (tmp_path / "tmt-log").read_bytes() == b"log content" + assert (tmp_path / "tmt-verbose-log").read_bytes() == b"log content" + + +class TestGetListOfContainersLogs: + """Tests for TestingFarmLogDownloader.get_list_of_containers_logs.""" + + def test_get_list_of_containers_logs_finds_links(self, downloader): + html = '\n\n' + result = downloader.get_list_of_containers_logs(html) + assert result == ['', ''] + + def test_get_list_of_containers_logs_empty(self, downloader): + result = downloader.get_list_of_containers_logs("no links here") + assert result == [] + + def test_get_list_of_containers_logs_exception(self, downloader, capsys): + with patch.object(download_logs, "re") as mock_re: + mock_re.search.side_effect = Exception("regex error") + result = downloader.get_list_of_containers_logs("html") + assert result is False + assert "Failed to get list of failed containers" in capsys.readouterr().out + + +class TestDownloadContainerLogs: + """Tests for TestingFarmLogDownloader.download_container_logs.""" + + def test_download_container_logs_no_data_link(self, downloader, capsys): + downloader.data_dir_url_link = None + result = downloader.download_container_logs() + assert result is False + assert "Data directory URL link not found" in capsys.readouterr().out + + def test_download_container_logs_success(self, downloader, tmp_path): + downloader.log_dir = tmp_path + downloader.data_dir_url_link = "http://example.com/data" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = "data dir" + mock_response.content = b"log" + + with patch("download_logs.requests.get", return_value=mock_response): + result = downloader.download_container_logs() + + assert result is True + + def test_download_container_logs_failed_directory(self, downloader, tmp_path): + (tmp_path / "results").mkdir() + downloader.log_dir = tmp_path + downloader.data_dir_url_link = "http://example.com/data" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = "results" + mock_response.content = b"log" + + with patch("download_logs.requests.get", return_value=mock_response): + result = downloader.download_container_logs(is_failed=True) + + assert result is True + + def test_download_container_logs_http_error(self, downloader, capsys): + downloader.data_dir_url_link = "http://example.com/data" + mock_response = MagicMock() + mock_response.status_code = 404 + + with patch("download_logs.requests.get", return_value=mock_response): + result = downloader.download_container_logs() + + assert result is False + assert "Failed to download data" in capsys.readouterr().out + + +class TestGetXmlReport: + """Tests for TestingFarmLogDownloader.get_xml_report.""" + + def test_get_xml_report_public_url_for_fedora(self, downloader, capsys): + downloader.request_id = "req-123" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b"" + + with patch("download_logs.requests.get", return_value=mock_response): + with patch( + "download_logs.xmltodict.parse", return_value={"testsuites": {}} + ): + result = downloader.get_xml_report() + + assert result is True + assert downloader.xml_dict == {"testsuites": {}} + assert "artifacts.dev.testing-farm.io" in capsys.readouterr().out + + def test_get_xml_report_public_url_for_c9s( + self, tmp_log_dir, log_file_with_request_id + ): + downloader = download_logs.TestingFarmLogDownloader( + log_file_with_request_id, "c9s", "test" + ) + downloader.get_request_id() + downloader.request_id = "req-456" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b"" + + with patch("download_logs.requests.get", return_value=mock_response): + with patch( + "download_logs.xmltodict.parse", return_value={"testsuites": {}} + ): + result = downloader.get_xml_report() + + assert result is True + + def test_get_xml_report_private_url_for_rhel( + self, tmp_log_dir, log_file_with_request_id + ): + downloader = download_logs.TestingFarmLogDownloader( + log_file_with_request_id, "rhel9", "test" + ) + downloader.get_request_id() + downloader.request_id = "req-789" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b"" + + with patch( + "download_logs.requests.get", return_value=mock_response + ) as mock_get: + with patch( + "download_logs.xmltodict.parse", return_value={"testsuites": {}} + ): + result = downloader.get_xml_report() + + assert result is True + assert "artifacts.osci.redhat.com" in str(mock_get.call_args[0][0]) + + def test_get_xml_report_failure_after_retries(self, downloader, capsys): + downloader.request_id = "req-123" + mock_response = MagicMock() + mock_response.status_code = 500 + + with patch("download_logs.requests.get", return_value=mock_response): + with patch("download_logs.time.sleep"): + result = downloader.get_xml_report() + + assert result is False + assert downloader.xml_dict is None + assert "Failed to download XML report" in capsys.readouterr().out diff --git a/daily_tests/tox.ini b/daily_tests/tox.ini index 9366ad0..21c0f1b 100644 --- a/daily_tests/tox.ini +++ b/daily_tests/tox.ini @@ -3,3 +3,5 @@ commands = python3 -m pytest --color=yes -v deps = pytest PyYAML + requests + xmltodict diff --git a/run_nightly_tests.sh b/run_nightly_tests.sh index 9b712f9..dd7115d 100755 --- a/run_nightly_tests.sh +++ b/run_nightly_tests.sh @@ -35,13 +35,7 @@ LOG_FILE="${LOCAL_LOGS_DIR}/${TARGET}-${TESTS}.log" export USER_ID=$(id -u) export GROUP_ID=$(id -g) API_KEY="API_KEY_PRIVATE" -# function generate_passwd_file() { -# grep -v ^ci-scripts /etc/passwd > "$HOME/passwd" -# echo "ci-scripts:x:${USER_ID}:${GROUP_ID}:User for running ci-scripts:${HOME}:/bin/bash" >> "$HOME/passwd" -# export LD_PRELOAD=libnss_wrapper.so -# export NSS_WRAPPER_PASSWD=${HOME}/passwd -# export NSS_WRAPPER_GROUP=/etc/group -# } + BRANCH="master" function prepare_environment() { mkdir -p "${LOCAL_LOGS_DIR}"