From f90a2856da9395c3fa37a2ada803748c9fc4a145 Mon Sep 17 00:00:00 2001 From: Amanda Richardson Date: Tue, 6 Aug 2024 18:47:54 -0500 Subject: [PATCH] addressing all of matts comments besides 1 --- smartsim/_core/entrypoints/file_operations.py | 4 + smartsim/_core/generation/generator.py | 77 ++---- smartsim/_core/utils/helpers.py | 13 + smartsim/entity/model.py | 1 - smartsim/experiment.py | 61 ++++- smartsim/launchable/job.py | 15 +- smartsim/launchable/jobGroup.py | 9 + smartsim/settings/dispatch.py | 5 +- tests/temp_tests/test_jobGroup.py | 23 +- tests/temp_tests/test_launchable.py | 12 + tests/test_experiment.py | 7 +- tests/test_generator.py | 233 +++++++++--------- 12 files changed, 262 insertions(+), 198 deletions(-) diff --git a/smartsim/_core/entrypoints/file_operations.py b/smartsim/_core/entrypoints/file_operations.py index 4271c2a63..beb68efce 100644 --- a/smartsim/_core/entrypoints/file_operations.py +++ b/smartsim/_core/entrypoints/file_operations.py @@ -128,11 +128,15 @@ def copy(parsed_args: argparse.Namespace) -> None: FileExistsError will be raised """ if os.path.isdir(parsed_args.source): + print("here") + print(parsed_args.source) + print(parsed_args.dest) shutil.copytree( parsed_args.source, parsed_args.dest, dirs_exist_ok=parsed_args.dirs_exist_ok, ) + print(os.listdir(parsed_args.dest)) else: shutil.copy(parsed_args.source, parsed_args.dest) diff --git a/smartsim/_core/generation/generator.py b/smartsim/_core/generation/generator.py index e25262c5e..174a4d2d9 100644 --- a/smartsim/_core/generation/generator.py +++ b/smartsim/_core/generation/generator.py @@ -51,21 +51,16 @@ class Generator: files into the Job directory. """ - def __init__(self, exp_path: str, run_id: str) -> None: + def __init__(self, root: str | os.PathLike[str]) -> None: """Initialize a generator object - The Generator class is responsible for creating Job directories. + TODO The Generator class is responsible for creating Job directories. It ensures that paths adhere to SmartSim path standards. Additionally, it creates a run directory to handle symlinking, configuration, and file copying to the job directory. - - :param gen_path: Path in which files need to be generated - :param run_ID: The id of the Experiment """ - self.exp_path = pathlib.Path(exp_path) - """The path under which the experiment operate""" - self.run_id = run_id - """The runID for Experiment.start""" + self.root = root + """The root path under which to generate files""" def log_file(self, log_path: pathlib.Path) -> str: """Returns the location of the file @@ -77,14 +72,12 @@ def log_file(self, log_path: pathlib.Path) -> str: """ return join(log_path, "smartsim_params.txt") - def generate_job(self, job: Job, job_index: int) -> pathlib.Path: - """Generate the Job directory - Generate the file structure for a SmartSim Job. This - includes writing and configuring input files for the entity. + def generate_job(self, job: Job, job_path: str, log_path: str): + """Write and configure input files for a Job. To have files or directories present in the created Job - directories, such as datasets or input files, call + directory, such as datasets or input files, call ``entity.attach_generator_files`` prior to generation. Tagged application files are read, checked for input variables to @@ -92,52 +85,20 @@ def generate_job(self, job: Job, job_index: int) -> pathlib.Path: specified with a tag within the input file itself. The default tag is surronding an input value with semicolons. e.g. ``THERMO=;90;`` - + + :param job: The job instance to write and configure files for. + :param job_path: The path to the \"run\" directory for the job instance. + :param log_path: The path to the \"log\" directory for the job instance. """ - # Generate ../job_name/run directory - job_path = self._generate_job_path(job, job_index) - # Generate ../job_name/log directory - log_path = self._generate_log_path(job, job_index) # Create and write to the parameter settings file 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") - # Perform file system ops + # Perform file system operations on attached files self._build_operations(job, job_path) - # Return Job path - return job_path - - def _generate_job_path(self, job: Job, job_index: int) -> pathlib.Path: - """ - Generate the run directory for a Job. - - :param job: The Job to generate a directory - :returns pathlib.Path:: The generated run path for the Job - """ - job_type = f"{job.__class__.__name__.lower()}s" - job_path = ( - self.exp_path / self.run_id / job_type / f"{job.name}-{job_index}" / "run" - ) - # Create Job directory - job_path.mkdir(exist_ok=True, parents=True) - return job_path - - def _generate_log_path(self, job: Job, job_index: int) -> pathlib.Path: - """ - Generate the log directory for a Job. - - :param job: The Job to generate a directory - :returns pathlib.Path:: The generated log path for the Job - """ - job_type = f"{job.__class__.__name__.lower()}s" - log_path = ( - self.exp_path / self.run_id / job_type / f"{job.name}-{job_index}" / "log" - ) - log_path.mkdir(exist_ok=True, parents=True) - return log_path def _build_operations(self, job: Job, job_path: pathlib.Path) -> None: """This method orchestrates file system ops for the attached SmartSim entity. @@ -151,7 +112,7 @@ def _build_operations(self, job: Job, job_path: pathlib.Path) -> None: app = t.cast(Application, job.entity) self._copy_files(app.files, job_path) self._symlink_files(app.files, job_path) - self._write_tagged_files(app, job_path) + self._write_tagged_files(app.files, app.params, job_path) @staticmethod def _copy_files(files: t.Union[EntityFiles, None], dest: pathlib.Path) -> None: @@ -217,7 +178,7 @@ def _symlink_files(files: t.Union[EntityFiles, None], dest: pathlib.Path) -> Non ) @staticmethod - def _write_tagged_files(app: Application, dest: pathlib.Path) -> None: + def _write_tagged_files(files: t.Union[EntityFiles, None], params: t.Mapping[str, str], dest: pathlib.Path) -> None: """Read, configure and write the tagged input files for a Job instance. This function specifically deals with the tagged files attached to an entity. @@ -226,9 +187,9 @@ def _write_tagged_files(app: Application, dest: pathlib.Path) -> None: :param dest: Path to the Jobs run directory """ # Return if no files are attached - if app.files is None: + if files is None: return - if app.files.tagged: + if files.tagged: to_write = [] def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None: @@ -247,11 +208,11 @@ def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None: mkdir(path.join(dest, tagged.base, path.basename(tagged_dir.base))) _build_tagged_files(tagged_dir) - if app.files.tagged_hierarchy: - _build_tagged_files(app.files.tagged_hierarchy) + if files.tagged_hierarchy: + _build_tagged_files(files.tagged_hierarchy) # Pickle the dictionary - pickled_dict = pickle.dumps(app.params) + pickled_dict = pickle.dumps(params) # Default tag delimiter tag = ";" # Encode the pickled dictionary with Base64 diff --git a/smartsim/_core/utils/helpers.py b/smartsim/_core/utils/helpers.py index d193b6604..af6c97c46 100644 --- a/smartsim/_core/utils/helpers.py +++ b/smartsim/_core/utils/helpers.py @@ -50,6 +50,19 @@ _T = t.TypeVar("_T") _TSignalHandlerFn = t.Callable[[int, t.Optional["FrameType"]], object] +def check_name(name: str) -> None: + """ + Checks if the input name is valid. + + :param name: The name to be checked. + + :raises ValueError: If the name contains the path separator (os.path.sep). + :raises ValueError: If the name is an empty string. + """ + if os.path.sep in name: + raise ValueError("Invalid input: String contains the path separator.") + if name == "": + raise ValueError("Invalid input: Name cannot be an empty string.") def unpack_fs_identifier(fs_id: str, token: str) -> t.Tuple[str, str]: """Unpack the unformatted feature store identifier diff --git a/smartsim/entity/model.py b/smartsim/entity/model.py index 1f54bf6e3..a1186cedd 100644 --- a/smartsim/entity/model.py +++ b/smartsim/entity/model.py @@ -226,7 +226,6 @@ def attach_generator_files( "`smartsim_params.txt` is a file automatically " + "generated by SmartSim and cannot be ovewritten." ) - # files is not a list of entity files self.files = EntityFiles(to_configure, to_copy, to_symlink) @property diff --git a/smartsim/experiment.py b/smartsim/experiment.py index ced006ff4..1deedb24b 100644 --- a/smartsim/experiment.py +++ b/smartsim/experiment.py @@ -181,10 +181,11 @@ def start(self, *jobs: Job) -> tuple[LaunchedJobID, ...]: jobs that can be used to query or alter the status of that particular execution of the job. """ - run_id = datetime.datetime.now().strftime("run-%H:%M:%ST%Y-%m-%d") + run_id = datetime.datetime.now().replace(microsecond=0).isoformat() + root = pathlib.Path(self.exp_path, run_id) """Create the run id for Experiment.start""" return self._dispatch( - Generator(self.exp_path, run_id), dispatch.DEFAULT_DISPATCHER, *jobs + Generator(root), dispatch.DEFAULT_DISPATCHER, *jobs ) def _dispatch( @@ -196,8 +197,8 @@ def _dispatch( ) -> tuple[LaunchedJobID, ...]: """Dispatch a series of jobs with a particular dispatcher - :param generator: The Generator holds the run_id and experiment - path for use when producing job directories. + :param generator: The generator is responsible for creating the + job run and log directory. :param dispatcher: The dispatcher that should be used to determine how to start a job based on its launch settings. :param job: The first job instance to dispatch @@ -208,7 +209,6 @@ def _dispatch( """ def execute_dispatch(generator: Generator, job: Job, idx: int) -> LaunchedJobID: - print(job) args = job.launch_settings.launch_args env = job.launch_settings.env_vars # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -259,18 +259,59 @@ def _generate(self, generator: Generator, job: Job, job_index: int) -> pathlib.P An instance of ``Generator`` and ``Job`` can be passed as an argument to the protected _generate member. - :param generator: Generator that holds the run_id and experiment - path for use when producing the job directory. - :param job: Job to generate file structure. - :returns: The generated Job path. + :param generator: The generator is responsible for creating the job run and log directory. + :param job: The job instance for which the output is generated. + :param job_index: The index of the job instance (used for naming). + :returns: The path to the generated output for the job instance. + :raises: A SmartSimError if an error occurs during the generation process. """ + # Generate ../job_name/run directory + job_path = self._generate_job_path(job, job_index, generator.root) + # Generate ../job_name/log directory + log_path = self._generate_log_path(job, job_index, generator.root) try: - job_path = generator.generate_job(job, job_index) + generator.generate_job(job, job_path, log_path) return job_path except SmartSimError as e: logger.error(e) raise + def _generate_job_root(self, job: Job, job_index: int, root: str) -> pathlib.Path: + """Generates the root directory for a specific job instance. + + :param job: The Job instance for which the root directory is generated. + :param job_index: The index of the Job instance (used for naming). + :returns: The path to the root directory for the Job instance. + """ + job_type = f"{job.__class__.__name__.lower()}s" + job_path = root / f"{job_type}/{job.name}-{job_index}" + job_path.mkdir(exist_ok=True, parents=True) + return job_path + + def _generate_job_path(self, job: Job, job_index: int, root: str) -> pathlib.Path: + """Generates the path for the \"run\" directory within the root directory + of a specific job instance. + + :param job (Job): The job instance for which the path is generated. + :param job_index (int): The index of the job instance (used for naming). + :returns: The path to the \"run\" directory for the job instance. + """ + path = self._generate_job_root(job, job_index, root) / "run" + path.mkdir(exist_ok=False, parents=True) + return path + + def _generate_log_path(self, job: Job, job_index: int, root: str) -> pathlib.Path: + """ + Generates the path for the \"log\" directory within the root directory of a specific job instance. + + :param job: The job instance for which the path is generated. + :param job_index: The index of the job instance (used for naming). + :returns: The path to the \"log\" directory for the job instance. + """ + path = self._generate_job_root(job, job_index, root) / "log" + path.mkdir(exist_ok=False, parents=True) + return path + def preview( self, *args: t.Any, diff --git a/smartsim/launchable/job.py b/smartsim/launchable/job.py index c2c8581b2..dc0f02c87 100644 --- a/smartsim/launchable/job.py +++ b/smartsim/launchable/job.py @@ -27,11 +27,13 @@ from __future__ import annotations import typing as t +import os from copy import deepcopy from smartsim._core.commands.launchCommands import LaunchCommands from smartsim.launchable.basejob import BaseJob from smartsim.settings import LaunchSettings +from smartsim._core.utils.helpers import check_name if t.TYPE_CHECKING: from smartsim.entity.entity import SmartSimEntity @@ -50,11 +52,12 @@ def __init__( self, entity: SmartSimEntity, launch_settings: LaunchSettings, - name: str | None = None, + name: str | None = "job", ): super().__init__() self._entity = deepcopy(entity) self._launch_settings = deepcopy(launch_settings) + check_name(name) self._name = name if name else entity.name @property @@ -62,20 +65,30 @@ def name(self) -> str: """Retrieves the name of the Job.""" return self._name + @name.setter + def name(self, name: str) -> None: + """Sets the name of the Job.""" + check_name(name) + self._entity = name + @property def entity(self) -> SmartSimEntity: + """Retrieves the Job entity.""" return deepcopy(self._entity) @entity.setter def entity(self, value: SmartSimEntity) -> None: + """Sets the Job entity.""" self._entity = deepcopy(value) @property def launch_settings(self) -> LaunchSettings: + """Retrieves the Job LaunchSettings.""" return deepcopy(self._launch_settings) @launch_settings.setter def launch_settings(self, value: LaunchSettings) -> None: + """Sets the Job LaunchSettings.""" self._launch_settings = deepcopy(value) def get_launch_steps(self) -> LaunchCommands: diff --git a/smartsim/launchable/jobGroup.py b/smartsim/launchable/jobGroup.py index 1a92caf54..760fd5789 100644 --- a/smartsim/launchable/jobGroup.py +++ b/smartsim/launchable/jobGroup.py @@ -6,6 +6,8 @@ from .basejob import BaseJob from .baseJobGroup import BaseJobGroup +from .._core.utils.helpers import check_name + if t.TYPE_CHECKING: from typing_extensions import Self @@ -23,12 +25,19 @@ def __init__( ) -> None: super().__init__() self._jobs = deepcopy(jobs) + check_name(name) self._name = name @property def name(self) -> str: """Retrieves the name of the JobGroup.""" return self._name + + @name.setter + def name(self, name: str) -> None: + """Sets the name of the JobGroup.""" + check_name(name) + self._entity = name @property def jobs(self) -> t.List[BaseJob]: diff --git a/smartsim/settings/dispatch.py b/smartsim/settings/dispatch.py index 5766c0780..47ccfe6fb 100644 --- a/smartsim/settings/dispatch.py +++ b/smartsim/settings/dispatch.py @@ -46,9 +46,7 @@ _T_contra = t.TypeVar("_T_contra", contravariant=True) _WorkingDirectory: TypeAlias = t.Union[str, os.PathLike[str]] -"""A type alias for a Jobs working directory. Paths may be strings or -PathLike objects. -""" +"""A working directory represented as a string or PathLike object""" _DispatchableT = t.TypeVar("_DispatchableT", bound="LaunchArguments") """Any type of luanch arguments, typically used when the type bound by the type @@ -453,7 +451,6 @@ class ShellLauncher: def __init__(self) -> None: self._launched: dict[LaunchedJobID, sp.Popen[bytes]] = {} - # TODO inject path here def start( self, command: tuple[str | os.PathLike[str], t.Sequence[str]] ) -> LaunchedJobID: diff --git a/tests/temp_tests/test_jobGroup.py b/tests/temp_tests/test_jobGroup.py index b129adb8d..e77041af8 100644 --- a/tests/temp_tests/test_jobGroup.py +++ b/tests/temp_tests/test_jobGroup.py @@ -44,25 +44,34 @@ def get_launch_steps(self): raise NotImplementedError +def test_invalid_job_name(wlmutils): + job_1 = Job(app_1, wlmutils.get_test_launcher()) + job_2 = Job(app_2,wlmutils.get_test_launcher()) + with pytest.raises(ValueError): + _ = JobGroup([job_1, job_2], name="") + with pytest.raises(ValueError): + _ = JobGroup([job_1, job_2], name="name/not/allowed") + + def test_create_JobGroup(): job_1 = MockJob() job_group = JobGroup([job_1]) assert len(job_group) == 1 -def test_getitem_JobGroup(): - job_1 = Job(app_1, LaunchSettings("slurm")) - job_2 = Job(app_2, LaunchSettings("slurm")) +def test_getitem_JobGroup(wlmutils): + job_1 = Job(app_1, wlmutils.get_test_launcher()) + job_2 = Job(app_2, wlmutils.get_test_launcher()) job_group = JobGroup([job_1, job_2]) get_value = job_group[0].entity.name assert get_value == job_1.entity.name -def test_setitem_JobGroup(): - job_1 = Job(app_1, LaunchSettings("slurm")) - job_2 = Job(app_2, LaunchSettings("slurm")) +def test_setitem_JobGroup(wlmutils): + job_1 = Job(app_1, wlmutils.get_test_launcher()) + job_2 = Job(app_2, wlmutils.get_test_launcher()) job_group = JobGroup([job_1, job_2]) - job_3 = Job(app_3, LaunchSettings("slurm")) + job_3 = Job(app_3, wlmutils.get_test_launcher()) job_group[1] = job_3 assert len(job_group) == 2 get_value = job_group[1] diff --git a/tests/temp_tests/test_launchable.py b/tests/temp_tests/test_launchable.py index fed75b7d0..b3889fb67 100644 --- a/tests/temp_tests/test_launchable.py +++ b/tests/temp_tests/test_launchable.py @@ -49,6 +49,18 @@ def test_launchable_init(): launchable = Launchable() assert isinstance(launchable, Launchable) +def test_invalid_job_name(wlmutils): + entity = Application( + "test_name", + run_settings=LaunchSettings(wlmutils.get_test_launcher()), + exe="echo", + exe_args=["spam", "eggs"], + ) + settings = LaunchSettings(wlmutils.get_test_launcher()) + with pytest.raises(ValueError): + _ = Job(entity, settings, name="") + with pytest.raises(ValueError): + _ = Job(entity, settings, name="path/to/name") def test_job_init(): entity = Application( diff --git a/tests/test_experiment.py b/tests/test_experiment.py index 474eb0aa8..6a2c20b99 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -28,14 +28,11 @@ import dataclasses import itertools -import tempfile import typing as t import uuid -import weakref import pytest -from smartsim._core.generation import Generator from smartsim.entity import _mock, entity from smartsim.experiment import Experiment from smartsim.launchable import job @@ -52,7 +49,7 @@ 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", "1") + monkeypatch.setattr(exp, "_generate", lambda gen, job, idx: "/tmp/job") yield exp @@ -180,7 +177,6 @@ class EchoHelloWorldEntity(entity.SmartSimEntity): """A simple smartsim entity that meets the `ExecutableProtocol` protocol""" def __init__(self): - path = tempfile.TemporaryDirectory() super().__init__("test-entity", _mock.Mock()) def __eq__(self, other): @@ -215,7 +211,6 @@ def test_start_can_launch_jobs( num_jobs: int, ) -> None: jobs = make_jobs(job_maker, num_jobs) - print(jobs) assert len(experiment._active_launchers) == 0, "Initialized w/ launchers" launched_ids = experiment.start(*jobs) assert len(experiment._active_launchers) == 1, "Unexpected number of launchers" diff --git a/tests/test_generator.py b/tests/test_generator.py index 04e104dc1..5f0941c09 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -4,6 +4,7 @@ from glob import glob from os import listdir from os import path as osp +import itertools import pytest @@ -20,42 +21,25 @@ @pytest.fixture -def get_gen_copy_file(fileutils): - """Fixture to yield directory to copy.""" +def get_gen_copy_dir(fileutils): yield fileutils.get_test_conf_path(osp.join("generator_files", "to_copy_dir")) @pytest.fixture -def get_gen_symlink_file(fileutils): - """Fixture to yield directory to symlink.""" +def get_gen_symlink_dir(fileutils): yield fileutils.get_test_conf_path(osp.join("generator_files", "to_symlink_dir")) @pytest.fixture -def get_gen_configure_file(fileutils): - """Fixture to yield directory to symlink.""" +def get_gen_configure_dir(fileutils): yield fileutils.get_test_conf_path(osp.join("generator_files", "tag_dir_template")) -@pytest.fixture -def echo_app(): - """Fixture to yield an instance of SmartSimEntity.""" - yield SmartSimEntity("echo_app", run_settings=_mock.Mock()) - - @pytest.fixture def generator_instance(test_dir) -> Generator: """Fixture to create an instance of Generator.""" - experiment_path = osp.join(test_dir, "experiment_name") - yield Generator(exp_path=experiment_path, run_id="mock_run") - - -@pytest.fixture -def job_instance(wlmutils, echo_app) -> Job: - """Fixture to create an instance of Job.""" - launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job = Job(echo_app, launch_settings) - return job + root = pathlib.Path(test_dir, "temp_id") + yield Generator(root=root) def test_log_file_path(generator_instance): @@ -65,131 +49,145 @@ def test_log_file_path(generator_instance): assert generator_instance.log_file(base_path) == expected_path -def test_generate_job_directory(test_dir, wlmutils): +def test_generate_job_directory(test_dir, wlmutils, generator_instance): """Test Generator.generate_job""" - # Experiment path - experiment_path = osp.join(test_dir, "experiment_name") # Create Job launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - app = Application("app_name", exe="python", run_settings="RunSettings") + app = Application("app_name", exe="python", run_settings="RunSettings") # Mock RunSettings job = Job(app, launch_settings) - # Mock start id + # Mock id run_id = "mock_run" - # Generator instance - gen = Generator(exp_path=experiment_path, run_id=run_id) - # Call Generator.generate_job - job_path = gen.generate_job(job, 1) - assert isinstance(job_path, pathlib.Path) - expected_run_path = ( - pathlib.Path(experiment_path) - / run_id - / f"{job.__class__.__name__.lower()}s" - / f"{app.name}-{1}" + # Create run directory + run_path = ( + generator_instance.root / "run" ) - assert job_path == expected_run_path - expected_log_path = ( - pathlib.Path(experiment_path) - / run_id - / f"{job.__class__.__name__.lower()}s" - / f"{app.name}-{1}" + run_path.mkdir(parents=True) + assert osp.isdir(run_path) + # Create log directory + log_path = ( + generator_instance.root / "log" ) - assert osp.isdir(expected_run_path) - assert osp.isdir(expected_log_path) - assert osp.isfile(osp.join(expected_log_path, "smartsim_params.txt")) - - -def test_exp_private_generate_method_app(wlmutils, test_dir, generator_instance): - """Test that Job directory was created from Experiment.""" + log_path.mkdir(parents=True) + assert osp.isdir(log_path) + # Call Generator.generate_job + generator_instance.generate_job(job, run_path, log_path) + # Assert smartsim params file created + assert osp.isfile(osp.join(log_path, "smartsim_params.txt")) + # Assert smartsim params correctly written to + with open(log_path / "smartsim_params.txt", 'r') as file: + content = file.read() + assert "Generation start date and time:" in content + + +def test_exp_private_generate_method(wlmutils, test_dir, generator_instance): + """Test that Job directory was created from Experiment._generate.""" + # Create Experiment exp = Experiment(name="experiment_name", exp_path=test_dir) - app = Application("name", "python", "RunSettings") + # Create Job + app = Application("name", "python", run_settings="RunSettings") # Mock RunSettings launch_settings = LaunchSettings(wlmutils.get_test_launcher()) job = Job(app, launch_settings) - job_execution_path = exp._generate(generator_instance, job, 1) + # Generate Job directory + job_index = 1 + job_execution_path = exp._generate(generator_instance, job, job_index) + # Assert Job run directory exists assert osp.isdir(job_execution_path) + # Assert Job log directory exists head, _ = os.path.split(job_execution_path) expected_log_path = pathlib.Path(head) / "log" assert osp.isdir(expected_log_path) -def test_generate_copy_file(fileutils, wlmutils, test_dir): - # Create the Job and attach generator file +def test_generate_copy_file(fileutils, wlmutils, generator_instance): + """Test that attached copy files are copied into Job directory""" + # Create the Job and attach copy generator file launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - app = Application("name", "python", "RunSettings") + app = Application("name", "python", run_settings="RunSettings") # Mock RunSettings script = fileutils.get_test_conf_path("sleep.py") app.attach_generator_files(to_copy=script) job = Job(app, launch_settings) - # Create the experiment - experiment_path = osp.join(test_dir, "experiment_name") - gen = Generator(exp_path=experiment_path, run_id="temp_run") - path = gen.generate_job(job, 1) - expected_file = pathlib.Path(path) / "sleep.py" + # Call Generator.generate_job + run_path = generator_instance.root / "run" + run_path.mkdir(parents=True) + log_path = generator_instance.root / "log" + log_path.mkdir(parents=True) + generator_instance.generate_job(job, run_path, log_path) + expected_file = run_path / "sleep.py" assert osp.isfile(expected_file) -def test_generate_copy_directory(wlmutils, test_dir, get_gen_copy_file): +def test_generate_copy_directory(wlmutils, get_gen_copy_dir, generator_instance): # Create the Job and attach generator file launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - app = Application("name", "python", "RunSettings") - app.attach_generator_files(to_copy=get_gen_copy_file) + app = Application("name", "python", run_settings="RunSettings") # Mock RunSettings + app.attach_generator_files(to_copy=get_gen_copy_dir) + print(f"what is this: {get_gen_copy_dir}") job = Job(app, launch_settings) - # Create the experiment - experiment_path = osp.join(test_dir, "experiment_name") - gen = Generator(exp_path=experiment_path, run_id="temp_run") - path = gen.generate_job(job, 1) - expected_file = pathlib.Path(path) / "mock.txt" + # Call Generator.generate_job + run_path = generator_instance.root / "run" + run_path.mkdir(parents=True) + log_path = generator_instance.root / "log" + log_path.mkdir(parents=True) + generator_instance.generate_job(job, run_path, log_path) + expected_file = run_path / "mock.txt" assert osp.isfile(expected_file) -def test_generate_symlink_directory(wlmutils, test_dir, get_gen_symlink_file): +def test_generate_symlink_directory(wlmutils, generator_instance, get_gen_symlink_dir): # Create the Job and attach generator file launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - app = Application("name", "python", "RunSettings") - # Path of directory to symlink - symlink_dir = get_gen_symlink_file + app = Application("name", "python", run_settings="RunSettings") # Mock RunSettings # Attach directory to Application - app.attach_generator_files(to_symlink=symlink_dir) + app.attach_generator_files(to_symlink=get_gen_symlink_dir) # Create Job job = Job(app, launch_settings) - # Create the experiment - experiment_path = osp.join(test_dir, "experiment_name") - gen = Generator(exp_path=experiment_path, run_id="temp_run") - # Generate Experiment file structure - job_path = gen.generate_job(job, 1) - expected_folder = pathlib.Path(job_path) / "to_symlink_dir" + # Call Generator.generate_job + run_path = generator_instance.root / "run" + run_path.mkdir(parents=True) + log_path = generator_instance.root / "log" + log_path.mkdir(parents=True) + generator_instance.generate_job(job, run_path, log_path) + expected_folder = run_path / "to_symlink_dir" assert osp.isdir(expected_folder) + assert expected_folder.is_symlink() + assert os.fspath(expected_folder.resolve()) == osp.realpath(get_gen_symlink_dir) # Combine symlinked file list and original file list for comparison - for written, correct in zip(listdir(symlink_dir), listdir(expected_folder)): + for written, correct in itertools.zip_longest(listdir(get_gen_symlink_dir), listdir(expected_folder)): # For each pair, check if the filenames are equal assert written == correct -def test_generate_symlink_file(get_gen_symlink_file, wlmutils, test_dir): +def test_generate_symlink_file(get_gen_symlink_dir, wlmutils, generator_instance): # Create the Job and attach generator file launch_settings = LaunchSettings(wlmutils.get_test_launcher()) app = Application("name", "python", "RunSettings") # Path of directory to symlink - symlink_dir = get_gen_symlink_file + symlink_dir = get_gen_symlink_dir # Get a list of all files in the directory symlink_files = sorted(glob(symlink_dir + "/*")) # Attach directory to Application app.attach_generator_files(to_symlink=symlink_files) # Create Job job = Job(app, launch_settings) - # Create the experiment - experiment_path = osp.join(test_dir, "experiment_name") - gen = Generator(exp_path=experiment_path, run_id="mock_run") - # Generate Experiment file structure - job_path = gen.generate_job(job, 1) - expected_file = pathlib.Path(job_path) / "mock2.txt" + + # Call Generator.generate_job + run_path = generator_instance.root / "run" + run_path.mkdir(parents=True) + log_path = generator_instance.root / "log" + log_path.mkdir(parents=True) + generator_instance.generate_job(job, run_path, log_path) + expected_file = pathlib.Path(run_path) / "mock2.txt" assert osp.isfile(expected_file) + assert expected_file.is_symlink() + assert os.fspath(expected_file.resolve()) == osp.join(osp.realpath(get_gen_symlink_dir), "mock2.txt") -def test_generate_configure(fileutils, wlmutils, test_dir): +def test_generate_configure(fileutils, wlmutils, generator_instance): # Directory of files to configure conf_path = fileutils.get_test_conf_path( osp.join("generator_files", "easy", "marked/") @@ -217,16 +215,16 @@ def test_generate_configure(fileutils, wlmutils, test_dir): app.attach_generator_files(to_configure=tagged_files) job = Job(app, launch_settings) - # Spin up Experiment - experiment_path = osp.join(test_dir, "experiment_name") - # Spin up Generator - gen = Generator(exp_path=experiment_path, run_id="temp_run") - # Execute file generation - job_path = gen.generate_job(job, 1) + # Call Generator.generate_job + run_path = generator_instance.root / "run" + run_path.mkdir(parents=True) + log_path = generator_instance.root / "log" + log_path.mkdir(parents=True) + generator_instance.generate_job(job, run_path, log_path) # Retrieve the list of configured files in the test directory - configured_files = sorted(glob(str(job_path) + "/*")) + configured_files = sorted(glob(str(run_path) + "/*")) # Use filecmp.cmp to check that the corresponding files are equal - for written, correct in zip(configured_files, correct_files): + for written, correct in itertools.zip_longest(configured_files, correct_files): assert filecmp.cmp(written, correct) @@ -236,11 +234,11 @@ def test_exp_private_generate_method_ensemble(test_dir, wlmutils, generator_inst launch_settings = LaunchSettings(wlmutils.get_test_launcher()) job_list = ensemble.as_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) - for job in job_list: - job_execution_path = exp._generate(generator_instance, job, 1) - head, _ = os.path.split(job_execution_path) + for i, job in enumerate(job_list): + job_run_path = exp._generate(generator_instance, job, i) + head, _ = os.path.split(job_run_path) expected_log_path = pathlib.Path(head) / "log" - assert osp.isdir(job_execution_path) + assert osp.isdir(job_run_path) assert osp.isdir(pathlib.Path(expected_log_path)) @@ -248,9 +246,20 @@ def test_generate_ensemble_directory(wlmutils, generator_instance): ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) job_list = ensemble.as_jobs(launch_settings) - for job in job_list: - job_path = generator_instance.generate_job(job, 1) - assert osp.isdir(job_path) + for i, job in enumerate(job_list): + # Call Generator.generate_job + run_path = generator_instance.root / f"run-{i}" + run_path.mkdir(parents=True) + log_path = generator_instance.root / f"log-{i}" + log_path.mkdir(parents=True) + generator_instance.generate_job(job, run_path, log_path) + # Assert smartsim params file created + assert osp.isfile(osp.join(log_path, "smartsim_params.txt")) + # Assert smartsim params correctly written to + with open(log_path / "smartsim_params.txt", 'r') as file: + content = file.read() + assert "Generation start date and time:" in content + def test_generate_ensemble_directory_start(test_dir, wlmutils, monkeypatch): @@ -273,13 +282,13 @@ def test_generate_ensemble_directory_start(test_dir, wlmutils, monkeypatch): assert osp.isdir(log_path) -def test_generate_ensemble_copy(test_dir, wlmutils, monkeypatch, get_gen_copy_file): +def test_generate_ensemble_copy(test_dir, wlmutils, monkeypatch, get_gen_copy_dir): monkeypatch.setattr( "smartsim.settings.dispatch._LauncherAdapter.start", lambda launch, exe, job_execution_path, env: "exit", ) ensemble = Ensemble( - "ensemble-name", "echo", replicas=2, files=EntityFiles(copy=get_gen_copy_file) + "ensemble-name", "echo", replicas=2, files=EntityFiles(copy=get_gen_copy_dir) ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) job_list = ensemble.as_jobs(launch_settings) @@ -294,7 +303,7 @@ def test_generate_ensemble_copy(test_dir, wlmutils, monkeypatch, get_gen_copy_fi def test_generate_ensemble_symlink( - test_dir, wlmutils, monkeypatch, get_gen_symlink_file + test_dir, wlmutils, monkeypatch, get_gen_symlink_dir ): monkeypatch.setattr( "smartsim.settings.dispatch._LauncherAdapter.start", @@ -304,7 +313,7 @@ def test_generate_ensemble_symlink( "ensemble-name", "echo", replicas=2, - files=EntityFiles(symlink=get_gen_symlink_file), + files=EntityFiles(symlink=get_gen_symlink_dir), ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) job_list = ensemble.as_jobs(launch_settings) @@ -314,12 +323,14 @@ def test_generate_ensemble_symlink( jobs_dir = os.path.join(test_dir, run_dir[0], "jobs") job_dir = listdir(jobs_dir) for ensemble_dir in job_dir: - sym_file_path = os.path.join(jobs_dir, ensemble_dir, "run", "to_symlink_dir") + sym_file_path = pathlib.Path(jobs_dir) / ensemble_dir / "run" / "to_symlink_dir" assert osp.isdir(sym_file_path) + assert sym_file_path.is_symlink() + assert os.fspath(sym_file_path.resolve()) == osp.realpath(get_gen_symlink_dir) def test_generate_ensemble_configure( - test_dir, wlmutils, monkeypatch, get_gen_configure_file + test_dir, wlmutils, monkeypatch, get_gen_configure_dir ): monkeypatch.setattr( "smartsim.settings.dispatch._LauncherAdapter.start", @@ -327,7 +338,7 @@ def test_generate_ensemble_configure( ) params = {"PARAM0": [0, 1], "PARAM1": [2, 3]} # Retrieve a list of files for configuration - tagged_files = sorted(glob(get_gen_configure_file + "/*")) + tagged_files = sorted(glob(get_gen_configure_dir + "/*")) ensemble = Ensemble( "ensemble-name", "echo",