From 8d34ebdd666c0ac4edad5508e1aab57e914192f5 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:51:29 +0100 Subject: [PATCH 01/17] Exclude tests from test coverage report --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..c712d259 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = tests/* From 90e115c9fe933c828e0cc3b62236b4a8898de761 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:06:11 +0100 Subject: [PATCH 02/17] Improved config file handling when running pytest - Back up `app.cfg` before testing, restore after - Copy `test_app.cfg` once, on test start --- tests/conftest.py | 18 ++++++++++++++++++ tests/test_eessi_bot_job_manager.py | 4 ---- tests/test_task_build.py | 6 ------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5c7aa6c4..6338687e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,9 @@ # license: GPLv2 # +import os +import shutil + def pytest_configure(config): # register custom markers @@ -24,3 +27,18 @@ def pytest_configure(config): config.addinivalue_line( "markers", "create_fails(bool): let function create_issue_comment return None" ) + + +def pytest_sessionstart(): + # Back up app.cfg if it exists + if os.path.exists("app.cfg"): + shutil.copyfile("app.cfg", "appbackup.cfg") + + # Copy needed app.cfg from tests directory + shutil.copyfile("tests/test_app.cfg", "app.cfg") + + +def pytest_sessionfinish(): + # Restore backup if it exists + if os.path.exists("appbackup.cfg"): + shutil.copyfile("appbackup.cfg", "app.cfg") diff --git a/tests/test_eessi_bot_job_manager.py b/tests/test_eessi_bot_job_manager.py index 5c5a9c05..4e032bea 100644 --- a/tests/test_eessi_bot_job_manager.py +++ b/tests/test_eessi_bot_job_manager.py @@ -12,15 +12,11 @@ # license: GPLv2 # -import shutil from eessi_bot_job_manager import EESSIBotSoftwareLayerJobManager def test_determine_running_jobs(): - # copy needed app.cfg from tests directory - shutil.copyfile("tests/test_app.cfg", "app.cfg") - job_manager = EESSIBotSoftwareLayerJobManager() assert job_manager.determine_running_jobs({}) == [] diff --git a/tests/test_task_build.py b/tests/test_task_build.py index fcb9f428..16840d51 100644 --- a/tests/test_task_build.py +++ b/tests/test_task_build.py @@ -18,7 +18,6 @@ import filecmp import os import re -import shutil from unittest.mock import patch # Third party imports (anything installed into the local Python environment) @@ -282,7 +281,6 @@ def no_sleep_after_create(delay): def test_create_pr_comment_succeeds(monkeypatch, mocked_github, tmpdir): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) - shutil.copyfile("tests/test_app.cfg", "app.cfg") # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') @@ -313,7 +311,6 @@ def test_create_pr_comment_succeeds(monkeypatch, mocked_github, tmpdir): def test_create_pr_comment_succeeds_none(monkeypatch, mocked_github, tmpdir): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) - shutil.copyfile("tests/test_app.cfg", "app.cfg") # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') @@ -340,7 +337,6 @@ def test_create_pr_comment_succeeds_none(monkeypatch, mocked_github, tmpdir): def test_create_pr_comment_raises_once_then_succeeds(monkeypatch, mocked_github, tmpdir): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) - shutil.copyfile("tests/test_app.cfg", "app.cfg") # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') @@ -367,7 +363,6 @@ def test_create_pr_comment_raises_once_then_succeeds(monkeypatch, mocked_github, def test_create_pr_comment_always_raises(monkeypatch, mocked_github, tmpdir): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) - shutil.copyfile("tests/test_app.cfg", "app.cfg") # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') @@ -395,7 +390,6 @@ def test_create_pr_comment_always_raises(monkeypatch, mocked_github, tmpdir): def test_create_pr_comment_three_raises(monkeypatch, mocked_github, tmpdir): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) - shutil.copyfile("tests/test_app.cfg", "app.cfg") # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') From 3164f0c6860f5552e17057c973f936bd149fccd2 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:55:07 +0100 Subject: [PATCH 03/17] Update run_cmd and run_subprocess tests - Add comments to test_run_cmd - Add comments and test cases to test_run_subprocess --- tests/test_eessi_bot_job_manager.py | 1 - tests/test_task_build.py | 40 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/test_eessi_bot_job_manager.py b/tests/test_eessi_bot_job_manager.py index 4e032bea..27c33478 100644 --- a/tests/test_eessi_bot_job_manager.py +++ b/tests/test_eessi_bot_job_manager.py @@ -12,7 +12,6 @@ # license: GPLv2 # - from eessi_bot_job_manager import EESSIBotSoftwareLayerJobManager diff --git a/tests/test_task_build.py b/tests/test_task_build.py index 16840d51..af3ea842 100644 --- a/tests/test_task_build.py +++ b/tests/test_task_build.py @@ -45,6 +45,7 @@ def test_run_cmd(tmpdir): assert output == "hello\n" assert err == "" + # Command fails and raise_on_error=True with pytest.raises(Exception): output, err, exit_code = run_cmd("ls -l /does_not_exists.txt", 'fail test', tmpdir, log_file=log_file) @@ -52,6 +53,7 @@ def test_run_cmd(tmpdir): assert output == "" assert "No such file or directory" in err + # Command fails and raise_on_error=False output, err, exit_code = run_cmd("ls -l /does_not_exists.txt", 'fail test', tmpdir, @@ -62,6 +64,7 @@ def test_run_cmd(tmpdir): assert output == "" assert "No such file or directory" in err + # Command does not exists and raise_on_error=True with pytest.raises(Exception): output, err, exit_code = run_cmd("this_command_does_not_exist", 'fail test', tmpdir, log_file=log_file) @@ -70,6 +73,7 @@ def test_run_cmd(tmpdir): assert ("this_command_does_not_exist: command not found" in err or "this_command_does_not_exist: not found" in err) + # Command does not exists and raise_on_error=False output, err, exit_code = run_cmd("this_command_does_not_exist", 'fail test', tmpdir, @@ -81,6 +85,7 @@ def test_run_cmd(tmpdir): assert ("this_command_does_not_exist: command not found" in err or "this_command_does_not_exist: not found" in err) + # Check that log_msg is written to log_file output, err, exit_code = run_cmd("echo hello", "test in file", tmpdir, log_file=log_file) with open(log_file, "r") as fp: assert "test in file" in fp.read() @@ -95,18 +100,53 @@ def test_run_subprocess(tmpdir): assert output == "hello\n" assert err == "" + # log_msg="" + output, err, exit_code = run_subprocess("echo hello", "", tmpdir, log_file=log_file) + + assert exit_code == 0 + assert output == "hello\n" + assert err == "" + with open(log_file, "r") as fp: + # TODO: Better way to do this? + assert "run_subprocess(): Running" in fp.read() + + # working_dir=tmpdir + output, err, exit_code = run_subprocess("pwd", "test", tmpdir, log_file=log_file) + + assert exit_code == 0 + assert tmpdir.strpath+"\n" == output + assert err == "" + + # working_dir=None + wd = os.getcwd() + output, err, exit_code = run_subprocess("pwd", "test", None, log_file=log_file) + + assert exit_code == 0 + assert wd in output + assert err == "" + + # env is not None + output, err, exit_code = run_subprocess("env", "test", tmpdir, log_file=log_file, env={"DUMMY": "123"}) + + assert exit_code == 0 + assert "DUMMY=123" in output + assert err == "" + + # Command fails output, err, exit_code = run_subprocess("ls -l /does_not_exists.txt", 'fail test', tmpdir, log_file=log_file) assert exit_code != 0 assert output == "" assert "No such file or directory" in err + # Command does not exist output, err, exit_code = run_subprocess("this_command_does_not_exist", 'fail test', tmpdir, log_file=log_file) assert exit_code != 0 assert output == "" assert ("this_command_does_not_exist: command not found" in err or "this_command_does_not_exist: not found" in err) + # Check that log_msg is written to log_file output, err, exit_code = run_subprocess("echo hello", "test in file", tmpdir, log_file=log_file) with open(log_file, "r") as fp: assert "test in file" in fp.read() From fbda75affaedb856f74ae643263ca5f6115b0235 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:09:08 +0100 Subject: [PATCH 04/17] Add tests for tools/args.py --- tests/test_tools_args.py | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_tools_args.py diff --git a/tests/test_tools_args.py b/tests/test_tools_args.py new file mode 100644 index 00000000..825b454e --- /dev/null +++ b/tests/test_tools_args.py @@ -0,0 +1,83 @@ +# Tests for 'tools/args.py' of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Sondre Bergsvaag Risanger (@sondrebr) +# +# license: GPLv2 +# + +# Standard library imports +from argparse import Namespace +from contextlib import nullcontext +import pytest + +# Local application imports (anything from EESSI/eessi-bot-software-layer) +from tools import args + + +# Test parse_common_args() +@pytest.mark.parametrize("test_args,expected_parsed,expected_unknown", [ + # No args + ([], Namespace(debug=False), []), + + # Short-form args + (["-d"], Namespace(debug=True), []), + + # Long-form args + (["--debug"], Namespace(debug=True), []), + + # Unknown args + (["-u", "--unknown"], Namespace(debug=False), ["-u", "--unknown"]), +]) +def test_parse_common_args(test_args, expected_parsed, expected_unknown): + parsed_args, unknown = args.parse_common_args(test_args) + + assert parsed_args == expected_parsed + assert sorted(unknown) == sorted(expected_unknown) + + +# Test event_handler_parse() +@pytest.mark.parametrize("test_args,expectation", [ + # No args + ([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000))), + + # Short-form args + (["-d", "-b", "-t", "-c", "-f", "file.json", "-p", "8000"], + nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000"))), + + # Long-form args + (["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000"], + nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file2.json", port="9000"))), + + # Unknown args - should fail and exit + (["-u"], pytest.raises(SystemExit)), + (["--unknown"], pytest.raises(SystemExit)), +]) +def test_event_handler_parse_known_args(test_args, expectation): + with expectation as expected: + assert args.event_handler_parse(test_args) == expected + + +# Test job_manager_parse() +@pytest.mark.parametrize("test_args,expectation", [ + # No args + ([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None))), + + # Short-form args + (["-d", "-i", "0", "-j", "17"], + nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17"))), + + # Long-form args + (["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48"], + nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48"))), + + # Unknown args - should fail and exit + (["-u"], pytest.raises(SystemExit)), + (["--unknown"], pytest.raises(SystemExit)), +]) +def test_job_manager_parse_known_args(test_args, expectation): + with expectation as expected: + assert args.job_manager_parse(test_args) == expected From 78322d107175ecbebe4d959d9bab5a391d0a3398 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:02:39 +0100 Subject: [PATCH 05/17] Add tests for tools/build_params.py --- tests/test_tools_build_params.py | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/test_tools_build_params.py diff --git a/tests/test_tools_build_params.py b/tests/test_tools_build_params.py new file mode 100644 index 00000000..02e79245 --- /dev/null +++ b/tests/test_tools_build_params.py @@ -0,0 +1,41 @@ +# Tests for 'tools/build_params.py' of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Sondre Bergsvaag Risanger (@sondrebr) +# +# license: GPLv2 +# + +# Standard library imports +from contextlib import nullcontext +import pytest + +# Local application imports (anything from EESSI/eessi-bot-software-layer) +from tools import build_params + + +# Test EESSIBotBuildParams class +@pytest.mark.parametrize("build_parameters,expectation", [ + # Test value error + ("notaparam", pytest.raises(build_params.EESSIBotBuildParamsValueError)), + + # Test name error + ("thisparam=doesnotexist", pytest.raises(build_params.EESSIBotBuildParamsNameError)), + + # Test complete component names + ("architecture=x86_64/amd/zen4,accelerator=nvidia/cc80", + nullcontext({"architecture": "x86_64/amd/zen4", "accelerator": "nvidia/cc80"})), + + # Test shortened component names + ("arch=aarch64/nvidia/grace,accel=nvidia/cc90", + nullcontext({"architecture": "aarch64/nvidia/grace", "accelerator": "nvidia/cc90"})), +]) +def test_EESSIBotBuildParams(build_parameters, expectation): + with expectation as expected_params: + params = build_params.EESSIBotBuildParams(build_parameters) + # Verify that the resulting object contains the expected items + for name, expected_value in expected_params.items(): + assert params[name] == expected_value From 55e4836d564be87bf726dadfc489debf3c3c6170 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:38:39 +0100 Subject: [PATCH 06/17] Add tests for tools/commands.py --- tests/test_tools_commands.py | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/test_tools_commands.py diff --git a/tests/test_tools_commands.py b/tests/test_tools_commands.py new file mode 100644 index 00000000..16d64090 --- /dev/null +++ b/tests/test_tools_commands.py @@ -0,0 +1,106 @@ +# Tests for 'tools/commands.py' of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Sondre Bergsvaag Risanger (@sondrebr) +# +# license: GPLv2 +# + +# Standard library imports +from contextlib import nullcontext + +# Third party imports (anything installed into the local Python environment) +import pytest + +# Local application imports (anything from EESSI/eessi-bot-software-layer) +from tools import commands + + +def test_contains_any_bot_command(): + # Test without command + body = "\n".join([ + "not a command", + "also not a command" + ]) + assert commands.contains_any_bot_command(body) is False + + # Test with command + body = "\n".join([ + "not a command", + "bot: help", + "also not a command" + ]) + assert commands.contains_any_bot_command(body) is True + + +def test_get_bot_command(): + # Test non-command + no_cmd = commands.get_bot_command("not a command") + assert no_cmd is None + + # Test command + test_cmd = "help" + cmd = commands.get_bot_command(f"bot: {test_cmd}") + assert cmd == test_cmd + + +# Helper classes for EESSIBotCommand test +class MockActionFilter(): + def __init__(self, action_filters=[]): + self.action_filters = action_filters + + def __eq__(self, other): + return self.action_filters == other.action_filters + + def to_string(self): + return " ".join([":".join(af) for af in self.action_filters]) + + +class MockCommand(): + def __init__(self, command, general_args=[], action_filters=MockActionFilter(), build_params=None): + self.command = command + self.general_args = general_args + self.action_filters = action_filters + self.build_params = build_params + + def __eq__(self, other): + return (self.command == other.command + and self.general_args == other.general_args + and self.action_filters == other.action_filters + and self.build_params == other.build_params) + + def to_string(self): + if self.action_filters is None: + return "" + string = self.command + if self.action_filters != MockActionFilter(): + string += f" {self.action_filters.to_string()}" + return string + + +# Test EESSIBotCommand class +@pytest.mark.parametrize("cmd_str,expectation", [ + # Test invalid filter + ("build for:arch=", pytest.raises(commands.EESSIBotCommandError)), + + # Test 'help' command + ("help", nullcontext(MockCommand("help"))), + + # Test 'status' command with last_build arg + ("status last_build", nullcontext(MockCommand("status", general_args=["last_build"], action_filters=None))), + + # Test 'build' command + ("build on:arch=icelake for:arch=x86_64/intel/icelake,accel=nvidia/cc90 repo:eessi.io-2025.06-software", + nullcontext(MockCommand("build", + action_filters=MockActionFilter([("architecture", "icelake"), + ("repository", "eessi.io-2025.06-software")]), + build_params={"architecture": "x86_64/intel/icelake", "accelerator": "nvidia/cc90"}))) +]) +def test_EESSIBotCommand(cmd_str, expectation): + with expectation as expected_command: + command = commands.EESSIBotCommand(cmd_str) + assert command == expected_command + assert command.to_string() == expected_command.to_string() From ff5bb3d34d23431a84b57c78aedde5dcd9725504 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:26:09 +0100 Subject: [PATCH 07/17] Add "Third party imports" comment for pytest --- tests/test_tools_args.py | 2 ++ tests/test_tools_build_params.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/test_tools_args.py b/tests/test_tools_args.py index 825b454e..a0db511c 100644 --- a/tests/test_tools_args.py +++ b/tests/test_tools_args.py @@ -12,6 +12,8 @@ # Standard library imports from argparse import Namespace from contextlib import nullcontext + +# Third party imports (anything installed into the local Python environment) import pytest # Local application imports (anything from EESSI/eessi-bot-software-layer) diff --git a/tests/test_tools_build_params.py b/tests/test_tools_build_params.py index 02e79245..3c4e9441 100644 --- a/tests/test_tools_build_params.py +++ b/tests/test_tools_build_params.py @@ -11,6 +11,8 @@ # Standard library imports from contextlib import nullcontext + +# Third party imports (anything installed into the local Python environment) import pytest # Local application imports (anything from EESSI/eessi-bot-software-layer) From 44e89a041318f6bca021c0b0a9e0fa157df8fa6d Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:26:24 +0100 Subject: [PATCH 08/17] Fix hound issues --- tests/test_tools_commands.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_tools_commands.py b/tests/test_tools_commands.py index 16d64090..4c7f8640 100644 --- a/tests/test_tools_commands.py +++ b/tests/test_tools_commands.py @@ -49,8 +49,8 @@ def test_get_bot_command(): # Helper classes for EESSIBotCommand test class MockActionFilter(): - def __init__(self, action_filters=[]): - self.action_filters = action_filters + def __init__(self, action_filters=None): + self.action_filters = action_filters or [] def __eq__(self, other): return self.action_filters == other.action_filters @@ -60,9 +60,9 @@ def to_string(self): class MockCommand(): - def __init__(self, command, general_args=[], action_filters=MockActionFilter(), build_params=None): + def __init__(self, command, general_args=None, action_filters=None, build_params=None): self.command = command - self.general_args = general_args + self.general_args = general_args or [] self.action_filters = action_filters self.build_params = build_params @@ -87,10 +87,10 @@ def to_string(self): ("build for:arch=", pytest.raises(commands.EESSIBotCommandError)), # Test 'help' command - ("help", nullcontext(MockCommand("help"))), + ("help", nullcontext(MockCommand("help", action_filters=MockActionFilter()))), # Test 'status' command with last_build arg - ("status last_build", nullcontext(MockCommand("status", general_args=["last_build"], action_filters=None))), + ("status last_build", nullcontext(MockCommand("status", general_args=["last_build"]))), # Test 'build' command ("build on:arch=icelake for:arch=x86_64/intel/icelake,accel=nvidia/cc90 repo:eessi.io-2025.06-software", From 876b66230a8783f670a33cfffbc6db5ae8cfe457 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:35:29 +0100 Subject: [PATCH 09/17] Additional tests for tools/job_metadata.py --- tests/test_tools_job_metadata.py | 35 ++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_tools_job_metadata.py b/tests/test_tools_job_metadata.py index 0d788248..b78f66b4 100644 --- a/tests/test_tools_job_metadata.py +++ b/tests/test_tools_job_metadata.py @@ -9,9 +9,22 @@ # license: GPLv2 # +# Standard library imports import os -from tools.job_metadata import get_section_from_file, JOB_PR_SECTION +# Local application imports (anything from EESSI/eessi-bot-software-layer) +from tools.job_metadata import determine_job_id_from_job_directory, get_section_from_file, JOB_PR_SECTION + + +def test_determine_job_id_from_job_directory(tmpdir): + logfile = os.path.join(tmpdir, "test_determine_job_id_from_job_directory.log") + + job_dir = os.path.join(tmpdir, "5") + assert determine_job_id_from_job_directory(job_dir, logfile) == 5 + + # determine_job_id_from_job_directory() should return 0 for non-job dirs + not_job_dir = os.path.join(tmpdir, "not-a-job-dir") + assert determine_job_id_from_job_directory(not_job_dir, logfile) == 0 def test_get_section_from_file(tmpdir): @@ -20,14 +33,32 @@ def test_get_section_from_file(tmpdir): path = os.path.join(tmpdir, 'test.metadata') assert get_section_from_file(path, JOB_PR_SECTION, logfile) is None + # Reading an empty file should return an empty dictionary + with open(path, 'w') as fp: + pass + metadata_pr = get_section_from_file(path, JOB_PR_SECTION, logfile) + assert metadata_pr == {} + + # Should return None if file exists but is invalid + with open(path, 'w') as fp: + fp.write("invalid format") + metadata_pr = get_section_from_file(path, JOB_PR_SECTION, logfile) + assert metadata_pr is None + + # Write a valid metadata file with open(path, 'w') as fp: fp.write('''[PR] repo=test - pr_number=12345''') + pr_number=12345 + pr_comment_id=23456 + job_owner=user01''') + # Verify that the metadata file is read correctly metadata_pr = get_section_from_file(path, JOB_PR_SECTION, logfile) expected = { "repo": "test", "pr_number": "12345", + "pr_comment_id": "23456", + "job_owner": "user01", } assert metadata_pr == expected From 2a303e7623f323c7d4df1174fb1040246060c077 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:33:36 +0100 Subject: [PATCH 10/17] Add tests for tools/logging.py --- tests/test_tools_logging.py | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_tools_logging.py diff --git a/tests/test_tools_logging.py b/tests/test_tools_logging.py new file mode 100644 index 00000000..a7855f4a --- /dev/null +++ b/tests/test_tools_logging.py @@ -0,0 +1,56 @@ +# Tests for 'tools/logging.py' of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Sondre Bergsvaag Risanger (@sondrebr) +# +# license: GPLv2 +# + +# Standard library imports +import os + +# Third party imports (anything installed into the local Python environment) +import pytest + +# Local application imports (anything from EESSI/eessi-bot-software-layer) +from tools import logging + + +# Test error() - should write to stderr and exit +def test_error(capfd): + # Test exit code 1 (default) + msg = "this is the first error message" + with pytest.raises(SystemExit, check=lambda err: int(err.code) == 1): + logging.error(msg) + assert msg in capfd.readouterr().err + + # Test exit code 0 + msg2 = "this is the second error message" + with pytest.raises(SystemExit, check=lambda err: int(err.code) == 0): + logging.error(msg2, rc=0) + assert msg2 in capfd.readouterr().err + + +# Test log() +def test_log(monkeypatch, tmp_path): + # Use a new log file + log_file = os.path.join(tmp_path, "test.log") + monkeypatch.setattr(logging, "LOG", log_file) + + # log() should create the file if it does not exist + msg = "this is the first test message" + logging.log(msg) + assert os.path.exists(tmp_path) + with open(log_file, "r") as fp: + assert msg in fp.read() + + # log() should not truncate the file if it already exists + msg2 = "this is the second test message" + logging.log(msg2) + with open(log_file, "r") as fp: + log_contents = fp.read() + assert msg in log_contents + assert msg2 in log_contents From 831425215f0c48c603a93209886a99296811a3cd Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:44:22 +0100 Subject: [PATCH 11/17] Add tests for tools/permissions.py --- tests/test_app.cfg | 1 + tests/test_tools_permissions.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/test_tools_permissions.py diff --git a/tests/test_app.cfg b/tests/test_app.cfg index 56c1d6cc..d0fff239 100644 --- a/tests/test_app.cfg +++ b/tests/test_app.cfg @@ -36,3 +36,4 @@ running_job = job `{job_id}` is running [finished_job_comments] [bot_control] +command_permission = user01 second_user diff --git a/tests/test_tools_permissions.py b/tests/test_tools_permissions.py new file mode 100644 index 00000000..c84f8eb9 --- /dev/null +++ b/tests/test_tools_permissions.py @@ -0,0 +1,33 @@ +# Tests for 'tools/permissions.py' of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Sondre Bergsvaag Risanger (@sondrebr) +# +# license: GPLv2 +# + +# Third party imports (anything installed into the local Python environment) +import pytest + +# Local application imports (anything from EESSI/eessi-bot-software-layer) +from tools import permissions + + +# Test check_command_permission() +@pytest.mark.parametrize("user,expected", [ + # In test_app.cfg: + # command_permission = user01 second_user + + # Users in test config + ("user01", True), + ("second_user", True), + + # Users not in test config + ("user03", False), + ("another_user_not_in_cfg", False), +]) +def test_check_command_permission(user, expected): + assert permissions.check_command_permission(user) is expected From 13c403962ddcf10b62779c77c32eb93eb54991b3 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:32:22 +0100 Subject: [PATCH 12/17] Replace tmpdir (legacy) with tmp_path --- tests/test_task_build.py | 70 ++++++++++++++++---------------- tests/test_tools_job_metadata.py | 14 +++---- tests/test_tools_pr_comments.py | 44 ++++++++++---------- 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/tests/test_task_build.py b/tests/test_task_build.py index af3ea842..cab91a35 100644 --- a/tests/test_task_build.py +++ b/tests/test_task_build.py @@ -36,10 +36,10 @@ from tests.test_tools_pr_comments import MockIssueComment -def test_run_cmd(tmpdir): +def test_run_cmd(tmp_path): """Tests for run_cmd function.""" - log_file = os.path.join(tmpdir, "log.txt") - output, err, exit_code = run_cmd("echo hello", 'test', tmpdir, log_file=log_file) + log_file = os.path.join(tmp_path, "log.txt") + output, err, exit_code = run_cmd("echo hello", 'test', tmp_path, log_file=log_file) assert exit_code == 0 assert output == "hello\n" @@ -47,7 +47,7 @@ def test_run_cmd(tmpdir): # Command fails and raise_on_error=True with pytest.raises(Exception): - output, err, exit_code = run_cmd("ls -l /does_not_exists.txt", 'fail test', tmpdir, log_file=log_file) + output, err, exit_code = run_cmd("ls -l /does_not_exists.txt", 'fail test', tmp_path, log_file=log_file) assert exit_code != 0 assert output == "" @@ -56,7 +56,7 @@ def test_run_cmd(tmpdir): # Command fails and raise_on_error=False output, err, exit_code = run_cmd("ls -l /does_not_exists.txt", 'fail test', - tmpdir, + tmp_path, log_file=log_file, raise_on_error=False) @@ -66,7 +66,7 @@ def test_run_cmd(tmpdir): # Command does not exists and raise_on_error=True with pytest.raises(Exception): - output, err, exit_code = run_cmd("this_command_does_not_exist", 'fail test', tmpdir, log_file=log_file) + output, err, exit_code = run_cmd("this_command_does_not_exist", 'fail test', tmp_path, log_file=log_file) assert exit_code != 0 assert output == "" @@ -76,7 +76,7 @@ def test_run_cmd(tmpdir): # Command does not exists and raise_on_error=False output, err, exit_code = run_cmd("this_command_does_not_exist", 'fail test', - tmpdir, + tmp_path, log_file=log_file, raise_on_error=False) @@ -86,22 +86,22 @@ def test_run_cmd(tmpdir): "this_command_does_not_exist: not found" in err) # Check that log_msg is written to log_file - output, err, exit_code = run_cmd("echo hello", "test in file", tmpdir, log_file=log_file) + output, err, exit_code = run_cmd("echo hello", "test in file", tmp_path, log_file=log_file) with open(log_file, "r") as fp: assert "test in file" in fp.read() -def test_run_subprocess(tmpdir): +def test_run_subprocess(tmp_path): """Tests for run_subprocess function.""" - log_file = os.path.join(tmpdir, "log.txt") - output, err, exit_code = run_subprocess("echo hello", 'test', tmpdir, log_file=log_file) + log_file = os.path.join(tmp_path, "log.txt") + output, err, exit_code = run_subprocess("echo hello", 'test', tmp_path, log_file=log_file) assert exit_code == 0 assert output == "hello\n" assert err == "" # log_msg="" - output, err, exit_code = run_subprocess("echo hello", "", tmpdir, log_file=log_file) + output, err, exit_code = run_subprocess("echo hello", "", tmp_path, log_file=log_file) assert exit_code == 0 assert output == "hello\n" @@ -110,11 +110,11 @@ def test_run_subprocess(tmpdir): # TODO: Better way to do this? assert "run_subprocess(): Running" in fp.read() - # working_dir=tmpdir - output, err, exit_code = run_subprocess("pwd", "test", tmpdir, log_file=log_file) + # working_dir=tmp_path + output, err, exit_code = run_subprocess("pwd", "test", tmp_path, log_file=log_file) assert exit_code == 0 - assert tmpdir.strpath+"\n" == output + assert f"{tmp_path}\n" == output assert err == "" # working_dir=None @@ -126,28 +126,28 @@ def test_run_subprocess(tmpdir): assert err == "" # env is not None - output, err, exit_code = run_subprocess("env", "test", tmpdir, log_file=log_file, env={"DUMMY": "123"}) + output, err, exit_code = run_subprocess("env", "test", tmp_path, log_file=log_file, env={"DUMMY": "123"}) assert exit_code == 0 assert "DUMMY=123" in output assert err == "" # Command fails - output, err, exit_code = run_subprocess("ls -l /does_not_exists.txt", 'fail test', tmpdir, log_file=log_file) + output, err, exit_code = run_subprocess("ls -l /does_not_exists.txt", 'fail test', tmp_path, log_file=log_file) assert exit_code != 0 assert output == "" assert "No such file or directory" in err # Command does not exist - output, err, exit_code = run_subprocess("this_command_does_not_exist", 'fail test', tmpdir, log_file=log_file) + output, err, exit_code = run_subprocess("this_command_does_not_exist", 'fail test', tmp_path, log_file=log_file) assert exit_code != 0 assert output == "" assert ("this_command_does_not_exist: command not found" in err or "this_command_does_not_exist: not found" in err) # Check that log_msg is written to log_file - output, err, exit_code = run_subprocess("echo hello", "test in file", tmpdir, log_file=log_file) + output, err, exit_code = run_subprocess("echo hello", "test in file", tmp_path, log_file=log_file) with open(log_file, "r") as fp: assert "test in file" in fp.read() @@ -318,14 +318,14 @@ def no_sleep_after_create(delay): # returns !None --> create_pr_comment returns comment (with id == 1) @pytest.mark.repo_name("EESSI/software-layer") @pytest.mark.pr_number(1) -def test_create_pr_comment_succeeds(monkeypatch, mocked_github, tmpdir): +def test_create_pr_comment_succeeds(monkeypatch, mocked_github, tmp_path): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') pr_number = 1 - job = Job(tmpdir, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") + job = Job(tmp_path, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") build_params = EESSIBotBuildParams("arch=amd/zen4,accel=nvidia/cc90") job_id = "123" @@ -348,14 +348,14 @@ def test_create_pr_comment_succeeds(monkeypatch, mocked_github, tmpdir): @pytest.mark.repo_name("EESSI/software-layer") @pytest.mark.pr_number(1) @pytest.mark.create_fails(True) -def test_create_pr_comment_succeeds_none(monkeypatch, mocked_github, tmpdir): +def test_create_pr_comment_succeeds_none(monkeypatch, mocked_github, tmp_path): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') pr_number = 1 - job = Job(tmpdir, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") + job = Job(tmp_path, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") build_params = EESSIBotBuildParams("arch=amd/zen4,accel=nvidia/cc90") job_id = "123" @@ -374,14 +374,14 @@ def test_create_pr_comment_succeeds_none(monkeypatch, mocked_github, tmpdir): @pytest.mark.repo_name("EESSI/software-layer") @pytest.mark.pr_number(1) @pytest.mark.create_raises("1") -def test_create_pr_comment_raises_once_then_succeeds(monkeypatch, mocked_github, tmpdir): +def test_create_pr_comment_raises_once_then_succeeds(monkeypatch, mocked_github, tmp_path): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') pr_number = 1 - job = Job(tmpdir, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") + job = Job(tmp_path, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") build_params = EESSIBotBuildParams("arch=amd/zen4,accel=nvidia/cc90") job_id = "123" @@ -400,14 +400,14 @@ def test_create_pr_comment_raises_once_then_succeeds(monkeypatch, mocked_github, @pytest.mark.repo_name("EESSI/software-layer") @pytest.mark.pr_number(1) @pytest.mark.create_raises("always_raise") -def test_create_pr_comment_always_raises(monkeypatch, mocked_github, tmpdir): +def test_create_pr_comment_always_raises(monkeypatch, mocked_github, tmp_path): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') pr_number = 1 - job = Job(tmpdir, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") + job = Job(tmp_path, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") build_params = EESSIBotBuildParams("arch=amd/zen4,accel=nvidia/cc90") job_id = "123" @@ -427,14 +427,14 @@ def test_create_pr_comment_always_raises(monkeypatch, mocked_github, tmpdir): @pytest.mark.repo_name("EESSI/software-layer") @pytest.mark.pr_number(1) @pytest.mark.create_raises("3") -def test_create_pr_comment_three_raises(monkeypatch, mocked_github, tmpdir): +def test_create_pr_comment_three_raises(monkeypatch, mocked_github, tmp_path): """Tests for function create_pr_comment.""" monkeypatch.setattr('tools.pr_comments.github', mocked_github) # creating a PR comment print("CREATING PR COMMENT") ym = datetime.today().strftime('%Y.%m') pr_number = 1 - job = Job(tmpdir, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") + job = Job(tmp_path, "test/architecture", "EESSI", "--speed-up", ym, pr_number, "fpga/magic", "user01") build_params = EESSIBotBuildParams("arch=amd/zen4,accel=nvidia/cc90") job_id = "123" @@ -452,12 +452,12 @@ def test_create_pr_comment_three_raises(monkeypatch, mocked_github, tmpdir): @pytest.mark.repo_name("test_repo") @pytest.mark.pr_number(999) -def test_create_read_metadata_file(mocked_github, tmpdir): +def test_create_read_metadata_file(mocked_github, tmp_path): """Tests for function create_metadata_file.""" # create some test data ym = datetime.today().strftime('%Y.%m') pr_number = 999 - job = Job(tmpdir, "test/architecture", "EESSI", "--speed_up_job", ym, pr_number, "fpga/magic", "user01") + job = Job(tmp_path, "test/architecture", "EESSI", "--speed_up_job", ym, pr_number, "fpga/magic", "user01") job_id = "123" @@ -466,7 +466,7 @@ def test_create_read_metadata_file(mocked_github, tmpdir): create_metadata_file(job, job_id, pr_comment) expected_file = f"_bot_job{job_id}.metadata" - expected_file_path = os.path.join(tmpdir, expected_file) + expected_file_path = os.path.join(tmp_path, expected_file) # assert expected_file exists assert os.path.exists(expected_file_path) @@ -489,7 +489,7 @@ def test_create_read_metadata_file(mocked_github, tmpdir): assert sorted(metadata["PR"].keys()) == ["job_owner", "pr_comment_id", "pr_number", "repo"] # use directory that does not exist - dir_does_not_exist = os.path.join(tmpdir, "dir_does_not_exist") + dir_does_not_exist = os.path.join(tmp_path, "dir_does_not_exist") job2 = Job(dir_does_not_exist, "test/architecture", "EESSI", "--speed_up_job", ym, pr_number, "fpga/magic", "user01") job_id2 = "222" @@ -509,12 +509,12 @@ def test_create_read_metadata_file(mocked_github, tmpdir): # use undefined values for parameters # job_id = None - job4 = Job(tmpdir, "test/architecture", "EESSI", "--speed_up_job", ym, pr_number, "fpga/magic", "user01") + job4 = Job(tmp_path, "test/architecture", "EESSI", "--speed_up_job", ym, pr_number, "fpga/magic", "user01") job_id4 = None create_metadata_file(job4, job_id4, pr_comment) expected_file4 = f"_bot_job{job_id}.metadata" - expected_file_path4 = os.path.join(tmpdir, expected_file4) + expected_file_path4 = os.path.join(tmp_path, expected_file4) # assert expected_file exists assert os.path.exists(expected_file_path4) diff --git a/tests/test_tools_job_metadata.py b/tests/test_tools_job_metadata.py index b78f66b4..c239d1eb 100644 --- a/tests/test_tools_job_metadata.py +++ b/tests/test_tools_job_metadata.py @@ -16,21 +16,21 @@ from tools.job_metadata import determine_job_id_from_job_directory, get_section_from_file, JOB_PR_SECTION -def test_determine_job_id_from_job_directory(tmpdir): - logfile = os.path.join(tmpdir, "test_determine_job_id_from_job_directory.log") +def test_determine_job_id_from_job_directory(tmp_path): + logfile = os.path.join(tmp_path, "test_determine_job_id_from_job_directory.log") - job_dir = os.path.join(tmpdir, "5") + job_dir = os.path.join(tmp_path, "5") assert determine_job_id_from_job_directory(job_dir, logfile) == 5 # determine_job_id_from_job_directory() should return 0 for non-job dirs - not_job_dir = os.path.join(tmpdir, "not-a-job-dir") + not_job_dir = os.path.join(tmp_path, "not-a-job-dir") assert determine_job_id_from_job_directory(not_job_dir, logfile) == 0 -def test_get_section_from_file(tmpdir): - logfile = os.path.join(tmpdir, 'test_get_section_from_file.log') +def test_get_section_from_file(tmp_path): + logfile = os.path.join(tmp_path, 'test_get_section_from_file.log') # if metadata file does not exist, we should get None as return value - path = os.path.join(tmpdir, 'test.metadata') + path = os.path.join(tmp_path, 'test.metadata') assert get_section_from_file(path, JOB_PR_SECTION, logfile) is None # Reading an empty file should return an empty dictionary diff --git a/tests/test_tools_pr_comments.py b/tests/test_tools_pr_comments.py index f89b3fd8..300fac25 100644 --- a/tests/test_tools_pr_comments.py +++ b/tests/test_tools_pr_comments.py @@ -636,8 +636,8 @@ def test_get_submitted_job_comment_retry(pr_job_get_comment_retry): # - pr.get_issue_comment(cmnt_id): 1st None ==> no edit # (patching pr.get_issue_comment via ContextManager to return None) -def test_update_comment_none(tmpdir): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_none(tmp_path): + log_file = os.path.join(tmp_path, "log.txt") with patch('github.PullRequest.PullRequest') as mock_pr: instance = mock_pr.return_value @@ -652,8 +652,8 @@ def test_update_comment_none(tmpdir): # log_file should contain error message "" expected = f"no comment with id {cmnt_id}, skipping update '{update}'" - file = tmpdir.join("log.txt") - actual = file.read() + file = tmp_path.joinpath("log.txt") + actual = file.read_text() # actual log message starts with a timestamp, hence we use 'in' assert expected in actual @@ -692,8 +692,8 @@ def test_update_comment_second_edit_succeeds(issue_edit_second_call_succeeds): # (edit_raises='N') or # (edit_raises='always_raise') # update_comment called with (str) -def test_update_comment_five_edit_fail(tmpdir, issue_edit_five_calls_fail): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_five_edit_fail(tmp_path, issue_edit_five_calls_fail): + log_file = os.path.join(tmp_path, "log.txt") # issue_edit_five_calls_fail provides one comment with "foo" os.environ['TEST_RAISE_EXCEPTION'] = '0' with pytest.raises(IssueCommentEditException): @@ -713,8 +713,8 @@ def test_update_comment_five_edit_fail(tmpdir, issue_edit_five_calls_fail): assert expected == actual -def test_update_comment_all_edit_fail(tmpdir, issue_edit_all_calls_fail): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_all_edit_fail(tmp_path, issue_edit_all_calls_fail): + log_file = os.path.join(tmp_path, "log.txt") # issue_edit_all_calls_fail provides one comment with "foo" os.environ['TEST_RAISE_EXCEPTION'] = '0' with pytest.raises(IssueCommentEditException): @@ -741,8 +741,8 @@ def test_update_comment_all_edit_fail(tmpdir, issue_edit_all_calls_fail): # (TEST_RAISE_EXCEPTION='0') # ==> edit: always fails (err2) # update_comment called with (int) -# def test_update_comment_edit_type_error(tmpdir, pr_with_any_comment): -# log_file = os.path.join(tmpdir, "log.txt") +# def test_update_comment_edit_type_error(tmp_path, pr_with_any_comment): +# log_file = os.path.join(tmp_path, "log.txt") # # pr_with_any_comment provides one comment with "foo" # os.environ['TEST_RAISE_EXCEPTION'] = '0' # #with pytest.raises(Exception) as err: @@ -763,8 +763,8 @@ def test_update_comment_all_edit_fail(tmpdir, issue_edit_all_calls_fail): # - pr.get_issue_comment(cmnt_id): 1st-Nth fail(err0) ==> no edit # (TEST_RAISE_EXCEPTION='N') -def test_update_comment_five_get_issue_comment_fail(tmpdir, issue_edit_five_calls_fail): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_five_get_issue_comment_fail(tmp_path, issue_edit_five_calls_fail): + log_file = os.path.join(tmp_path, "log.txt") # issue_edit_five_calls_fail just provides retry testing for # get_issue_comment # since all calls to this shall fail, we don't use the edit part here @@ -788,8 +788,8 @@ def test_update_comment_five_get_issue_comment_fail(tmpdir, issue_edit_five_call # - pr.get_issue_comment(cmnt_id): 1st-Nth fail(err0) ==> no edit # (TEST_RAISE_EXCEPTION='always_raise') -def test_update_comment_all_get_issue_comment_fail(tmpdir, issue_edit_all_calls_fail): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_all_get_issue_comment_fail(tmp_path, issue_edit_all_calls_fail): + log_file = os.path.join(tmp_path, "log.txt") # issue_edit_all_calls_fail just provides retry testing for # get_issue_comment # since all calls to this shall fail, we don't use the edit part here @@ -816,8 +816,8 @@ def test_update_comment_all_get_issue_comment_fail(tmpdir, issue_edit_all_calls_ # ==> edit: 1st succeeds # (edit_raises='0') # update_comment called with (str) -def test_update_comment_second_get_call_first_edit(tmpdir, issue_edit_first_call_succeeds): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_second_get_call_first_edit(tmp_path, issue_edit_first_call_succeeds): + log_file = os.path.join(tmp_path, "log.txt") os.environ['TEST_RAISE_EXCEPTION'] = '1' update_comment(0, issue_edit_first_call_succeeds, "-update", log_file=log_file) @@ -845,8 +845,8 @@ def test_update_comment_second_get_call_first_edit(tmpdir, issue_edit_first_call # ==> edit: 1st-(N-1)th fail(err1), 2nd-Nth succeeds # (edit_raises='1') # update_comment called with (str) -def test_update_comment_second_get_call_second_edit(tmpdir, issue_edit_second_call_succeeds): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_second_get_call_second_edit(tmp_path, issue_edit_second_call_succeeds): + log_file = os.path.join(tmp_path, "log.txt") os.environ['TEST_RAISE_EXCEPTION'] = '1' update_comment(0, issue_edit_second_call_succeeds, "-update", log_file=log_file) @@ -874,8 +874,8 @@ def test_update_comment_second_get_call_second_edit(tmpdir, issue_edit_second_ca # ==> edit: 1st-Nth fail(err1) # (edit_raises='N') or # update_comment called with (str) -def test_update_comment_second_get_call_five_edits_fail(tmpdir, issue_edit_five_calls_fail): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_second_get_call_five_edits_fail(tmp_path, issue_edit_five_calls_fail): + log_file = os.path.join(tmp_path, "log.txt") os.environ['TEST_RAISE_EXCEPTION'] = '1' with pytest.raises(IssueCommentEditException): update_comment(0, issue_edit_five_calls_fail, "-update", log_file=log_file) @@ -904,8 +904,8 @@ def test_update_comment_second_get_call_five_edits_fail(tmpdir, issue_edit_five_ # ==> edit: 1st-Nth fail(err1) # (edit_raises='always_raise') # update_comment called with (str) -def test_update_comment_second_get_call_all_edits_fail(tmpdir, issue_edit_all_calls_fail): - log_file = os.path.join(tmpdir, "log.txt") +def test_update_comment_second_get_call_all_edits_fail(tmp_path, issue_edit_all_calls_fail): + log_file = os.path.join(tmp_path, "log.txt") os.environ['TEST_RAISE_EXCEPTION'] = '1' with pytest.raises(IssueCommentEditException): update_comment(0, issue_edit_all_calls_fail, "-update", log_file=log_file) From 0f472524ae38dc8849fd32bf7e98d2596543f4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Bergsv=C3=A5g=20Risanger?= <168830227+sondrebr@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:23:52 +0100 Subject: [PATCH 13/17] Apply suggestion from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Röblitz --- tests/test_tools_job_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tools_job_metadata.py b/tests/test_tools_job_metadata.py index c239d1eb..40d596bc 100644 --- a/tests/test_tools_job_metadata.py +++ b/tests/test_tools_job_metadata.py @@ -34,7 +34,7 @@ def test_get_section_from_file(tmp_path): assert get_section_from_file(path, JOB_PR_SECTION, logfile) is None # Reading an empty file should return an empty dictionary - with open(path, 'w') as fp: + with open(path, 'w') as _: pass metadata_pr = get_section_from_file(path, JOB_PR_SECTION, logfile) assert metadata_pr == {} From cbacfa28b5a2489f7b2e6c74ea29e0921ab7c936 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:36:44 +0100 Subject: [PATCH 14/17] Update conftest.py authors --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6338687e..9d87804f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ # EESSI software layer, see https://github.com/EESSI/software-layer # # author: Thomas Roeblitz (@trz42) +# author: Sondre Bergsvaag Risanger (@sondrebr) # # license: GPLv2 # From def659e85a17dcb16b056c9e8d1b9286ee89e600 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:02:19 +0100 Subject: [PATCH 15/17] Apply suggested formatting change --- tests/test_tools_job_metadata.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_tools_job_metadata.py b/tests/test_tools_job_metadata.py index 40d596bc..5a1bdfc0 100644 --- a/tests/test_tools_job_metadata.py +++ b/tests/test_tools_job_metadata.py @@ -47,11 +47,13 @@ def test_get_section_from_file(tmp_path): # Write a valid metadata file with open(path, 'w') as fp: - fp.write('''[PR] - repo=test - pr_number=12345 - pr_comment_id=23456 - job_owner=user01''') + fp.write(""" +[PR] +repo=test +pr_number=12345 +pr_comment_id=23456 +job_owner=user01 +""") # Verify that the metadata file is read correctly metadata_pr = get_section_from_file(path, JOB_PR_SECTION, logfile) From 387d4d765805d068353a15cf021d8b7834a0499c Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:44:18 +0100 Subject: [PATCH 16/17] Add test cases for contains_any_bot_command --- tests/test_tools_commands.py | 59 +++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/tests/test_tools_commands.py b/tests/test_tools_commands.py index 4c7f8640..f1f9f203 100644 --- a/tests/test_tools_commands.py +++ b/tests/test_tools_commands.py @@ -19,21 +19,50 @@ from tools import commands -def test_contains_any_bot_command(): - # Test without command - body = "\n".join([ - "not a command", - "also not a command" - ]) - assert commands.contains_any_bot_command(body) is False - - # Test with command - body = "\n".join([ - "not a command", - "bot: help", - "also not a command" - ]) - assert commands.contains_any_bot_command(body) is True +# Test contains_any_bot_command() with both single-line and multi-line comments +@pytest.mark.parametrize("body,expected", [ + # Test single-line comments + # Existing command, no space after ':' + ("bot:help", True), + # Existing command, space after ':' + ("bot: help", True), + + # Command does not exist, no space after ':' + ("bot:nohelp", True), + # Command does not exist, space after ':' + ("bot: nohelp", True), + + # Leading whitespace, no space after ':' + (" bot:help", False), + # Leading whitespace, space after ':' + (" bot: help", False), + + # Test multi-line comments + # Valid command in first line, no space after ':' + ("\n".join(["bot:help", "not a command", "also not a command"]), True), + # Valid command in first line, space after ':' + ("\n".join(["bot: help", "not a command", "also not a command"]), True), + + # Valid command after first line, no space after ':' + ("\n".join(["not a command", "bot:help", "also not a command"]), True), + # Valid command after first line, space after ':' + ("\n".join(["not a command", "bot: help", "also not a command"]), True), + + # Multiple valid commands, no space after ':' + ("\n".join(["bot:help", "bot:nohelp", "also not a command"]), True), + # Multiple valid commands, space after ':' + ("\n".join(["bot: help", "bot: nohelp", "also not a command"]), True), + + # No commands + ("\n".join(["not a command", "also not a command"]), False), + + # Command with leading whitespace after second line, no space after ':' + ("\n".join(["not a command", " bot:help", "also not a command"]), False), + # Command with leading whitespace after second line, space after ':' + ("\n".join(["not a command", " bot: help", "also not a command"]), False), +]) +def test_contains_any_bot_command(body, expected): + assert commands.contains_any_bot_command(body) is expected def test_get_bot_command(): From e0bbc93e4e5008141e92d92acfd5aa225410dc29 Mon Sep 17 00:00:00 2001 From: "Sondre B. Risanger" <168830227+sondrebr@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:23:21 +0100 Subject: [PATCH 17/17] Add test cases for get_bot_command --- tests/test_tools_commands.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_tools_commands.py b/tests/test_tools_commands.py index f1f9f203..da0d70af 100644 --- a/tests/test_tools_commands.py +++ b/tests/test_tools_commands.py @@ -70,10 +70,31 @@ def test_get_bot_command(): no_cmd = commands.get_bot_command("not a command") assert no_cmd is None - # Test command - test_cmd = "help" - cmd = commands.get_bot_command(f"bot: {test_cmd}") - assert cmd == test_cmd + # Test different commands with varying formatting + test_cmds = [ + # All existing commands + "build", "cancel", "help", "show_config", "status", + # Build command with filters + "build on:arch=icelake for:arch=x86_64/intel/icelake,accel=nvidia/cc90 repo:eessi.io-2025.06-software", + # Non-existant command + "this_command_does_not_exist", + ] + for test_cmd in test_cmds: + # Valid formatting, with and without space after ':' + cmd = commands.get_bot_command(f"bot:{test_cmd}") + assert cmd == test_cmd + cmd = commands.get_bot_command(f"bot: {test_cmd}") + assert cmd == test_cmd + + # Leading whitespace, with and without space after ':' - should return None + cmd = commands.get_bot_command(f" bot:{test_cmd}") + assert cmd is None + cmd = commands.get_bot_command(f" bot: {test_cmd}") + assert cmd is None + + # Without ':' - should return None + cmd = commands.get_bot_command(f"bot {test_cmd}") + assert cmd is None # Helper classes for EESSIBotCommand test