GRM-13: feat: replace print() with stdlib logging module
- Create logging_config.py with get_logger() providing dual handlers: - Console handler (stderr) controlled by GRM_LOG_LEVEL env var (default INFO) - File handler (~/.local/state/grm/logs/grm.log) capturing everything at DEBUG - Replace all print() calls in executor.py and report.py with logger.info()/error() - Add error logging before raising AnsibleError in executor.run() - Add GRM_LOG_LEVEL to README configuration table and logging documentation - Update all unit tests to mock logger instead of using capsys - 114 tests, 100% coverage, pyright clean, ruff clean
This commit is contained in:
+57
-44
@@ -20,31 +20,40 @@ def _mock_popen_process(returncode: int = 0) -> MagicMock:
|
||||
|
||||
|
||||
class TestAnsibleExecutorRun:
|
||||
def test_run_success(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"], description="Test run")
|
||||
def test_run_success(self, tmp_path: Path) -> None:
|
||||
mock_logger = MagicMock()
|
||||
with patch("gitea_runner_manager.executor.get_logger", return_value=mock_logger):
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"], description="Test run")
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "[GRM] Test run" in captured.out
|
||||
assert "[GRM] Done" in captured.out
|
||||
messages = [call.args[0] for call in mock_logger.info.call_args_list]
|
||||
assert any("Test run" in msg for msg in messages)
|
||||
assert any("Done" in msg for msg in messages)
|
||||
|
||||
def test_run_failure(self, tmp_path: Path) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=1)
|
||||
with pytest.raises(AnsibleError, match="exit code 1"):
|
||||
executor.run(["false"], description="Test run")
|
||||
mock_logger = MagicMock()
|
||||
with patch("gitea_runner_manager.executor.get_logger", return_value=mock_logger):
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=1)
|
||||
with pytest.raises(AnsibleError, match="exit code 1"):
|
||||
executor.run(["false"], description="Test run")
|
||||
|
||||
def test_run_default_description(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"])
|
||||
assert mock_logger.error.called
|
||||
assert "exit code 1" in mock_logger.error.call_args[0][0]
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "[GRM] Running Ansible playbook" in captured.out
|
||||
def test_run_default_description(self, tmp_path: Path) -> None:
|
||||
mock_logger = MagicMock()
|
||||
with patch("gitea_runner_manager.executor.get_logger", return_value=mock_logger):
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"])
|
||||
|
||||
messages = [call.args[0] for call in mock_logger.info.call_args_list]
|
||||
assert any("Running Ansible playbook" in msg for msg in messages)
|
||||
|
||||
def test_run_with_stdout_lines(self, tmp_path: Path) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
@@ -60,20 +69,22 @@ class TestAnsibleExecutorRun:
|
||||
handle.write.assert_any_call("line1\n")
|
||||
handle.write.assert_any_call("line2\n")
|
||||
|
||||
def test_run_extracts_status(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
def test_run_extracts_status(self, tmp_path: Path) -> None:
|
||||
mock_logger = MagicMock()
|
||||
log_content = (
|
||||
"TASK [gitea-runner : Report runner status]\n"
|
||||
"ok: [127.0.0.1] => {\n"
|
||||
' "msg": "Runner \'127.0.0.1\' is installed and running."\n'
|
||||
"}\n"
|
||||
)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open(read_data=log_content)):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"], description="Test run")
|
||||
with patch("gitea_runner_manager.executor.get_logger", return_value=mock_logger):
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open(read_data=log_content)):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"], description="Test run")
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Runner '127.0.0.1' is installed and running" in captured.out
|
||||
messages = [call.args[0] for call in mock_logger.info.call_args_list]
|
||||
assert any("Runner '127.0.0.1' is installed and running" in msg for msg in messages)
|
||||
|
||||
|
||||
class TestAnsibleExecutorExtractStatus:
|
||||
@@ -129,32 +140,34 @@ class TestAnsibleExecutorPrepareLog:
|
||||
|
||||
|
||||
class TestAnsibleExecutorTranslation:
|
||||
def test_run_translated(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
def test_run_translated(self, tmp_path: Path) -> None:
|
||||
mock_logger = MagicMock()
|
||||
with (
|
||||
patch.dict(os.environ, {"GRM_LANG": "de"}),
|
||||
patch("subprocess.Popen") as mock_popen,
|
||||
patch("builtins.open", mock_open()),
|
||||
patch("gitea_runner_manager.executor.get_logger", return_value=mock_logger),
|
||||
):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"]) # use default description to test translation
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"]) # use default description to test translation
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Ansible-Playbook wird ausgeführt" in captured.out
|
||||
assert "Fertig" in captured.out
|
||||
messages = [call.args[0] for call in mock_logger.info.call_args_list]
|
||||
assert any("Ansible-Playbook wird ausgeführt" in msg for msg in messages)
|
||||
assert any("Fertig" in msg for msg in messages)
|
||||
|
||||
def test_unsupported_lang_fallback(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
def test_unsupported_lang_fallback(self, tmp_path: Path) -> None:
|
||||
mock_logger = MagicMock()
|
||||
with (
|
||||
patch.dict(os.environ, {"GRM_LANG": "xx"}),
|
||||
patch("subprocess.Popen") as mock_popen,
|
||||
patch("builtins.open", mock_open()),
|
||||
patch("gitea_runner_manager.executor.get_logger", return_value=mock_logger),
|
||||
):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"])
|
||||
executor = AnsibleExecutor(log_dir=tmp_path)
|
||||
with patch("subprocess.Popen") as mock_popen, patch("builtins.open", mock_open()):
|
||||
mock_popen.return_value = _mock_popen_process(returncode=0)
|
||||
executor.run(["echo", "hello"])
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "[GRM] Running Ansible playbook" in captured.out # English fallback
|
||||
messages = [call.args[0] for call in mock_logger.info.call_args_list]
|
||||
assert any("Running Ansible playbook" in msg for msg in messages) # English fallback
|
||||
|
||||
|
||||
class TestAnsibleExecutorAdHoc:
|
||||
|
||||
Reference in New Issue
Block a user