diff --git a/smartsim/_core/generation/generator.py b/smartsim/_core/generation/generator.py index 6486e6ec5..37f6ce419 100644 --- a/smartsim/_core/generation/generator.py +++ b/smartsim/_core/generation/generator.py @@ -108,12 +108,16 @@ def _log_file(log_path: pathlib.Path) -> pathlib.Path: """ return pathlib.Path(log_path) / "smartsim_params.txt" - def _output_files(self, log_path: pathlib.Path, job_name: str) -> t.Tuple[pathlib.Path, pathlib.Path]: + def _output_files( + self, log_path: pathlib.Path, job_name: str + ) -> t.Tuple[pathlib.Path, pathlib.Path]: out_file_path = log_path / (job_name + ".out") err_file_path = log_path / (job_name + ".err") return out_file_path, err_file_path - def generate_job(self, job: Job, job_index: int) -> t.Tuple[pathlib.Path, pathlib.Path, pathlib.Path]: + def generate_job( + self, job: Job, job_index: int + ) -> t.Tuple[pathlib.Path, pathlib.Path, pathlib.Path]: """Write and configure input files for a Job. To have files or directories present in the created Job @@ -140,7 +144,7 @@ def generate_job(self, job: Job, job_index: int) -> t.Tuple[pathlib.Path, pathli with open(self._log_file(log_path), mode="w", encoding="utf-8") as log_file: dt_string = datetime.now().strftime("%d/%m/%Y %H:%M:%S") log_file.write(f"Generation start date and time: {dt_string}\n") - + # Create output files out_file, err_file = self._output_files(log_path, job.entity.name) # Open and write to .out file @@ -344,4 +348,4 @@ def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None: # logger.log( # level=self.log_level, # msg=f"Configured application {entity.name} with no parameters", - # ) \ No newline at end of file + # ) diff --git a/smartsim/_core/launcher/dragon/dragonLauncher.py b/smartsim/_core/launcher/dragon/dragonLauncher.py index 56bb10bff..e71e694ee 100644 --- a/smartsim/_core/launcher/dragon/dragonLauncher.py +++ b/smartsim/_core/launcher/dragon/dragonLauncher.py @@ -27,8 +27,8 @@ from __future__ import annotations import os -import typing as t import pathlib +import typing as t from smartsim._core.schemas.dragonRequests import DragonRunPolicy from smartsim.error import errors diff --git a/smartsim/experiment.py b/smartsim/experiment.py index 5f82152e6..7137bed36 100644 --- a/smartsim/experiment.py +++ b/smartsim/experiment.py @@ -43,8 +43,8 @@ from smartsim._core.config import CONFIG from smartsim._core.control.launch_history import LaunchHistory as _LaunchHistory from smartsim.error import errors -from smartsim.status import InvalidJobStatus, JobStatus from smartsim.settings import dispatch +from smartsim.status import InvalidJobStatus, JobStatus from ._core import Controller, Generator, Manifest, previewrenderer from .database import FeatureStore @@ -59,8 +59,8 @@ from .log import ctx_exp_path, get_logger, method_contextualizer if t.TYPE_CHECKING: - from smartsim.settings.dispatch import ExecutableProtocol from smartsim.launchable.job import Job + from smartsim.settings.dispatch import ExecutableProtocol from smartsim.types import LaunchedJobID logger = get_logger(__name__) @@ -281,7 +281,9 @@ def get_status( return tuple(stats) @_contextualize - def _generate(self, generator: Generator, job: Job, job_index: int) -> t.Tuple[pathlib.Path, pathlib.Path, pathlib.Path]: + def _generate( + self, generator: Generator, job: Job, job_index: int + ) -> t.Tuple[pathlib.Path, pathlib.Path, pathlib.Path]: """Generate the directory structure and files for a ``Job`` If files or directories are attached to an ``Application`` object @@ -392,4 +394,4 @@ def _append_to_fs_identifier_list(self, fs_identifier: str) -> None: "with the same identifier" ) # Otherwise, add - self._fs_identifiers.add(fs_identifier) \ No newline at end of file + self._fs_identifiers.add(fs_identifier) diff --git a/smartsim/settings/arguments/launch/local.py b/smartsim/settings/arguments/launch/local.py index c638b6470..a61068cd4 100644 --- a/smartsim/settings/arguments/launch/local.py +++ b/smartsim/settings/arguments/launch/local.py @@ -25,9 +25,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations -from subprocess import PIPE import typing as t +from subprocess import PIPE from smartsim.log import get_logger from smartsim.settings.dispatch import ShellLauncher, dispatch, make_shell_format_fn diff --git a/smartsim/settings/arguments/launch/lsf.py b/smartsim/settings/arguments/launch/lsf.py index b8b6f5d91..e99c39af7 100644 --- a/smartsim/settings/arguments/launch/lsf.py +++ b/smartsim/settings/arguments/launch/lsf.py @@ -26,12 +26,18 @@ from __future__ import annotations -import typing as t import pathlib import subprocess +import typing as t from smartsim.log import get_logger -from smartsim.settings.dispatch import ShellLauncher, dispatch, ExecutableProtocol, _EnvironMappingType, ShellLauncherCommand +from smartsim.settings.dispatch import ( + ExecutableProtocol, + ShellLauncher, + ShellLauncherCommand, + _EnvironMappingType, + dispatch, +) from ...common import set_check_input from ...launchCommand import LauncherType @@ -39,12 +45,15 @@ logger = get_logger(__name__) -def _as_jsrun_command(args: LaunchArguments, - exe: ExecutableProtocol, - path: pathlib.Path, - env: _EnvironMappingType, - stdout_path: pathlib.Path, - stderr_path: pathlib.Path) -> ShellLauncherCommand: + +def _as_jsrun_command( + args: LaunchArguments, + exe: ExecutableProtocol, + path: pathlib.Path, + env: _EnvironMappingType, + stdout_path: pathlib.Path, + stderr_path: pathlib.Path, +) -> ShellLauncherCommand: command_tuple = ( "jsrun", *(args.format_launch_args() or ()), @@ -52,10 +61,11 @@ def _as_jsrun_command(args: LaunchArguments, *exe.as_program_arguments(), f"--stdio_stdout={stdout_path}", f"--stdio_stderr={stderr_path}", - ) # add output and err to CMD tuple -> add dev Null for stdout and stderr - return ShellLauncherCommand(env, path, subprocess.DEVNULL, subprocess.DEVNULL, command_tuple) + return ShellLauncherCommand( + env, path, subprocess.DEVNULL, subprocess.DEVNULL, command_tuple + ) @dispatch(with_format=_as_jsrun_command, to_launcher=ShellLauncher) diff --git a/smartsim/settings/arguments/launch/slurm.py b/smartsim/settings/arguments/launch/slurm.py index a92ac1646..141e9adcd 100644 --- a/smartsim/settings/arguments/launch/slurm.py +++ b/smartsim/settings/arguments/launch/slurm.py @@ -28,14 +28,21 @@ import os import pathlib -import subprocess import re +import subprocess import typing as t -from smartsim.log import get_logger -from smartsim.settings.dispatch import ShellLauncher, dispatch, make_shell_format_fn from smartsim._core.commands import Command -from smartsim.settings.dispatch import ExecutableProtocol, _FormatterType, _EnvironMappingType, ShellLauncherCommand +from smartsim.log import get_logger +from smartsim.settings.dispatch import ( + ExecutableProtocol, + ShellLauncher, + ShellLauncherCommand, + _EnvironMappingType, + _FormatterType, + dispatch, + make_shell_format_fn, +) from ...common import set_check_input from ...launchCommand import LauncherType @@ -43,12 +50,15 @@ logger = get_logger(__name__) -def _as_srun_command(args: LaunchArguments, - exe: ExecutableProtocol, - path: pathlib.Path, - env: _EnvironMappingType, - stdout_path: pathlib.Path, - stderr_path: pathlib.Path) -> ShellLauncherCommand: + +def _as_srun_command( + args: LaunchArguments, + exe: ExecutableProtocol, + path: pathlib.Path, + env: _EnvironMappingType, + stdout_path: pathlib.Path, + stderr_path: pathlib.Path, +) -> ShellLauncherCommand: command_tuple = ( "srun", *(args.format_launch_args() or ()), @@ -56,11 +66,12 @@ def _as_srun_command(args: LaunchArguments, *exe.as_program_arguments(), f"--output={stdout_path}", f"--error={stderr_path}", - ) # add output and err to CMD tuple -> add dev Null for stdout and stderr - return ShellLauncherCommand(env, path, subprocess.DEVNULL, subprocess.DEVNULL, command_tuple) - + return ShellLauncherCommand( + env, path, subprocess.DEVNULL, subprocess.DEVNULL, command_tuple + ) + @dispatch(with_format=_as_srun_command, to_launcher=ShellLauncher) class SlurmLaunchArguments(LaunchArguments): diff --git a/smartsim/settings/dispatch.py b/smartsim/settings/dispatch.py index a2af2bd4f..77ac6c74a 100644 --- a/smartsim/settings/dispatch.py +++ b/smartsim/settings/dispatch.py @@ -29,33 +29,36 @@ import abc import collections.abc import dataclasses +import io import os -import subprocess as sp import pathlib +import subprocess +import subprocess as sp import typing as t import uuid -import os +from subprocess import STDOUT import psutil from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack +from smartsim._core.commands import Command, CommandList from smartsim._core.utils import helpers from smartsim.error import errors from smartsim.status import JobStatus from smartsim.types import LaunchedJobID -from smartsim._core.commands import Command, CommandList + from ..settings.launchCommand import LauncherType -from subprocess import STDOUT if t.TYPE_CHECKING: from smartsim.experiment import Experiment from smartsim.settings.arguments import LaunchArguments + class ShellLauncherCommand(t.NamedTuple): env: _EnvironMappingType path: pathlib.Path - stdout: TextIOWrapper - stderr: TextIOWrapper + stdout: io.TextIOWrapper | int + stderr: io.TextIOWrapper | int command_tuple: tuple[str, tuple[str, ...]] | t.Sequence[str] @@ -79,7 +82,14 @@ class ShellLauncherCommand(t.NamedTuple): a job """ _FormatterType: TypeAlias = t.Callable[ - [_DispatchableT, "ExecutableProtocol", _WorkingDirectory, _EnvironMappingType, pathlib.Path, pathlib.Path], + [ + _DispatchableT, + "ExecutableProtocol", + _WorkingDirectory, + _EnvironMappingType, + pathlib.Path, + pathlib.Path, + ], _LaunchableT, ] """A callable that is capable of formatting the components of a job into a type @@ -448,7 +458,7 @@ def get_status( def make_shell_format_fn( - run_command: str | None + run_command: str | None, ) -> _FormatterType[LaunchArguments, ShellLauncherCommand]: """A function that builds a function that formats a `LaunchArguments` as a shell executable sequence of strings for a given launching utility. @@ -478,6 +488,7 @@ def make_shell_format_fn( :returns: A function to format an arguments, an executable, and an environment as a shell launchable sequence for strings. """ + def impl( args: LaunchArguments, exe: ExecutableProtocol, @@ -496,27 +507,36 @@ def impl( if run_command is not None else exe.as_program_arguments() ) - return ShellLauncherCommand(env, pathlib.Path(path), stdout_path, stderr_path, command_tuple) + return ShellLauncherCommand( + env, pathlib.Path(path), open(stdout_path), open(stderr_path), command_tuple + ) return impl - + class ShellLauncher: """Mock launcher for launching/tracking simple shell commands""" + # add a def check def __init__(self) -> None: self._launched: dict[LaunchedJobID, sp.Popen[bytes]] = {} + # covariant, contravariant, + boliscoff substitution princ def start( - self, shell_command: ShellLauncherCommand # this should be a named tuple + self, shell_command: ShellLauncherCommand # this should be a named tuple ) -> LaunchedJobID: id_ = create_job_id() # raise ValueError -> invalid stuff throw exe, *rest = shell_command.command_tuple expanded_exe = helpers.expand_exe_path(exe) - # pylint: disable-next=consider-using-with - self._launched[id_] = sp.Popen((expanded_exe, *rest), cwd=shell_command.path, env={k:v for k,v in shell_command.env.items() if v is not None}, stdout=shell_command.stdout, stderr=shell_command.stderr) + self._launched[id_] = sp.Popen( + (expanded_exe, *rest), + cwd=shell_command.path, + env={k: v for k, v in shell_command.env.items() if v is not None}, + stdout=shell_command.stdout, + stderr=shell_command.stderr, + ) # Popen starts a new process and gives you back a handle to process, getting back the pid - process id return id_ @@ -529,7 +549,9 @@ def _get_status(self, id_: LaunchedJobID, /) -> JobStatus: if (proc := self._launched.get(id_)) is None: msg = f"Launcher `{self}` has not launched a job with id `{id_}`" raise errors.LauncherJobNotFound(msg) - ret_code = proc.poll() # add a test that mocks out poll and raise some exception - terminal -> import subprocess -> start something echo blah - then poll and see what a valid fake output is + ret_code = ( + proc.poll() + ) # add a test that mocks out poll and raise some exception - terminal -> import subprocess -> start something echo blah - then poll and see what a valid fake output is print(ret_code) # try/catch around here and then reaise a smartsim.error if ret_code is None: diff --git a/tests/temp_tests/test_settings/test_alpsLauncher.py b/tests/temp_tests/test_settings/test_alpsLauncher.py index bf770ea6a..ed18d5810 100644 --- a/tests/temp_tests/test_settings/test_alpsLauncher.py +++ b/tests/temp_tests/test_settings/test_alpsLauncher.py @@ -23,9 +23,12 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import pytest +import io +import os import pathlib +import pytest + from smartsim.settings import LaunchSettings from smartsim.settings.arguments.launch.alps import ( AprunLaunchArguments, @@ -213,14 +216,15 @@ def test_invalid_exclude_hostlist_format(): ), ) def test_formatting_launch_args(mock_echo_executable, args, expected, test_dir): - outfile = "out.txt" - errfile = "err.txt" - shell_launch_cmd = _as_aprun_command( - AprunLaunchArguments(args), mock_echo_executable, test_dir, {}, outfile, errfile - ) + out = os.path.join(test_dir, "out.txt") + err = os.path.join(test_dir, "err.txt") + with open(out, "w") as _, open(err, "w") as _: + shell_launch_cmd = _as_aprun_command( + AprunLaunchArguments(args), mock_echo_executable, test_dir, {}, out, err + ) assert isinstance(shell_launch_cmd, ShellLauncherCommand) assert shell_launch_cmd.command_tuple == expected assert shell_launch_cmd.path == pathlib.Path(test_dir) assert shell_launch_cmd.env == {} - assert shell_launch_cmd.stdout == outfile - assert shell_launch_cmd.stderr == errfile + assert isinstance(shell_launch_cmd.stdout, io.TextIOWrapper) + assert isinstance(shell_launch_cmd.stderr, io.TextIOWrapper) diff --git a/tests/temp_tests/test_settings/test_dragonLauncher.py b/tests/temp_tests/test_settings/test_dragonLauncher.py index 3b2837a60..6e3722dde 100644 --- a/tests/temp_tests/test_settings/test_dragonLauncher.py +++ b/tests/temp_tests/test_settings/test_dragonLauncher.py @@ -90,7 +90,13 @@ def test_formatting_launch_args_into_request( if v is not NOT_SET } expected_run_req = DragonRunRequestView( - exe="echo", exe_args=["hello", "world"], path=test_dir, env={}, output_file="output.txt", error_file="error.txt", **expected_args + exe="echo", + exe_args=["hello", "world"], + path=test_dir, + env={}, + output_file="output.txt", + error_file="error.txt", + **expected_args, ) assert req.exe == expected_run_req.exe assert req.exe_args == expected_run_req.exe_args diff --git a/tests/temp_tests/test_settings/test_localLauncher.py b/tests/temp_tests/test_settings/test_localLauncher.py index 7e2605576..65bcb6bc9 100644 --- a/tests/temp_tests/test_settings/test_localLauncher.py +++ b/tests/temp_tests/test_settings/test_localLauncher.py @@ -23,16 +23,19 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import pytest +import io +import os import pathlib +import pytest + from smartsim.settings import LaunchSettings from smartsim.settings.arguments.launch.local import ( LocalLaunchArguments, _as_local_command, ) -from smartsim.settings.launchCommand import LauncherType from smartsim.settings.dispatch import ShellLauncherCommand +from smartsim.settings.launchCommand import LauncherType pytestmark = pytest.mark.group_a @@ -145,14 +148,15 @@ def test_format_env_vars(): def test_formatting_returns_original_exe(mock_echo_executable, test_dir): - outfile = "out.txt" - errfile = "err.txt" - shell_launch_cmd = _as_local_command( - LocalLaunchArguments({}), mock_echo_executable, test_dir, {}, outfile, errfile - ) + out = os.path.join(test_dir, "out.txt") + err = os.path.join(test_dir, "err.txt") + with open(out, "w") as _, open(err, "w") as _: + shell_launch_cmd = _as_local_command( + LocalLaunchArguments({}), mock_echo_executable, test_dir, {}, out, err + ) assert isinstance(shell_launch_cmd, ShellLauncherCommand) assert shell_launch_cmd.command_tuple == ("echo", "hello", "world") assert shell_launch_cmd.path == pathlib.Path(test_dir) assert shell_launch_cmd.env == {} - assert shell_launch_cmd.stdout == outfile - assert shell_launch_cmd.stderr == errfile + assert isinstance(shell_launch_cmd.stdout, io.TextIOWrapper) + assert isinstance(shell_launch_cmd.stderr, io.TextIOWrapper) diff --git a/tests/temp_tests/test_settings/test_lsfLauncher.py b/tests/temp_tests/test_settings/test_lsfLauncher.py index e3879a3bc..eef23f530 100644 --- a/tests/temp_tests/test_settings/test_lsfLauncher.py +++ b/tests/temp_tests/test_settings/test_lsfLauncher.py @@ -23,9 +23,10 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import pytest import subprocess +import pytest + from smartsim.settings import LaunchSettings from smartsim.settings.arguments.launch.lsf import ( JsrunLaunchArguments, @@ -92,30 +93,90 @@ def test_launch_args(): @pytest.mark.parametrize( "args, expected", ( - pytest.param({}, ("jsrun", "--", "echo", "hello", "world", '--stdio_stdout=output.txt', '--stdio_stderr=error.txt'), id="Empty Args"), + pytest.param( + {}, + ( + "jsrun", + "--", + "echo", + "hello", + "world", + "--stdio_stdout=output.txt", + "--stdio_stderr=error.txt", + ), + id="Empty Args", + ), pytest.param( {"n": "1"}, - ("jsrun", "-n", "1", "--", "echo", "hello", "world", '--stdio_stdout=output.txt', '--stdio_stderr=error.txt'), + ( + "jsrun", + "-n", + "1", + "--", + "echo", + "hello", + "world", + "--stdio_stdout=output.txt", + "--stdio_stderr=error.txt", + ), id="Short Arg", ), pytest.param( {"nrs": "1"}, - ("jsrun", "--nrs=1", "--", "echo", "hello", "world", '--stdio_stdout=output.txt', '--stdio_stderr=error.txt'), + ( + "jsrun", + "--nrs=1", + "--", + "echo", + "hello", + "world", + "--stdio_stdout=output.txt", + "--stdio_stderr=error.txt", + ), id="Long Arg", ), pytest.param( {"v": None}, - ("jsrun", "-v", "--", "echo", "hello", "world", '--stdio_stdout=output.txt', '--stdio_stderr=error.txt'), + ( + "jsrun", + "-v", + "--", + "echo", + "hello", + "world", + "--stdio_stdout=output.txt", + "--stdio_stderr=error.txt", + ), id="Short Arg (No Value)", ), pytest.param( {"verbose": None}, - ("jsrun", "--verbose", "--", "echo", "hello", "world", '--stdio_stdout=output.txt', '--stdio_stderr=error.txt'), + ( + "jsrun", + "--verbose", + "--", + "echo", + "hello", + "world", + "--stdio_stdout=output.txt", + "--stdio_stderr=error.txt", + ), id="Long Arg (No Value)", ), pytest.param( {"tasks_per_rs": "1", "n": "123"}, - ("jsrun", "--tasks_per_rs=1", "-n", "123", "--", "echo", "hello", "world", '--stdio_stdout=output.txt', '--stdio_stderr=error.txt'), + ( + "jsrun", + "--tasks_per_rs=1", + "-n", + "123", + "--", + "echo", + "hello", + "world", + "--stdio_stdout=output.txt", + "--stdio_stderr=error.txt", + ), id="Short and Long Args", ), ), diff --git a/tests/temp_tests/test_settings/test_mpiLauncher.py b/tests/temp_tests/test_settings/test_mpiLauncher.py index fa0d1cc7f..eff6b3ca2 100644 --- a/tests/temp_tests/test_settings/test_mpiLauncher.py +++ b/tests/temp_tests/test_settings/test_mpiLauncher.py @@ -24,13 +24,14 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import io import itertools +import os +import pathlib import pytest -import pathlib from smartsim.settings import LaunchSettings -from smartsim.settings.dispatch import ShellLauncherCommand from smartsim.settings.arguments.launch.mpi import ( MpiexecLaunchArguments, MpirunLaunchArguments, @@ -39,6 +40,7 @@ _as_mpirun_command, _as_orterun_command, ) +from smartsim.settings.dispatch import ShellLauncherCommand from smartsim.settings.launchCommand import LauncherType pytestmark = pytest.mark.group_a @@ -288,12 +290,13 @@ def test_invalid_hostlist_format(launcher): def test_formatting_launch_args( mock_echo_executable, cls, fmt, cmd, args, expected, test_dir ): - outfile = "out.txt" - errfile = "err.txt" - shell_launch_cmd = fmt(cls(args), mock_echo_executable, test_dir, {}, outfile, errfile) + out = os.path.join(test_dir, "out.txt") + err = os.path.join(test_dir, "err.txt") + with open(out, "w") as _, open(err, "w") as _: + shell_launch_cmd = fmt(cls(args), mock_echo_executable, test_dir, {}, out, err) assert isinstance(shell_launch_cmd, ShellLauncherCommand) assert shell_launch_cmd.command_tuple == (cmd,) + expected assert shell_launch_cmd.path == pathlib.Path(test_dir) assert shell_launch_cmd.env == {} - assert shell_launch_cmd.stdout == outfile - assert shell_launch_cmd.stderr == errfile + assert isinstance(shell_launch_cmd.stdout, io.TextIOWrapper) + assert isinstance(shell_launch_cmd.stderr, io.TextIOWrapper) diff --git a/tests/temp_tests/test_settings/test_palsLauncher.py b/tests/temp_tests/test_settings/test_palsLauncher.py index a25034401..261c04c17 100644 --- a/tests/temp_tests/test_settings/test_palsLauncher.py +++ b/tests/temp_tests/test_settings/test_palsLauncher.py @@ -24,16 +24,19 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import pytest +import io +import os import pathlib +import pytest + from smartsim.settings import LaunchSettings from smartsim.settings.arguments.launch.pals import ( PalsMpiexecLaunchArguments, _as_pals_command, ) -from smartsim.settings.launchCommand import LauncherType from smartsim.settings.dispatch import ShellLauncherCommand +from smartsim.settings.launchCommand import LauncherType pytestmark = pytest.mark.group_a @@ -134,14 +137,20 @@ def test_invalid_hostlist_format(): ), ) def test_formatting_launch_args(mock_echo_executable, args, expected, test_dir): - outfile = "out.txt" - errfile = "err.txt" - shell_launch_cmd = _as_pals_command( - PalsMpiexecLaunchArguments(args), mock_echo_executable, test_dir, {}, outfile, errfile - ) + out = os.path.join(test_dir, "out.txt") + err = os.path.join(test_dir, "err.txt") + with open(out, "w") as _, open(err, "w") as _: + shell_launch_cmd = _as_pals_command( + PalsMpiexecLaunchArguments(args), + mock_echo_executable, + test_dir, + {}, + out, + err, + ) assert isinstance(shell_launch_cmd, ShellLauncherCommand) assert shell_launch_cmd.command_tuple == expected assert shell_launch_cmd.path == pathlib.Path(test_dir) assert shell_launch_cmd.env == {} - assert shell_launch_cmd.stdout == outfile - assert shell_launch_cmd.stderr == errfile + assert isinstance(shell_launch_cmd.stdout, io.TextIOWrapper) + assert isinstance(shell_launch_cmd.stderr, io.TextIOWrapper) diff --git a/tests/temp_tests/test_settings/test_slurmLauncher.py b/tests/temp_tests/test_settings/test_slurmLauncher.py index 59068fc4f..892978dfb 100644 --- a/tests/temp_tests/test_settings/test_slurmLauncher.py +++ b/tests/temp_tests/test_settings/test_slurmLauncher.py @@ -23,18 +23,19 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import pytest import pathlib import subprocess +import pytest + +from smartsim._core.commands import Command from smartsim.settings import LaunchSettings -from smartsim.settings.dispatch import ShellLauncherCommand from smartsim.settings.arguments.launch.slurm import ( SlurmLaunchArguments, _as_srun_command, ) +from smartsim.settings.dispatch import ShellLauncherCommand from smartsim.settings.launchCommand import LauncherType -from smartsim._core.commands import Command pytestmark = pytest.mark.group_a @@ -292,37 +293,103 @@ def test_set_het_groups(monkeypatch): @pytest.mark.parametrize( "args, expected", ( - pytest.param({}, ("srun", "--", "echo", "hello", "world", '--output=output.txt', '--error=error.txt'), id="Empty Args"), + pytest.param( + {}, + ( + "srun", + "--", + "echo", + "hello", + "world", + "--output=output.txt", + "--error=error.txt", + ), + id="Empty Args", + ), pytest.param( {"N": "1"}, - ("srun", "-N", "1", "--", "echo", "hello", "world", '--output=output.txt', '--error=error.txt'), + ( + "srun", + "-N", + "1", + "--", + "echo", + "hello", + "world", + "--output=output.txt", + "--error=error.txt", + ), id="Short Arg", ), pytest.param( {"nodes": "1"}, - ("srun", "--nodes=1", "--", "echo", "hello", "world", '--output=output.txt', '--error=error.txt'), + ( + "srun", + "--nodes=1", + "--", + "echo", + "hello", + "world", + "--output=output.txt", + "--error=error.txt", + ), id="Long Arg", ), pytest.param( {"v": None}, - ("srun", "-v", "--", "echo", "hello", "world", '--output=output.txt', '--error=error.txt'), + ( + "srun", + "-v", + "--", + "echo", + "hello", + "world", + "--output=output.txt", + "--error=error.txt", + ), id="Short Arg (No Value)", ), pytest.param( {"verbose": None}, - ("srun", "--verbose", "--", "echo", "hello", "world", '--output=output.txt', '--error=error.txt'), + ( + "srun", + "--verbose", + "--", + "echo", + "hello", + "world", + "--output=output.txt", + "--error=error.txt", + ), id="Long Arg (No Value)", ), pytest.param( {"nodes": "1", "n": "123"}, - ("srun", "--nodes=1", "-n", "123", "--", "echo", "hello", "world", '--output=output.txt', '--error=error.txt'), + ( + "srun", + "--nodes=1", + "-n", + "123", + "--", + "echo", + "hello", + "world", + "--output=output.txt", + "--error=error.txt", + ), id="Short and Long Args", ), ), ) def test_formatting_launch_args(mock_echo_executable, args, expected, test_dir): - - shell_launch_cmd = _as_srun_command(args=SlurmLaunchArguments(args), exe=mock_echo_executable, path=test_dir, env={}, stdout_path="output.txt", stderr_path="error.txt") + shell_launch_cmd = _as_srun_command( + args=SlurmLaunchArguments(args), + exe=mock_echo_executable, + path=test_dir, + env={}, + stdout_path="output.txt", + stderr_path="error.txt", + ) assert isinstance(shell_launch_cmd, ShellLauncherCommand) print(shell_launch_cmd.command_tuple) assert shell_launch_cmd.command_tuple == expected diff --git a/tests/test_experiment.py b/tests/test_experiment.py index ad22a53c7..6e4fd8a7f 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -53,7 +53,11 @@ def experiment(monkeypatch, test_dir, dispatcher): """ exp = Experiment(f"test-exp-{uuid.uuid4()}", test_dir) monkeypatch.setattr(dispatch, "DEFAULT_DISPATCHER", dispatcher) - monkeypatch.setattr(exp, "_generate", lambda gen, job, idx: ("/tmp/job", "/tmp/job/out.txt", "/tmp/job/err.txt")) + monkeypatch.setattr( + exp, + "_generate", + lambda gen, job, idx: ("/tmp/job", "/tmp/job/out.txt", "/tmp/job/err.txt"), + ) yield exp @@ -64,7 +68,9 @@ def dispatcher(): """ d = dispatch.Dispatcher() to_record: dispatch._FormatterType[MockLaunchArgs, LaunchRecord] = ( - lambda settings, exe, path, env, out, err: LaunchRecord(settings, exe, env, path, out, err) + lambda settings, exe, path, env, out, err: LaunchRecord( + settings, exe, env, path, out, err + ) ) d.dispatch(MockLaunchArgs, with_format=to_record, to_launcher=NoOpRecordLauncher) yield d diff --git a/tests/test_generator.py b/tests/test_generator.py index 1c82a4175..602f300ea 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -14,8 +14,8 @@ from smartsim.entity import Application, Ensemble from smartsim.entity.files import EntityFiles from smartsim.launchable import Job -from smartsim.settings import LaunchSettings -from smartsim.settings import dispatch +from smartsim.settings import LaunchSettings, dispatch + # TODO Add JobGroup tests when JobGroup becomes a Launchable pytestmark = pytest.mark.group_a diff --git a/tests/test_shell_launcher.py b/tests/test_shell_launcher.py index 7d5641bcf..3eb6a7766 100644 --- a/tests/test_shell_launcher.py +++ b/tests/test_shell_launcher.py @@ -24,39 +24,41 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import tempfile -import unittest.mock -import pytest -import subprocess -import pathlib -import psutil import difflib import os +import pathlib +import subprocess +import tempfile +import unittest.mock import uuid import weakref -from smartsim.entity import _mock, entity, Application + +import psutil +import pytest + from smartsim import Experiment +from smartsim._core.commands import Command +from smartsim._core.utils import helpers +from smartsim._core.utils.shell import * +from smartsim.entity import Application, _mock, entity +from smartsim.error.errors import LauncherJobNotFound +from smartsim.launchable import Job from smartsim.settings import LaunchSettings -from smartsim.settings.dispatch import ShellLauncher from smartsim.settings.arguments.launch.slurm import ( SlurmLaunchArguments, _as_srun_command, ) -from smartsim.status import JobStatus -from smartsim._core.utils.shell import * -from smartsim._core.commands import Command -from smartsim._core.utils import helpers -from smartsim.settings.dispatch import sp, ShellLauncher, ShellLauncherCommand +from smartsim.settings.dispatch import ShellLauncher, ShellLauncherCommand, sp from smartsim.settings.launchCommand import LauncherType -from smartsim.launchable import Job +from smartsim.status import JobStatus from smartsim.types import LaunchedJobID -from smartsim.error.errors import LauncherJobNotFound # TODO tests bad vars in Popen call at beginning - # tests -> helper.exe : pass in None, empty str, path with a space at beginning, a non valid command - # -> write a test for the invalid num of items - test_shell_launcher_fails_on_any_invalid_len_input - # -> have border tests for 0,1,4,6 cmd vals -> work correctly without them -> raise ValueError - # do all of the failures as well as the sucess criteria +# tests -> helper.exe : pass in None, empty str, path with a space at beginning, a non valid command +# -> write a test for the invalid num of items - test_shell_launcher_fails_on_any_invalid_len_input +# -> have border tests for 0,1,4,6 cmd vals -> work correctly without them -> raise ValueError +# do all of the failures as well as the sucess criteria + class EchoHelloWorldEntity(entity.SmartSimEntity): """A simple smartsim entity that meets the `ExecutableProtocol` protocol""" @@ -72,32 +74,40 @@ def __eq__(self, other): def as_program_arguments(self): return (helpers.expand_exe_path("echo"), "Hello", "World!") + def create_directory(directory_path: str) -> pathlib.Path: """Creates the execution directory for testing.""" tmp_dir = pathlib.Path(directory_path) tmp_dir.mkdir(exist_ok=True, parents=True) return tmp_dir + def generate_output_files(tmp_dir: pathlib.Path): """Generates output and error files within the run directory for testing.""" out_file = tmp_dir / "tmp.out" err_file = tmp_dir / "tmp.err" return out_file, err_file + def generate_directory(test_dir: str): """Generates a execution directory, output file, and error file for testing.""" execution_dir = create_directory(os.path.join(test_dir, "tmp")) out_file, err_file = generate_output_files(execution_dir) return execution_dir, out_file, err_file + @pytest.fixture def shell_cmd(test_dir: str) -> ShellLauncherCommand: """Fixture to create an instance of Generator.""" run_dir, out_file, err_file = generate_directory(test_dir) - return ShellLauncherCommand({}, run_dir, out_file, err_file, EchoHelloWorldEntity().as_program_arguments()) + return ShellLauncherCommand( + {}, run_dir, out_file, err_file, EchoHelloWorldEntity().as_program_arguments() + ) + # UNIT TESTS + def test_shell_launcher_command_init(shell_cmd: ShellLauncherCommand, test_dir: str): """Test that ShellLauncherCommand initializes correctly""" assert shell_cmd.env == {} @@ -106,11 +116,13 @@ def test_shell_launcher_command_init(shell_cmd: ShellLauncherCommand, test_dir: assert shell_cmd.stderr == shell_cmd.path / "tmp.err" assert shell_cmd.command_tuple == EchoHelloWorldEntity().as_program_arguments() + def test_shell_launcher_init(): """Test that ShellLauncher initializes correctly""" shell_launcher = ShellLauncher() assert shell_launcher._launched == {} + def test_shell_launcher_start_calls_popen(shell_cmd: ShellLauncherCommand): """Test that the process leading up to the shell launcher popen call was correct""" shell_launcher = ShellLauncher() @@ -118,6 +130,7 @@ def test_shell_launcher_start_calls_popen(shell_cmd: ShellLauncherCommand): _ = shell_launcher.start(shell_cmd) mock_open.assert_called_once() + def test_shell_launcher_start_calls_popen_with_value(shell_cmd: ShellLauncherCommand): """Test that popen was called with correct values""" shell_launcher = ShellLauncher() @@ -131,12 +144,22 @@ def test_shell_launcher_start_calls_popen_with_value(shell_cmd: ShellLauncherCom stderr=shell_cmd.stderr, ) + def test_popen_returns_popen_object(test_dir: str): """Test that the popen call returns a popen object""" shell_launcher = ShellLauncher() run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: - cmd = ShellLauncherCommand({}, run_dir, subprocess.DEVNULL, subprocess.DEVNULL, EchoHelloWorldEntity().as_program_arguments()) + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): + cmd = ShellLauncherCommand( + {}, + run_dir, + subprocess.DEVNULL, + subprocess.DEVNULL, + EchoHelloWorldEntity().as_program_arguments(), + ) id = shell_launcher.start(cmd) proc = shell_launcher._launched[id] assert isinstance(proc, sp.Popen) @@ -146,8 +169,13 @@ def test_popen_writes_to_output_file(test_dir: str): """Test that popen writes to .out file upon successful process call""" shell_launcher = ShellLauncher() run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: - cmd = ShellLauncherCommand({}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments()) + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): + cmd = ShellLauncherCommand( + {}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments() + ) id = shell_launcher.start(cmd) val = shell_launcher.get_status(id) print(val) @@ -167,7 +195,10 @@ def test_popen_fails_with_invalid_cmd(test_dir): """Test that popen returns a non zero returncode after failure""" shell_launcher = ShellLauncher() run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): args = (helpers.expand_exe_path("srun"), "--flag_dne") cmd = ShellLauncherCommand({}, run_dir, out, err, args) id = shell_launcher.start(cmd) @@ -185,8 +216,13 @@ def test_popen_issues_unique_ids(test_dir): """Validate that all ids are unique within ShellLauncher._launched""" shell_launcher = ShellLauncher() run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: - cmd = ShellLauncherCommand({}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments()) + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): + cmd = ShellLauncherCommand( + {}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments() + ) for _ in range(5): _ = shell_launcher.start(cmd) assert len(shell_launcher._launched) == 5 @@ -204,8 +240,13 @@ def test_shell_launcher_returns_complete_status(test_dir): """Test tht ShellLauncher returns the status of completed Jobs""" shell_launcher = ShellLauncher() run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: - cmd = ShellLauncherCommand({}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments()) + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): + cmd = ShellLauncherCommand( + {}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments() + ) for _ in range(5): id = shell_launcher.start(cmd) proc = shell_launcher._launched[id] @@ -214,13 +255,17 @@ def test_shell_launcher_returns_complete_status(test_dir): val = list(code.keys())[0] assert code[val] == JobStatus.COMPLETED + def test_shell_launcher_returns_failed_status(test_dir): """Test tht ShellLauncher returns the status of completed Jobs""" # Init ShellLauncher shell_launcher = ShellLauncher() # Generate testing directory run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): # Construct a invalid command to execute args = (helpers.expand_exe_path("srun"), "--flag_dne") cmd = ShellLauncherCommand({}, run_dir, out, err, args) @@ -244,9 +289,14 @@ def test_shell_launcher_returns_running_status(test_dir): shell_launcher = ShellLauncher() # Generate testing directory run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): # Construct a command to execute - cmd = ShellLauncherCommand({}, run_dir, out, err, (helpers.expand_exe_path("sleep"), "5")) + cmd = ShellLauncherCommand( + {}, run_dir, out, err, (helpers.expand_exe_path("sleep"), "5") + ) # Start the execution of the command using a ShellLauncher for _ in range(5): id = shell_launcher.start(cmd) @@ -255,7 +305,7 @@ def test_shell_launcher_returns_running_status(test_dir): val = list(code.keys())[0] # Assert that subprocess has completed assert code[val] == JobStatus.RUNNING - + @pytest.mark.parametrize( "psutil_status,job_status", @@ -274,14 +324,19 @@ def test_shell_launcher_returns_running_status(test_dir): pytest.param(psutil.STATUS_ZOMBIE, JobStatus.COMPLETED, id="merp"), ], ) -def test_this(psutil_status,job_status,monkeypatch: pytest.MonkeyPatch, test_dir): +def test_this(psutil_status, job_status, monkeypatch: pytest.MonkeyPatch, test_dir): shell_launcher = ShellLauncher() run_dir, out_file, err_file = generate_directory(test_dir) - with open(out_file, "w", encoding="utf-8") as out, open(err_file, "w", encoding="utf-8") as err: - cmd = ShellLauncherCommand({}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments()) + with ( + open(out_file, "w", encoding="utf-8") as out, + open(err_file, "w", encoding="utf-8") as err, + ): + cmd = ShellLauncherCommand( + {}, run_dir, out, err, EchoHelloWorldEntity().as_program_arguments() + ) id = shell_launcher.start(cmd) proc = shell_launcher._launched[id] monkeypatch.setattr(proc, "poll", lambda: None) monkeypatch.setattr(psutil.Process, "status", lambda self: psutil_status) value = shell_launcher.get_status(id) - assert value.get(id) == job_status \ No newline at end of file + assert value.get(id) == job_status