File size: 3,300 Bytes
9aa5185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""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()