| """Tests for Singularity/Apptainer preflight availability check. |
| |
| Verifies that a clear error is raised when neither apptainer nor |
| singularity is installed, instead of a cryptic FileNotFoundError. |
| |
| See: https://github.com/NousResearch/hermes-agent/issues/1511 |
| """ |
|
|
| import subprocess |
| from unittest.mock import patch, MagicMock |
|
|
| import pytest |
|
|
| from tools.environments.singularity import ( |
| _find_singularity_executable, |
| _ensure_singularity_available, |
| ) |
|
|
|
|
| class TestFindSingularityExecutable: |
| """_find_singularity_executable resolution tests.""" |
|
|
| def test_prefers_apptainer(self): |
| """When both are available, apptainer should be preferred.""" |
| def which_both(name): |
| return f"/usr/bin/{name}" if name in ("apptainer", "singularity") else None |
|
|
| with patch("shutil.which", side_effect=which_both): |
| assert _find_singularity_executable() == "apptainer" |
|
|
| def test_falls_back_to_singularity(self): |
| """When only singularity is available, use it.""" |
| def which_singularity_only(name): |
| return "/usr/bin/singularity" if name == "singularity" else None |
|
|
| with patch("shutil.which", side_effect=which_singularity_only): |
| assert _find_singularity_executable() == "singularity" |
|
|
| def test_raises_when_neither_found(self): |
| """Must raise RuntimeError with install instructions.""" |
| with patch("shutil.which", return_value=None): |
| with pytest.raises(RuntimeError, match="Neither.*apptainer.*nor.*singularity"): |
| _find_singularity_executable() |
|
|
|
|
| class TestEnsureSingularityAvailable: |
| """_ensure_singularity_available preflight tests.""" |
|
|
| def test_returns_executable_on_success(self): |
| """Returns the executable name when version check passes.""" |
| fake_result = MagicMock(returncode=0, stderr="") |
|
|
| with patch("shutil.which", side_effect=lambda n: "/usr/bin/apptainer" if n == "apptainer" else None), \ |
| patch("subprocess.run", return_value=fake_result): |
| assert _ensure_singularity_available() == "apptainer" |
|
|
| def test_raises_on_version_failure(self): |
| """Raises RuntimeError when version command fails.""" |
| fake_result = MagicMock(returncode=1, stderr="unknown flag") |
|
|
| with patch("shutil.which", side_effect=lambda n: "/usr/bin/apptainer" if n == "apptainer" else None), \ |
| patch("subprocess.run", return_value=fake_result): |
| with pytest.raises(RuntimeError, match="version.*failed"): |
| _ensure_singularity_available() |
|
|
| def test_raises_on_timeout(self): |
| """Raises RuntimeError when version command times out.""" |
| with patch("shutil.which", side_effect=lambda n: "/usr/bin/apptainer" if n == "apptainer" else None), \ |
| patch("subprocess.run", side_effect=subprocess.TimeoutExpired("apptainer", 10)): |
| with pytest.raises(RuntimeError, match="timed out"): |
| _ensure_singularity_available() |
|
|
| def test_raises_when_not_installed(self): |
| """Raises RuntimeError when neither executable exists.""" |
| with patch("shutil.which", return_value=None): |
| with pytest.raises(RuntimeError, match="Neither.*apptainer.*nor.*singularity"): |
| _ensure_singularity_available() |
|
|