From a195845b8c708b44eb5876d1911ec06f074a9908 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:12:48 +0000 Subject: [PATCH 1/4] build(deps): bump typeguard from 2.13.3 to 4.6.0 Bumps [typeguard](https://github.com/agronholm/typeguard) from 2.13.3 to 4.6.0. - [Release notes](https://github.com/agronholm/typeguard/releases) - [Commits](https://github.com/agronholm/typeguard/compare/2.13.3...4.6.0) --- updated-dependencies: - dependency-name: typeguard dependency-version: 4.6.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- dev-requirements.txt | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index d537b2b..19e1da3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1030,9 +1030,9 @@ twine==6.1.0 \ --hash=sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384 \ --hash=sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd # via l9gpu (pyproject.toml) -typeguard==2.13.3 \ - --hash=sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4 \ - --hash=sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 # via l9gpu (pyproject.toml) types-psutil==7.0.0.20250601 \ --hash=sha256:0c372e2d1b6529938a080a6ba4a9358e3dfc8526d82fabf40c1ef9325e4ca52e \ diff --git a/pyproject.toml b/pyproject.toml index 4de509d..8b881a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "pynvml==11.4.*", "typing_extensions", "click==8.0.4", - "typeguard==2.13.3", + "typeguard==4.6.0", "click-option-group", "pydantic==2.13.4", "omegaconf~=2.2", From 49bf44e35aa422aca446d0821ff2042d5b7c3c13 Mon Sep 17 00:00:00 2001 From: Shekhar Patil Date: Fri, 18 Sep 2026 10:34:02 +0530 Subject: [PATCH 2/4] test: adapt health check doubles to Typeguard 4 protocols --- .../health_checks_tests/test_check_sensors.py | 4 +- .../test_check_ssh_certs.py | 4 +- .../health_checks_tests/test_check_storage.py | 112 ++++-------------- 3 files changed, 30 insertions(+), 90 deletions(-) diff --git a/l9gpu/tests/health_checks_tests/test_check_sensors.py b/l9gpu/tests/health_checks_tests/test_check_sensors.py index 885be74..a06de27 100644 --- a/l9gpu/tests/health_checks_tests/test_check_sensors.py +++ b/l9gpu/tests/health_checks_tests/test_check_sensors.py @@ -28,8 +28,8 @@ class FakeSensorsCheckImpl: def get_sensors( self, - _timeout_secs: int, - _logger: logging.Logger, + timeout_secs: int, + logger: logging.Logger, ) -> ShellCommandOut: """Return pregenerated output instead of calling ipmi-sensors.""" return self.sensors_out diff --git a/l9gpu/tests/health_checks_tests/test_check_ssh_certs.py b/l9gpu/tests/health_checks_tests/test_check_ssh_certs.py index ed2c329..3d2fa40 100644 --- a/l9gpu/tests/health_checks_tests/test_check_ssh_certs.py +++ b/l9gpu/tests/health_checks_tests/test_check_ssh_certs.py @@ -46,7 +46,7 @@ class FakeSshCertsCheckImpl: log_level: str = "INFO" log_folder: str = "/tmp" - def get_ipa_certs(self, _host: str, timeout_secs: int) -> ShellCommandOut: + def get_ipa_certs(self, host: str, timeout_secs: int) -> ShellCommandOut: """ Return first param instead of invoking ipa host-show. @@ -62,7 +62,7 @@ def get_ipa_certs(self, _host: str, timeout_secs: int) -> ShellCommandOut: ) return self.params[0] - def get_ssh_certs(self, _host: str, timeout_secs: int) -> ShellCommandOut: + def get_ssh_certs(self, host: str, timeout_secs: int) -> ShellCommandOut: """ Return second param instead of invoking ssh-keyscan and ssh-keygen. diff --git a/l9gpu/tests/health_checks_tests/test_check_storage.py b/l9gpu/tests/health_checks_tests/test_check_storage.py index 3a655da..a83686d 100644 --- a/l9gpu/tests/health_checks_tests/test_check_storage.py +++ b/l9gpu/tests/health_checks_tests/test_check_storage.py @@ -15,9 +15,11 @@ from l9gpu.tests.fakes import FakeShellCommandOut -@dataclass -class FakeCheckDiskStorageImpl: - disk_usage: ShellCommandOut +class FakeStorageCheck: + """Implement the full storage protocol without running host commands. + + Each test overrides the operations it exercises; unexpected calls fail. + """ cluster = "test cluster" type = "prolog" @@ -27,30 +29,38 @@ class FakeCheckDiskStorageImpl: def get_disk_usage( self, timeout_secs: int, volume: str, logger: logging.Logger ) -> ShellCommandOut: - return self.disk_usage + raise NotImplementedError def get_mount_status( self, timeout_secs: int, dir: str, logger: logging.Logger ) -> PipedShellCommandOut: - return PipedShellCommandOut([0, 0], "dummy output") + raise NotImplementedError def check_file_exists(self, f: str, logger: logging.Logger) -> bool: - return True + raise NotImplementedError def check_directory_exists(self, dir: str, logger: logging.Logger) -> bool: - return True + raise NotImplementedError def get_fstab_mount_info( self, timeout_secs: int, mountpoint: str, logger: logging.Logger ) -> Tuple[ShellCommandOut, ShellCommandOut]: - return FakeShellCommandOut([""], 0, "dummy output"), FakeShellCommandOut( - [""], 0, "dummy output" - ) + raise NotImplementedError def get_disk_size( self, timeout_secs: int, volume: str, units: str, logger: logging.Logger ) -> PipedShellCommandOut: - return PipedShellCommandOut([0], "") + raise NotImplementedError + + +@dataclass +class FakeCheckDiskStorageImpl(FakeStorageCheck): + disk_usage: ShellCommandOut + + def get_disk_usage( + self, timeout_secs: int, volume: str, logger: logging.Logger + ) -> ShellCommandOut: + return self.disk_usage @pytest.fixture @@ -212,30 +222,14 @@ def test_inode_usage( @dataclass -class FakeCheckMountImpl: +class FakeCheckMountImpl(FakeStorageCheck): mount_status: PipedShellCommandOut - cluster = "test cluster" - type = "prolog" - log_level = "INFO" - log_folder = "/tmp" - - def get_disk_usage( - self, timeout_secs: int, volume: str, logger: logging.Logger - ) -> ShellCommandOut: - return FakeShellCommandOut([""], 0, "dummy output") - def get_mount_status( self, timeout_secs: int, dir: str, logger: logging.Logger ) -> PipedShellCommandOut: return self.mount_status - def check_file_exists(self, f: str, logger: logging.Logger) -> bool: - return True - - def check_directory_exists(self, dir: str, logger: logging.Logger) -> bool: - return True - @pytest.fixture def mount_tester(request: pytest.FixtureRequest) -> FakeCheckMountImpl: @@ -297,37 +291,15 @@ def test_mounted_directory( @dataclass -class FakeCheckExistanceImpl: +class FakeCheckExistanceImpl(FakeStorageCheck): existance: bool - cluster = "test cluster" - type = "prolog" - log_level = "INFO" - log_folder = "/tmp" - - def get_disk_usage( - self, timeout_secs: int, volume: str, logger: logging.Logger - ) -> ShellCommandOut: - return FakeShellCommandOut([""], 0, "dummy output") - - def get_mount_status( - self, timeout_secs: int, dir: str, logger: logging.Logger - ) -> PipedShellCommandOut: - return PipedShellCommandOut([0, 0], "dummy output") - def check_file_exists(self, f: str, logger: logging.Logger) -> bool: return self.existance def check_directory_exists(self, dir: str, logger: logging.Logger) -> bool: return self.existance - def get_fstab_mount_info( - self, timeout_secs: int, mountpoint: str, logger: logging.Logger - ) -> Tuple[ShellCommandOut, ShellCommandOut]: - return FakeShellCommandOut([""], 0, "dummy output"), FakeShellCommandOut( - [""], 0, "dummy output" - ) - @pytest.fixture def existance_tester(request: pytest.FixtureRequest) -> FakeCheckExistanceImpl: @@ -453,11 +425,7 @@ def test_dir_existance( @dataclass -class FakeExceptionImpl: - cluster = "test cluster" - type = "prolog" - log_level = "INFO" - log_folder = "/tmp" +class FakeExceptionImpl(FakeStorageCheck): def get_disk_usage( self, timeout_secs: int, volume: str, logger: logging.Logger @@ -475,13 +443,6 @@ def check_file_exists(self, f: str, logger: logging.Logger) -> bool: def check_directory_exists(self, dir: str, logger: logging.Logger) -> bool: raise ValueError("Some error") - def get_fstab_mount_info( - self, timeout_secs: int, mountpoint: str, logger: logging.Logger - ) -> Tuple[ShellCommandOut, ShellCommandOut]: - return FakeShellCommandOut([""], 0, "dummy output"), FakeShellCommandOut( - [""], 0, "dummy output" - ) - def test_file_dir_exception( caplog: pytest.LogCaptureFixture, @@ -517,30 +478,9 @@ def test_file_dir_exception( @dataclass -class FakeCheckMountpointImpl: +class FakeCheckMountpointImpl(FakeStorageCheck): mountpoint: Tuple[ShellCommandOut, ShellCommandOut] - cluster = "test cluster" - type = "prolog" - log_level = "INFO" - log_folder = "/tmp" - - def get_disk_usage( - self, timeout_secs: int, volume: str, logger: logging.Logger - ) -> ShellCommandOut: - return FakeShellCommandOut([""], 0, "dummy output") - - def get_mount_status( - self, timeout_secs: int, dir: str, logger: logging.Logger - ) -> PipedShellCommandOut: - return PipedShellCommandOut([0, 0], "dummy output") - - def check_file_exists(self, f: str, logger: logging.Logger) -> bool: - return True - - def check_directory_exists(self, dir: str, logger: logging.Logger) -> bool: - return True - def get_fstab_mount_info( self, timeout_secs: int, mountpoint: str, logger: logging.Logger ) -> Tuple[ShellCommandOut, ShellCommandOut]: @@ -630,7 +570,7 @@ def test_check_mountpoint( @dataclass -class FakeDiskSizeImpl: +class FakeDiskSizeImpl(FakeStorageCheck): def get_disk_size( self, timeout_secs: int, volume: str, units: str, logger: logging.Logger ) -> PipedShellCommandOut: From d2b03643de508283cc1a0b218036983fb810cc2d Mon Sep 17 00:00:00 2001 From: Shekhar Patil Date: Fri, 18 Sep 2026 10:56:57 +0530 Subject: [PATCH 3/4] fix: align shell command results with runtime protocol checks --- l9gpu/health_checks/subprocess.py | 8 +-- .../health_checks_tests/test_subprocess.py | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 l9gpu/tests/health_checks_tests/test_subprocess.py diff --git a/l9gpu/health_checks/subprocess.py b/l9gpu/health_checks/subprocess.py index 260b068..5aca3dd 100644 --- a/l9gpu/health_checks/subprocess.py +++ b/l9gpu/health_checks/subprocess.py @@ -2,14 +2,14 @@ # Copyright (c) Last9, Inc. import subprocess from dataclasses import dataclass -from typing import Dict, List, Optional, Protocol +from typing import Dict, List, Optional, Protocol, Union class ShellCommandOut(Protocol): - args: List[str] + args: Union[str, List[str]] returncode: int stdout: str - stderr: str + stderr: Optional[str] def check_returncode(self) -> None: ... @@ -23,7 +23,7 @@ class PipedShellCommandOut: def handle_subprocess_exception(exc: Exception) -> ShellCommandOut: if isinstance(exc, subprocess.TimeoutExpired): return subprocess.CompletedProcess( - args=[exc.cmd], + args=exc.cmd, returncode=128, stdout="Error command timeout because of timeout setting.\n", ) diff --git a/l9gpu/tests/health_checks_tests/test_subprocess.py b/l9gpu/tests/health_checks_tests/test_subprocess.py new file mode 100644 index 0000000..11549bf --- /dev/null +++ b/l9gpu/tests/health_checks_tests/test_subprocess.py @@ -0,0 +1,54 @@ +# Copyright (c) Last9, Inc. +import shlex +import subprocess +import sys + +import pytest +from typeguard import check_type + +from l9gpu.health_checks.subprocess import ( + handle_subprocess_exception, + shell_command, + ShellCommandOut, +) + + +@pytest.mark.parametrize("returncode", [0, 2]) +def test_shell_command_result_matches_protocol(returncode: int) -> None: + cmd = shlex.join( + [ + sys.executable, + "-c", + "import sys; print('output'); print('error', file=sys.stderr); " + f"sys.exit({returncode})", + ] + ) + + result = shell_command(cmd, timeout_secs=10) + + check_type(result, ShellCommandOut) + assert result.args == cmd + assert result.returncode == returncode + assert set(result.stdout.splitlines()) == {"output", "error"} + assert result.stderr is None + + +@pytest.mark.parametrize("cmd", ["test-command --flag", ["test-command", "--flag"]]) +def test_timeout_result_matches_protocol(cmd: str | list[str]) -> None: + result = handle_subprocess_exception(subprocess.TimeoutExpired(cmd, 10)) + + check_type(result, ShellCommandOut) + assert result.args == cmd + assert result.returncode == 128 + assert "Error command timeout because of timeout setting." in result.stdout + with pytest.raises(subprocess.CalledProcessError): + result.check_returncode() + + +def test_unknown_exception_result_matches_protocol() -> None: + result = handle_subprocess_exception(ValueError("unexpected failure")) + + check_type(result, ShellCommandOut) + assert result.args == [] + assert result.returncode == 2 + assert "Unknown subprocess exception" in result.stdout From 38ca2b7a7065eb7202f66cc713a3989fdcbc2ac2 Mon Sep 17 00:00:00 2001 From: Shekhar Patil Date: Fri, 18 Sep 2026 11:00:04 +0530 Subject: [PATCH 4/4] test: isolate subprocess exit status from Slurm child reaper --- l9gpu/tests/health_checks_tests/test_subprocess.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/l9gpu/tests/health_checks_tests/test_subprocess.py b/l9gpu/tests/health_checks_tests/test_subprocess.py index 11549bf..aaa9839 100644 --- a/l9gpu/tests/health_checks_tests/test_subprocess.py +++ b/l9gpu/tests/health_checks_tests/test_subprocess.py @@ -1,5 +1,6 @@ # Copyright (c) Last9, Inc. import shlex +import signal import subprocess import sys @@ -24,7 +25,13 @@ def test_shell_command_result_matches_protocol(returncode: int) -> None: ] ) - result = shell_command(cmd, timeout_secs=10) + # The sacct_backfill CLI installs a child-reaping handler at import time. + # Let subprocess collect its own exit status, regardless of test order. + previous_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL) + try: + result = shell_command(cmd, timeout_secs=10) + finally: + signal.signal(signal.SIGCHLD, previous_handler) check_type(result, ShellCommandOut) assert result.args == cmd