From 5bee8f1f734a0f9547301de93e43357ccb09c956 Mon Sep 17 00:00:00 2001
From: adrianciu <adrian.ciu@codemart.ro>
Date: Thu, 27 Feb 2025 13:26:45 +0200
Subject: [PATCH] dedal: CLI; tests

---
 dedal/bll/SpackManager.py                     |  35 ++++
 dedal/bll/cli_utils.py                        |  23 +++
 dedal/cli/SpackManager.py                     |   0
 dedal/cli/spack_manager_api.py                | 153 +++++++++++++++
 dedal/model/SpackDescriptor.py                |   6 +-
 dedal/spack_factory/SpackOperation.py         |  53 ++---
 .../spack_from_scratch_test.py                |  10 +-
 .../spack_operation_creator_test.py           |  50 +++++
 .../unit_tests/spack_manager_api_test.py      | 183 ++++++++++++++++++
 9 files changed, 480 insertions(+), 33 deletions(-)
 create mode 100644 dedal/bll/SpackManager.py
 create mode 100644 dedal/bll/cli_utils.py
 delete mode 100644 dedal/cli/SpackManager.py
 create mode 100644 dedal/cli/spack_manager_api.py
 create mode 100644 dedal/tests/integration_tests/spack_operation_creator_test.py
 create mode 100644 dedal/tests/unit_tests/spack_manager_api_test.py

diff --git a/dedal/bll/SpackManager.py b/dedal/bll/SpackManager.py
new file mode 100644
index 00000000..e5fae221
--- /dev/null
+++ b/dedal/bll/SpackManager.py
@@ -0,0 +1,35 @@
+import os
+from dedal.model.SpackDescriptor import SpackDescriptor
+from dedal.spack_factory.SpackOperationCreator import SpackOperationCreator
+from dedal.configuration.SpackConfig import SpackConfig
+
+
+class SpackManager:
+    """
+    This class defines the logic used by the CLI
+    """
+
+    def __init__(self, spack_config: SpackConfig = None, use_cache=False):
+        self._spack_config = spack_config
+        self._use_cache = use_cache
+
+    def _get_spack_operation(self):
+        return SpackOperationCreator.get_spack_operator(self._spack_config, self._use_cache)
+
+    def install_spack(self, version: str, bashrc_path=os.path.expanduser("~/.bashrc")):
+        self._get_spack_operation().install_spack(spack_version=f'v{version}', bashrc_path=bashrc_path)
+
+    def add_spack_repo(self, repo: SpackDescriptor):
+        """
+        After additional repo was added, setup_spack_env must be invoked
+        """
+        self._spack_config.add_repo(repo)
+
+    def setup_spack_env(self):
+        self._get_spack_operation().setup_spack_env()
+
+    def concretize_spack_env(self):
+        self._get_spack_operation().concretize_spack_env()
+
+    def install_packages(self, jobs: int):
+        self._get_spack_operation().install_packages(jobs=jobs)
diff --git a/dedal/bll/cli_utils.py b/dedal/bll/cli_utils.py
new file mode 100644
index 00000000..bfc74ed0
--- /dev/null
+++ b/dedal/bll/cli_utils.py
@@ -0,0 +1,23 @@
+import jsonpickle
+import os
+
+
+def save_config(spack_config_data, config_path: str):
+    """Save config to JSON file."""
+    with open(config_path, "w") as data_file:
+        data_file.write(jsonpickle.encode(spack_config_data))
+
+
+def load_config(config_path: str):
+    """Load config from JSON file."""
+    if os.path.exists(config_path):
+        with open(config_path, "r") as data_file:
+            data = jsonpickle.decode(data_file.read())
+            return data
+    return {}
+
+
+def clear_config(config_path: str):
+    """Delete the JSON config file."""
+    if os.path.exists(config_path):
+        os.remove(config_path)
diff --git a/dedal/cli/SpackManager.py b/dedal/cli/SpackManager.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/dedal/cli/spack_manager_api.py b/dedal/cli/spack_manager_api.py
new file mode 100644
index 00000000..78918849
--- /dev/null
+++ b/dedal/cli/spack_manager_api.py
@@ -0,0 +1,153 @@
+import os
+from pathlib import Path
+import click
+import jsonpickle
+
+from dedal.bll.SpackManager import SpackManager
+from dedal.bll.cli_utils import save_config, load_config
+from dedal.configuration.GpgConfig import GpgConfig
+from dedal.configuration.SpackConfig import SpackConfig
+from dedal.model.SpackDescriptor import SpackDescriptor
+from dedal.utils.utils import resolve_path
+
+SESSION_CONFIG_PATH = os.path.expanduser(f'~/tmp/dedal/dedal_session.json')
+os.makedirs(os.path.dirname(SESSION_CONFIG_PATH), exist_ok=True)
+
+
+@click.group()
+@click.pass_context
+def cli(ctx: click.Context):
+    config = load_config(SESSION_CONFIG_PATH)
+    if ctx.invoked_subcommand not in ['set-config', 'install-spack'] and not config:
+        click.echo('No configuration set. Use `set-config` first.')
+        ctx.exit(1)
+    if config:
+        config['env_path'] = resolve_path(config['env_path'])
+        env = SpackDescriptor(config['env_name'], config['env_path'], config['env_git_path'])
+        gpg = GpgConfig(config['gpg_name'], config['gpg_mail']) if config['gpg_name'] and config['gpg_mail'] else None
+        spack_config = SpackConfig(env=env, repos=None, install_dir=config['install_dir'],
+                                   upstream_instance=config['upstream_instance'],
+                                   concretization_dir=config['concretization_dir'],
+                                   buildcache_dir=config['buildcache_dir'],
+                                   system_name=config['system_name'], gpg=gpg,
+                                   use_spack_global=config['use_spack_global'])
+        ctx.obj = SpackManager(spack_config, use_cache=config['use_cache'])
+
+
+@cli.command()
+@click.option('--use_cache', is_flag=True, default=False, help='Enables cashing')
+@click.option('--use_spack_global', is_flag=True, default=False, help='Uses spack installed globally on the os')
+@click.option('--env_name', type=str, default=None, help='Environment name')
+@click.option('--env_path', type=str, default=None, help='Environment path to download locally')
+@click.option('--env_git_path', type=str, default=None, help='Git path to download the environment')
+@click.option('--install_dir', type=str,
+              help='Install directory for installing spack; spack environments and repositories are stored here')
+@click.option('--upstream_instance', type=str, default=None, help='Upstream instance for spack environment')
+@click.option('--system_name', type=str, default=None, help='System name; it is used inside the spack environment')
+@click.option('--concretization_dir', type=str, default=None,
+              help='Directory where the concretization caching (spack.lock) will be downloaded')
+@click.option('--buildcache_dir', type=str, default=None,
+              help='Directory where the binary caching is downloaded for the spack packages')
+@click.option('--gpg_name', type=str, default=None, help='Gpg name')
+@click.option('--gpg_mail', type=str, default=None, help='Gpg mail contact address')
+@click.option('--cache_version_concretize', type=str, default='v1', help='Cache version for concretizaion data')
+@click.option('--cache_version_build', type=str, default='v1', help='Cache version for binary caches data')
+def set_config(use_cache, env_name, env_path, env_git_path, install_dir, upstream_instance, system_name,
+               concretization_dir,
+               buildcache_dir, gpg_name, gpg_mail, use_spack_global, cache_version_concretize, cache_version_build):
+    """Set configuration parameters for tahe session."""
+    spack_config_data = {
+        'use_cache': use_cache,
+        'env_name': env_name,
+        'env_path': env_path,
+        'env_git_path': env_git_path,
+        'install_dir': install_dir,
+        'upstream_instance': upstream_instance,
+        'system_name': system_name,
+        'concretization_dir': Path(concretization_dir) if concretization_dir else None,
+        'buildcache_dir': Path(buildcache_dir) if buildcache_dir else None,
+        'gpg_name': gpg_name,
+        'gpg_mail': gpg_mail,
+        'use_spack_global': use_spack_global,
+        'repos': [],
+        'cache_version_concretize': cache_version_concretize,
+        'cache_version_build': cache_version_build,
+    }
+    save_config(spack_config_data, SESSION_CONFIG_PATH)
+    click.echo('Configuration saved.')
+
+
+@click.command()
+def show_config():
+    """Show the current configuration."""
+    config = load_config(SESSION_CONFIG_PATH)
+    if config:
+        click.echo(jsonpickle.encode(config, indent=2))
+    else:
+        click.echo('No configuration set. Use `set-config` first.')
+
+
+@cli.command()
+@click.option('--spack_version', type=str, default='0.23.0', help='Spack version')
+@click.option('--bashrc_path', type=str, default="~/.bashrc", help='Path to .bashrc')
+@click.pass_context
+def install_spack(ctx: click.Context, spack_version: str, bashrc_path: str):
+    """Install spack in the install_dir folder"""
+    bashrc_path = os.path.expanduser(bashrc_path)
+    if ctx.obj is None:
+        SpackManager().install_spack(spack_version, bashrc_path)
+    else:
+        ctx.obj.install_spack(spack_version, bashrc_path)
+
+
+@cli.command()
+@click.option('--repo_name', type=str, required=True, default=None, help='Repository name')
+@click.option('--path', type=str, required=True, default=None, help='Repository path to download locally')
+@click.option('--git_path', type=str, required=True, default=None, help='Git path to download the repository')
+def add_spack_repo(repo_name: str, path: str, git_path: str = None):
+    """Adds a spack repository to the spack environments. The setup command must be rerun."""
+    path = resolve_path(path)
+    repo = SpackDescriptor(repo_name, path, git_path)
+    config = load_config(SESSION_CONFIG_PATH)
+    config['repos'].append(repo)
+    save_config(config, SESSION_CONFIG_PATH)
+    click.echo('dedal setup_spack_env must be reran after each repo is added for the environment.')
+
+
+@cli.command()
+@click.pass_context
+def setup_spack_env(ctx: click.Context):
+    """Setups a spack environment according to the given configuration."""
+    ctx.obj.setup_spack_env()
+
+
+@cli.command()
+@click.pass_context
+def concretize(ctx: click.Context):
+    """Spack concretization step"""
+    ctx.obj.concretize_spack_env()
+
+
+@cli.command()
+@click.option('--jobs', type=int, default=2, help='Number of parallel jobs for spack installation')
+@click.pass_context
+def install_packages(ctx: click.Context, jobs):
+    """Installs spack packages present in the spack environment defined in configuration"""
+    ctx.obj.install_packages(jobs=jobs)
+
+
+@click.command()
+def clear_config():
+    """Clear stored configuration"""
+    if os.path.exists(SESSION_CONFIG_PATH):
+        os.remove(SESSION_CONFIG_PATH)
+        click.echo('Configuration cleared!')
+    else:
+        click.echo('No configuration to clear.')
+
+
+cli.add_command(show_config)
+cli.add_command(clear_config)
+
+if __name__ == '__main__':
+    cli()
diff --git a/dedal/model/SpackDescriptor.py b/dedal/model/SpackDescriptor.py
index 421c4824..939164a0 100644
--- a/dedal/model/SpackDescriptor.py
+++ b/dedal/model/SpackDescriptor.py
@@ -7,7 +7,7 @@ class SpackDescriptor:
     Provides details about the spack environment
     """
 
-    def __init__(self, env_name: str, path: Path = Path(os.getcwd()).resolve(), git_path: str = None):
-        self.env_name = env_name
-        self.path = path if isinstance(path,Path) else Path(path)
+    def __init__(self, name: str, path: Path = Path(os.getcwd()).resolve(), git_path: str = None):
+        self.name = name
+        self.path = path.resolve() if isinstance(path, Path) else Path(path).resolve()
         self.git_path = git_path
diff --git a/dedal/spack_factory/SpackOperation.py b/dedal/spack_factory/SpackOperation.py
index 58dcad8b..aebabc0b 100644
--- a/dedal/spack_factory/SpackOperation.py
+++ b/dedal/spack_factory/SpackOperation.py
@@ -39,24 +39,27 @@ class SpackOperation:
         self.spack_config.buildcache_dir = spack_config.buildcache_dir
         if self.spack_config.buildcache_dir:
             os.makedirs(self.spack_config.buildcache_dir, exist_ok=True)
+        if self.spack_config.env and spack_config.env.name:
+            self.env_path: Path = spack_config.env.path / spack_config.env.name
+            self.spack_command_on_env = f'{self.spack_setup_script} spack env activate -p {self.env_path}'
+        else:
+            self.spack_command_on_env = self.spack_setup_script
         if self.spack_config.env and spack_config.env.path:
             self.spack_config.env.path = spack_config.env.path
             self.spack_config.env.path.mkdir(parents=True, exist_ok=True)
-            self.env_path: Path = spack_config.env.path / spack_config.env.env_name
-            self.spack_command_on_env = f'{self.spack_setup_script} spack env activate -p {self.env_path}'
 
     def create_fetch_spack_environment(self):
         if self.spack_config.env.git_path:
-            git_clone_repo(self.spack_config.env.env_name, self.spack_config.env.path / self.spack_config.env.env_name,
+            git_clone_repo(self.spack_config.env.name, self.spack_config.env.path / self.spack_config.env.name,
                            self.spack_config.env.git_path,
                            logger=self.logger)
         else:
-            os.makedirs(self.spack_config.env.path / self.spack_config.env.env_name, exist_ok=True)
+            os.makedirs(self.spack_config.env.path / self.spack_config.env.name, exist_ok=True)
             run_command("bash", "-c",
                         f'{self.spack_setup_script} spack env create -d {self.env_path}',
                         check=True, logger=self.logger,
-                        info_msg=f"Created {self.spack_config.env.env_name} spack environment",
-                        exception_msg=f"Failed to create {self.spack_config.env.env_name} spack environment",
+                        info_msg=f"Created {self.spack_config.env.name} spack environment",
+                        exception_msg=f"Failed to create {self.spack_config.env.name} spack environment",
                         exception=BashCommandException)
 
     def setup_spack_env(self):
@@ -79,13 +82,13 @@ class SpackOperation:
         self.create_fetch_spack_environment()
         if self.spack_config.install_dir.exists():
             for repo in self.spack_config.repos:
-                repo_dir = self.spack_config.install_dir / repo.path / repo.env_name
-                git_clone_repo(repo.env_name, repo_dir, repo.git_path, logger=self.logger)
-                if not self.spack_repo_exists(repo.env_name):
-                    self.add_spack_repo(repo.path, repo.env_name)
-                    self.logger.debug(f'Added spack repository {repo.env_name}')
+                repo_dir = self.spack_config.install_dir / repo.path / repo.name
+                git_clone_repo(repo.name, repo_dir, repo.git_path, logger=self.logger)
+                if not self.spack_repo_exists(repo.name):
+                    self.add_spack_repo(repo.path, repo.name)
+                    self.logger.debug(f'Added spack repository {repo.name}')
                 else:
-                    self.logger.debug(f'Spack repository {repo.env_name} already added')
+                    self.logger.debug(f'Spack repository {repo.name} already added')
 
     def spack_repo_exists(self, repo_name: str) -> bool | None:
         """Check if the given Spack repository exists."""
@@ -116,7 +119,7 @@ class SpackOperation:
                              self.spack_command_on_env,
                              check=True,
                              capture_output=True, text=True, logger=self.logger,
-                             info_msg=f'Checking if environment {self.spack_config.env.env_name} exists')
+                             info_msg=f'Checking if environment {self.spack_config.env.name} exists')
         if result is None:
             return False
         return True
@@ -127,8 +130,8 @@ class SpackOperation:
         run_command("bash", "-c",
                     f'{self.spack_command_on_env} && spack repo add {repo_path}/{repo_name}',
                     check=True, logger=self.logger,
-                    info_msg=f"Added {repo_name} to spack environment {self.spack_config.env.env_name}",
-                    exception_msg=f"Failed to add {repo_name} to spack environment {self.spack_config.env.env_name}",
+                    info_msg=f"Added {repo_name} to spack environment {self.spack_config.env.name}",
+                    exception_msg=f"Failed to add {repo_name} to spack environment {self.spack_config.env.name}",
                     exception=SpackRepoException)
 
     @check_spack_env
@@ -137,18 +140,18 @@ class SpackOperation:
                              f'{self.spack_command_on_env} && spack compiler list',
                              check=True, logger=self.logger,
                              capture_output=True, text=True,
-                             info_msg=f"Checking spack environment compiler version for {self.spack_config.env.env_name}",
-                             exception_msg=f"Failed to checking spack environment compiler version for {self.spack_config.env.env_name}",
+                             info_msg=f"Checking spack environment compiler version for {self.spack_config.env.name}",
+                             exception_msg=f"Failed to checking spack environment compiler version for {self.spack_config.env.name}",
                              exception=BashCommandException)
         # todo add error handling and tests
         if result.stdout is None:
-            self.logger.debug('No gcc found for {self.env.env_name}')
+            self.logger.debug(f'No gcc found for {self.spack_config.env.name}')
             return None
 
         # Find the first occurrence of a GCC compiler using regex
         match = re.search(r"gcc@([\d\.]+)", result.stdout)
         gcc_version = match.group(1)
-        self.logger.debug(f'Found gcc for {self.spack_config.env.env_name}: {gcc_version}')
+        self.logger.debug(f'Found gcc for {self.spack_config.env.name}: {gcc_version}')
         return gcc_version
 
     def get_spack_installed_version(self):
@@ -168,8 +171,8 @@ class SpackOperation:
                     f'{self.spack_command_on_env} && spack concretize {force}',
                     check=True,
                     logger=self.logger,
-                    info_msg=f'Concertization step for {self.spack_config.env.env_name}',
-                    exception_msg=f'Failed the concertization step for {self.spack_config.env.env_name}',
+                    info_msg=f'Concertization step for {self.spack_config.env.name}',
+                    exception_msg=f'Failed the concertization step for {self.spack_config.env.name}',
                     exception=SpackConcertizeException)
 
     def create_gpg_keys(self):
@@ -178,8 +181,8 @@ class SpackOperation:
                         f'{self.spack_setup_script} spack gpg init && spack gpg create {self.spack_config.gpg.name} {self.spack_config.gpg.mail}',
                         check=True,
                         logger=self.logger,
-                        info_msg=f'Created pgp keys for {self.spack_config.env.env_name}',
-                        exception_msg=f'Failed to create pgp keys mirror {self.spack_config.env.env_name}',
+                        info_msg=f'Created pgp keys for {self.spack_config.env.name}',
+                        exception_msg=f'Failed to create pgp keys mirror {self.spack_config.env.name}',
                         exception=SpackGpgException)
         else:
             raise SpackGpgException('No GPG configuration was defined is spack configuration')
@@ -225,8 +228,8 @@ class SpackOperation:
                                      stderr=subprocess.PIPE,
                                      text=True,
                                      logger=self.logger,
-                                     info_msg=f"Installing spack packages for {self.spack_config.env.env_name}",
-                                     exception_msg=f"Error installing spack packages for {self.spack_config.env.env_name}",
+                                     info_msg=f"Installing spack packages for {self.spack_config.env.name}",
+                                     exception_msg=f"Error installing spack packages for {self.spack_config.env.name}",
                                      exception=SpackInstallPackagesException)
         log_command(install_result, str(Path(os.getcwd()).resolve() / ".generate_cache.log"))
         return install_result
diff --git a/dedal/tests/integration_tests/spack_from_scratch_test.py b/dedal/tests/integration_tests/spack_from_scratch_test.py
index 794caef1..7e8d900c 100644
--- a/dedal/tests/integration_tests/spack_from_scratch_test.py
+++ b/dedal/tests/integration_tests/spack_from_scratch_test.py
@@ -16,7 +16,7 @@ def test_spack_repo_exists_1(tmp_path):
     spack_operation = SpackOperationCreator.get_spack_operator(config)
     spack_operation.install_spack()
     with pytest.raises(NoSpackEnvironmentException):
-        spack_operation.spack_repo_exists(env.env_name)
+        spack_operation.spack_repo_exists(env.name)
 
 
 def test_spack_repo_exists_2(tmp_path):
@@ -27,7 +27,7 @@ def test_spack_repo_exists_2(tmp_path):
     assert isinstance(spack_operation, SpackOperation)
     spack_operation.install_spack()
     spack_operation.setup_spack_env()
-    assert spack_operation.spack_repo_exists(env.env_name) == False
+    assert spack_operation.spack_repo_exists(env.name) == False
 
 
 def test_spack_from_scratch_setup_1(tmp_path):
@@ -38,7 +38,7 @@ def test_spack_from_scratch_setup_1(tmp_path):
     assert isinstance(spack_operation, SpackOperation)
     spack_operation.install_spack()
     spack_operation.setup_spack_env()
-    assert spack_operation.spack_repo_exists(env.env_name) == False
+    assert spack_operation.spack_repo_exists(env.name) == False
 
 
 def test_spack_from_scratch_setup_2(tmp_path):
@@ -52,7 +52,7 @@ def test_spack_from_scratch_setup_2(tmp_path):
     assert isinstance(spack_operation, SpackOperation)
     spack_operation.install_spack()
     spack_operation.setup_spack_env()
-    assert spack_operation.spack_repo_exists(env.env_name) == True
+    assert spack_operation.spack_repo_exists(env.name) == True
 
 
 def test_spack_from_scratch_setup_3(tmp_path):
@@ -88,7 +88,7 @@ def test_spack_not_a_valid_repo():
     spack_operation = SpackOperationCreator.get_spack_operator(config)
     assert isinstance(spack_operation, SpackOperation)
     with pytest.raises(BashCommandException):
-        spack_operation.add_spack_repo(repo.path, repo.env_name)
+        spack_operation.add_spack_repo(repo.path, repo.name)
 
 
 @pytest.mark.skip(
diff --git a/dedal/tests/integration_tests/spack_operation_creator_test.py b/dedal/tests/integration_tests/spack_operation_creator_test.py
new file mode 100644
index 00000000..226184b0
--- /dev/null
+++ b/dedal/tests/integration_tests/spack_operation_creator_test.py
@@ -0,0 +1,50 @@
+from dedal.spack_factory.SpackOperationCreateCache import SpackOperationCreateCache
+
+from dedal.configuration.SpackConfig import SpackConfig
+from dedal.model.SpackDescriptor import SpackDescriptor
+from dedal.spack_factory.SpackOperation import SpackOperation
+from dedal.spack_factory.SpackOperationCreator import SpackOperationCreator
+from dedal.spack_factory.SpackOperationUseCache import SpackOperationUseCache
+from dedal.tests.testing_variables import ebrains_spack_builds_git, test_spack_env_git
+
+
+def test_spack_creator_scratch_1(tmp_path):
+    install_dir = tmp_path
+    env = SpackDescriptor('test-spack-env', install_dir, test_spack_env_git)
+    repo = SpackDescriptor('ebrains-spack-builds', install_dir, ebrains_spack_builds_git)
+    spack_config = SpackConfig(env, install_dir=install_dir)
+    spack_config.add_repo(repo)
+    spack_operation = SpackOperationCreator.get_spack_operator(spack_config)
+    assert isinstance(spack_operation, SpackOperation)
+
+
+def test_spack_creator_scratch_2(tmp_path):
+    spack_config = None
+    spack_operation = SpackOperationCreator.get_spack_operator(spack_config)
+    assert isinstance(spack_operation, SpackOperation)
+
+
+def test_spack_creator_scratch_3():
+    spack_config = SpackConfig()
+    spack_operation = SpackOperationCreator.get_spack_operator(spack_config)
+    assert isinstance(spack_operation, SpackOperation)
+
+
+def test_spack_creator_create_cache(tmp_path):
+    install_dir = tmp_path
+    env = SpackDescriptor('test-spack-env', install_dir, test_spack_env_git)
+    repo = SpackDescriptor('ebrains-spack-builds', install_dir, ebrains_spack_builds_git)
+    spack_config = SpackConfig(env, install_dir=install_dir, concretization_dir=install_dir, buildcache_dir=install_dir)
+    spack_config.add_repo(repo)
+    spack_operation = SpackOperationCreator.get_spack_operator(spack_config)
+    assert isinstance(spack_operation, SpackOperationCreateCache)
+
+
+def test_spack_creator_use_cache(tmp_path):
+    install_dir = tmp_path
+    env = SpackDescriptor('test-spack-env', install_dir, test_spack_env_git)
+    repo = SpackDescriptor('ebrains-spack-builds', install_dir, ebrains_spack_builds_git)
+    spack_config = SpackConfig(env, install_dir=install_dir, concretization_dir=install_dir, buildcache_dir=install_dir)
+    spack_config.add_repo(repo)
+    spack_operation = SpackOperationCreator.get_spack_operator(spack_config, use_cache=True)
+    assert isinstance(spack_operation, SpackOperationUseCache)
diff --git a/dedal/tests/unit_tests/spack_manager_api_test.py b/dedal/tests/unit_tests/spack_manager_api_test.py
new file mode 100644
index 00000000..5d32a56d
--- /dev/null
+++ b/dedal/tests/unit_tests/spack_manager_api_test.py
@@ -0,0 +1,183 @@
+import os
+
+import pytest
+from unittest.mock import patch, MagicMock
+from click.testing import CliRunner
+from dedal.cli.spack_manager_api import show_config, clear_config, install_spack, add_spack_repo, install_packages, \
+    setup_spack_env, concretize, set_config
+from dedal.model.SpackDescriptor import SpackDescriptor
+
+
+@pytest.fixture
+def runner():
+    return CliRunner()
+
+
+@pytest.fixture
+def mocked_session_path():
+    return '/mocked/tmp/session.json'
+
+
+@pytest.fixture
+def mock_spack_manager():
+    mock_spack_manager = MagicMock()
+    mock_spack_manager.install_spack = MagicMock()
+    mock_spack_manager.add_spack_repo = MagicMock()
+    mock_spack_manager.setup_spack_env = MagicMock()
+    mock_spack_manager.concretize_spack_env = MagicMock()
+    mock_spack_manager.install_packages = MagicMock()
+    return mock_spack_manager
+
+
+@pytest.fixture
+def mock_load_config():
+    with patch('dedal.cli.spack_manager_api.load_config') as mock_load:
+        yield mock_load
+
+
+@pytest.fixture
+def mock_save_config():
+    with patch('dedal.cli.spack_manager_api.save_config') as mock_save:
+        yield mock_save
+
+
+@pytest.fixture
+def mock_clear_config():
+    with patch('dedal.cli.spack_manager_api.clear_config') as mock_clear:
+        yield mock_clear
+
+
+def test_show_config_no_config(runner, mock_load_config):
+    mock_load_config.return_value = None
+    result = runner.invoke(show_config)
+    assert 'No configuration set. Use `set-config` first.' in result.output
+
+
+def test_show_config_with_config(runner, mock_load_config):
+    """Test the show_config command when config is present."""
+    mock_load_config.return_value = {"key": "value"}
+    result = runner.invoke(show_config)
+    assert result.exit_code == 0
+    assert '"key": "value"' in result.output
+
+
+def test_clear_config(runner, mock_clear_config):
+    """Test the clear_config command."""
+    with patch('os.path.exists', return_value=True), patch('os.remove') as mock_remove:
+        result = runner.invoke(clear_config)
+        assert 'Configuration cleared!' in result.output
+        mock_remove.assert_called_once()
+
+
+def test_install_spack_no_context_1(runner, mock_spack_manager):
+    """Test install_spack with no context, using SpackManager."""
+    with patch('dedal.cli.spack_manager_api.SpackManager', return_value=mock_spack_manager):
+        result = runner.invoke(install_spack, ['--spack_version', '0.24.0'])
+    mock_spack_manager.install_spack.assert_called_once_with('0.24.0', os.path.expanduser("~/.bashrc"))
+    assert result.exit_code == 0
+
+
+def test_install_spack_no_context_2(runner, mock_spack_manager):
+    """Test install_spack with no context, using SpackManager and the default value for spack_version."""
+    with patch('dedal.cli.spack_manager_api.SpackManager', return_value=mock_spack_manager):
+        result = runner.invoke(install_spack)
+    mock_spack_manager.install_spack.assert_called_once_with('0.23.0', os.path.expanduser("~/.bashrc"))
+    assert result.exit_code == 0
+
+
+def test_install_spack_with_mocked_context_1(runner, mock_spack_manager):
+    """Test install_spack with a mocked context, using ctx.obj as SpackManager."""
+    result = runner.invoke(install_spack, ['--spack_version', '0.24.0', '--bashrc_path', '/home/.bahsrc'], obj=mock_spack_manager)
+    mock_spack_manager.install_spack.assert_called_once_with('0.24.0', '/home/.bahsrc')
+    assert result.exit_code == 0
+
+
+def test_install_spack_with_mocked_context_2(runner, mock_spack_manager):
+    """Test install_spack with a mocked context, using ctx.obj as SpackManager and the default value for spack_version."""
+    result = runner.invoke(install_spack, obj=mock_spack_manager)
+    mock_spack_manager.install_spack.assert_called_once_with('0.23.0', os.path.expanduser("~/.bashrc"))
+    assert result.exit_code == 0
+
+
+def test_setup_spack_env(runner, mock_spack_manager):
+    """Test setup_spack_env with a mocked context, using ctx.obj as SpackManager."""
+    result = runner.invoke(setup_spack_env, obj=mock_spack_manager)
+    mock_spack_manager.setup_spack_env.assert_called_once_with()
+    assert result.exit_code == 0
+
+
+def test_concretize(runner, mock_spack_manager):
+    """Test install_spack with a mocked context, using ctx.obj as SpackManager."""
+    result = runner.invoke(concretize, obj=mock_spack_manager)
+    mock_spack_manager.concretize_spack_env.assert_called_once_with()
+    assert result.exit_code == 0
+
+
+def test_install_packages_1(runner, mock_spack_manager):
+    """Test install_packages with a mocked context, using ctx.obj as SpackManager."""
+    result = runner.invoke(install_packages, obj=mock_spack_manager)
+    mock_spack_manager.install_packages.assert_called_once_with(jobs=2)
+    assert result.exit_code == 0
+
+
+def test_install_packages(runner, mock_spack_manager):
+    """Test install_packages with a mocked context, using ctx.obj as SpackManager."""
+    result = runner.invoke(install_packages, ['--jobs', 3], obj=mock_spack_manager)
+    mock_spack_manager.install_packages.assert_called_once_with(jobs=3)
+    assert result.exit_code == 0
+
+
+@patch('dedal.cli.spack_manager_api.resolve_path')
+@patch('dedal.cli.spack_manager_api.SpackDescriptor')
+def test_add_spack_repo(mock_spack_descriptor, mock_resolve_path, mock_load_config, mock_save_config,
+                        mocked_session_path, runner):
+    """Test adding a spack repository with mocks."""
+    expected_config = {'repos': [SpackDescriptor(name='test-repo')]}
+    repo_name = 'test-repo'
+    path = '/path'
+    git_path = 'https://example.com/repo.git'
+    mock_resolve_path.return_value = '/resolved/path'
+    mock_load_config.return_value = expected_config
+    mock_repo_instance = MagicMock()
+    mock_spack_descriptor.return_value = mock_repo_instance
+
+    with patch('dedal.cli.spack_manager_api.SESSION_CONFIG_PATH', mocked_session_path):
+        result = runner.invoke(add_spack_repo, ['--repo_name', repo_name, '--path', path, '--git_path', git_path])
+
+    assert result.exit_code == 0
+    assert 'dedal setup_spack_env must be reran after each repo is added' in result.output
+    mock_resolve_path.assert_called_once_with(path)
+    mock_spack_descriptor.assert_called_once_with(repo_name, '/resolved/path', git_path)
+    assert mock_repo_instance in expected_config['repos']
+    mock_save_config.assert_called_once_with(expected_config, mocked_session_path)
+
+
+def test_set_config(runner, mock_save_config, mocked_session_path):
+    """Test set_config."""
+    with patch('dedal.cli.spack_manager_api.SESSION_CONFIG_PATH', mocked_session_path):
+        result = runner.invoke(set_config, ['--env_name', 'test', '--system_name', 'sys'])
+
+    expected_config = {
+        'use_cache': False,
+        'env_name': 'test',
+        'env_path': None,
+        'env_git_path': None,
+        'install_dir': None,
+        'upstream_instance': None,
+        'system_name': 'sys',
+        'concretization_dir': None,
+        'buildcache_dir': None,
+        'gpg_name': None,
+        'gpg_mail': None,
+        'use_spack_global': False,
+        'repos': [],
+        'cache_version_concretize': 'v1',
+        'cache_version_build': 'v1',
+    }
+
+    mock_save_config.assert_called_once()
+    saved_config, saved_path = mock_save_config.call_args[0]
+    assert saved_path == mocked_session_path
+    assert saved_config == expected_config
+    assert result.exit_code == 0
+    assert 'Configuration saved.' in result.output
-- 
GitLab