diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..bad44636b34555264f9823e84b29108d3b192b2d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.vscode
+**/*.pyc
+logs
+dev-23a/
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1ad39094f161660f238a7ecd312166adb88001b2
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,101 @@
+image: python:latest
+
+stages:
+  - test
+
+# prepare set up of latest python image to run style checks
+.prepare_style: &prepare_style
+  - cat /etc/issue
+  - python3 -V
+  - which python3
+  - python3 -m venv ../venv
+  - source ../venv/bin/activate
+  - which python3
+  - pwd
+  - ls -l
+  - pip install -U pip
+  - pip --version
+  - pip install pytest black ruff pydocstyle
+  - pytest --version
+  - cat /etc/issue
+
+
+# prepare set up of Debian system to run py.test
+.prepare_debian: &prepare_debian
+  - echo "Execute this command before any 'script:' commands."
+  - cat /etc/issue
+  - pwd
+  - ls -l
+
+  - echo "Install Python3"
+  - apt-get update
+  - apt-get install -y python3 python3-venv
+  - python3 -m venv --help
+  - python3 -m venv venv
+  - source venv/bin/activate
+  - which python3
+  - python3 --version
+
+  - echo "Install Python dependencies for running the tests"
+  - pip install -U pip
+  - pip --version
+  - pip install pytest black ruff archspec
+
+  - echo "Diagnostics - which versions are we using"
+  - python3 --version
+  - pytest --version
+  - black --version
+  - ruff --version
+
+  - echo "Install additional packages we need to run spack-setup.sh"
+  - apt-get install -y git rsync
+  - echo "Install additional packages we need to run spack"
+  # Taken from https://github.com/fangohr/oommf-in-spack/blob/main/Dockerfile
+  - apt-get install -y --no-install-recommends
+            autoconf
+            build-essential
+            ca-certificates
+            coreutils
+            curl
+            environment-modules
+            file
+            gfortran
+            git
+            openssh-server
+            unzip
+
+  - export MPSD_MICROARCH=$(archspec cpu)
+  - echo "Setting MPSD_MICROARCH variable to $MPSD_MICROARCH"
+  - echo "Which version of Debian are we running?"
+  - cat /etc/issue
+
+
+style:
+  stage: test
+  image: python:latest
+  script:
+    - *prepare_style
+    - black --version
+    - ruff --version
+    - pydocstyle --version
+    - ruff .
+    - black --check --diff .
+    - pydocstyle mpsd-software-environment.py
+    - pydocstyle tests.py
+    # we could also use `ruff --select D` for pycodestyle. But the behaviour is not exactly the same.
+
+
+test-bullseye:
+  stage: test
+  image: debian:bullseye-slim
+  script:
+    - *prepare_debian
+    - pytest -v -l tests.py
+
+test-bookworm:
+  stage: test
+  image: debian:bookworm-slim
+  script:
+    - *prepare_debian
+    - pytest -v -l tests.py
+
diff --git a/install-dev23a.sh b/install-dev23a.sh
index 368bbe81ce54d878f252aad8f3d5b4eff654dbae..c01e10b7078becf1735c680f3e5ac8b219910416 100755
--- a/install-dev23a.sh
+++ b/install-dev23a.sh
@@ -1,11 +1,17 @@
 #!/bin/bash
+# Script to build all toolchains for this MPSD release ( 23a )
+# Run this script inside the cloned folder for eg:
+# mpsddeb@mpsd-hpc-ibm-022:/opt_mpsd/linux-debian11/mpsd-software-environments$ ./install-dev23a.sh 
+
 set -e
+cd ..
 mkdir -p dev-23a
 cd dev-23a
 # clone repo if it doesn't exist yet
 [ -d 'spack-environments' ] || git clone git@gitlab.gwdg.de:mpsd-cs/spack-environments.git 
 pushd spack-environments
 git checkout dev-23a
+git pull
 popd
 mkdir -p sandybridge
 cd sandybridge
@@ -13,3 +19,8 @@ cd sandybridge
 ../spack-environments/spack_setup.sh foss2021a-serial
 ../spack-environments/spack_setup.sh foss2021a-mpi
 ../spack-environments/spack_setup.sh foss2021a-cuda-mpi
+
+
+../spack-environments/spack_setup.sh foss2022a-serial
+../spack-environments/spack_setup.sh foss2022a-mpi
+../spack-environments/spack_setup.sh foss2022a-cuda-mpi
diff --git a/mpsd-software-environment.py b/mpsd-software-environment.py
new file mode 100755
index 0000000000000000000000000000000000000000..59a831b65c3f46b0eb1935bb1e690dc46da633a4
--- /dev/null
+++ b/mpsd-software-environment.py
@@ -0,0 +1,576 @@
+#!/usr/bin/env python3
+
+"""mpsd-software-environment: tool for installation of toolchains."""
+
+import argparse
+import datetime
+import os
+import subprocess
+import sys
+import time
+from pathlib import Path
+from typing import List, Tuple
+
+about_tool = """
+Build toolchains using Spack.\n
+
+This function builds toolchains for MPSD-HPC at the appropriate directory, \n
+for given system architecture and MPSD software stack version.\n
+The toolchains
+are built using the bash script spack_setup.sh, and the results are logged. """
+
+config_vars = {
+    "cmd_log_file": "install.log",
+    "build_log_file": (
+        "logs/mpsd_spack_ver_toolchains_"
+        f"{datetime.datetime.now().replace(microsecond=0).isoformat()}.log"
+    ),
+    # TODO: modify toolchains,mpsd_spack_ver when the variable is available
+    "spack_environments_repo": "https://gitlab.gwdg.de/mpsd-cs/spack-environments.git",
+}
+
+
+# Helper class to change directory via context manager
+class os_chdir:
+    """The os_chdir class is a context manager.
+
+    It changes the current directory to a specified directory
+    and returns to the original directory after execution.
+    """
+
+    def __init__(self, new_dir):
+        """Initialize, save original directory."""
+        self.new_dir = new_dir
+        self.saved_dir = os.getcwd()
+
+    def __enter__(self):
+        """Go to target directory (main action for context)."""
+        os.chdir(self.new_dir)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """On exist we return to original directory."""
+        os.chdir(self.saved_dir)
+
+
+def run(*args, counter=[0], **kwargs):
+    """
+    Run a subprocess and log the call.
+
+    Convenience function to call `subprocess.run` and provide some metadata
+    about the call.
+
+    Parameters
+    ----------
+    args : tuple
+        passed on to subprocess.run(*args). For example
+        ("ls -l") or (["ls", "-l"])
+    counter : TYPE, optional
+        list with one integer, starting from [0].
+        This is (a Python hack) to count the number of
+        calls of this function, so the different calls of subprocess.run
+        are easier to follow in the log files.
+    kwargs : dict
+        keyword-value arguments to be passed to subprocess.run. For example,
+        `shell=True`.
+
+    Returns
+    -------
+    process : subprocess.CompletedProcess
+        CompletedProcess object as returned by `subprocess.run` .
+
+    Examples
+    --------
+    >>> run(['date', '+%Y-%m-%d'])
+    ##-03 Starting subprocess.run(['date', '+%Y-%m-%d']) with options
+    ##-03   getcwd=/Users/fangohr/git/mpsd-software-environments
+    ##-03   COMMAND=date +%Y-%m-%d
+    2023-05-30
+    ##-03   Completed in 0.0054s.
+    ##-03
+    CompletedProcess(args=['date', '+%Y-%m-%d'], returncode=0)
+
+    >>> run(['date +%Y-%m-%d'], shell=True)
+    ##-04 Starting subprocess.run(['date +%Y-%m-%d']) with options shell=True
+    ##-04   getcwd=/Users/fangohr/git/mpsd-software-environments
+    ##-04   COMMAND=date +%Y-%m-%d
+    2023-05-30
+    ##-04   Completed in 0.0069s.
+    ##-04
+    CompletedProcess(args=['date +%Y-%m-%d'], returncode=0)
+    """
+    # token is printed in front of every meta-data line - useful for
+    # searching the logs. Starts with "##-00", then "##-01", ...
+    token = f"##-{counter[0]:02d}"
+
+    counter[0] += 1  # increase counter
+
+    # make command nicely readable: ["ls", "-l"] -> "ls -l"
+    assert isinstance(args, tuple)
+    assert len(args) == 1
+    arg = args[0]
+    # either args is a tuple containing a string | Example: ('ls -1',)
+    if isinstance(arg, str):
+        command = arg
+        # or we have a tuple containing a list of strings.
+        # Example: (['ls', '-1'],)
+    elif isinstance(arg, list):
+        command = " ".join(arg)
+    else:
+        # we do not expect this to happen
+        raise NotImplementedError(f"{arg=}, {args=}")
+
+    # make options (such as `shell=True`) nicely readable
+    options = ", ".join([f"{key}={value}" for key, value in kwargs.items()])
+
+    # provide information about upcoming subprocess.run call
+    print(f"{token} Starting subprocess.run({arg}) with options {options}")
+    print(f"{token}   getcwd={os.getcwd()}")
+    print(f"{token}   COMMAND={command}")
+
+    time_start = time.time()
+    process = subprocess.run(*args, **kwargs)
+    execution_time = time.time() - time_start
+
+    print(f"{token}   Completed in {execution_time:.4f}s.")
+    print(f"{token}")  # near-empty line to make reading logs easier
+    return process
+
+
+def setup_log_cmd(
+    mpsd_release: str, script_dir: str, msg: str = None, **kwargs
+) -> None:
+    """
+    Log the command used to build the toolchains.
+
+    It also logs information about the software environment installer branch,
+    the Spack environments branch, and the commit hashes of each.
+    It also logs steps taken
+    in the install process using the optional message argument.
+
+    Parameters
+    ----------
+    - mpsd_release : str
+        The name of the release to install toolchains for.
+    - script_dir : str
+        The path to the directory where the scripts are located.
+    - msg : str, optional
+        An optional message to log in the command log file.
+    - **kwargs : dict
+        A dictionary with values for
+        - spe_branch : str
+            The name of the Spack environments branch.
+        - spe_commit_hash : str
+            The commit hash of the Spack environments branch.
+
+    Returns
+    -------
+    - None
+    """
+    release_base_dir = script_dir / mpsd_release
+
+    # Write to the log file with the following format
+    # --------------------------------------------------
+    # 2023-02-29T23:32:01, install-software-environment.py --release 23b --install ALL
+    # Software environment installer branch: script_branch (commit hash: \
+    #    script_commit_hash)
+    # Spack environments branch: dev-23a (commit hash: spe_commit_hash)
+    # MSGs
+    with os_chdir(release_base_dir):
+        with open(config_vars["cmd_log_file"], "a") as f:
+            if msg:
+                # Write the message to the log file
+                f.write(msg + "\n")
+            else:
+                # Write the header
+                f.write("-" * 50 + "\n")
+
+                # Gather data to log
+                # call statement:
+                cmd_line = " ".join(sys.argv)
+                # script branch and commit hash
+                with os_chdir(script_dir):
+                    script_branch = (
+                        run(
+                            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
+                            stdout=subprocess.PIPE,
+                            check=True,
+                        )
+                        .stdout.decode()
+                        .strip()
+                    )
+                    script_commit_hash = (
+                        run(
+                            ["git", "rev-parse", "--short", "HEAD"],
+                            stdout=subprocess.PIPE,
+                            check=True,
+                        )
+                        .stdout.decode()
+                        .strip()
+                    )
+                # spack-environments branch and commit hash from kwargs
+                spe_branch = kwargs.get("spe_branch", None)
+                spe_commit_hash = kwargs.get("spe_commit_hash", None)
+
+                # Write to log file
+                f.write(f"{datetime.datetime.now().isoformat()}, {cmd_line}\n")
+                f.write(
+                    f"Software environment installer branch: {script_branch} "
+                    f"(commit hash: {script_commit_hash})\n"
+                )
+                f.write(
+                    f"Spack environments branch: {spe_branch} "
+                    f"(commit hash: {spe_commit_hash})\n"
+                )
+
+
+def create_dir_structure(mpsd_release: str, script_dir: Path) -> None:
+    """
+    Create the directory structure and clone spack environments repo.
+
+    The create_dir_structure function creates the directory structure for
+    the specified release and clones the Spack environments repository if it
+    doesn't exist.
+
+    Parameters
+    ----------
+    - mpsd_release: A string representing the MPSD release version.
+    - script_dir: A Path object representing the path to the scripts directory.
+
+    Returns
+    -------
+    - None
+    """
+    # Create the directory structure for the release
+    release_base_dir = script_dir / mpsd_release
+    release_base_dir.mkdir(parents=True, exist_ok=True)
+
+    with os_chdir(release_base_dir):
+        # Clone the spack-environments repo if it doesn't exist
+        if not os.path.exists("spack-environments"):
+            run(
+                [
+                    "git",
+                    "clone",
+                    config_vars["spack_environments_repo"],
+                ],
+                check=True,
+            )
+        with os_chdir("spack-environments"):
+            # Git fetch and checkout the release branch and git pull
+            # to be sure that the resulting repo is up to date
+            run(["git", "fetch", "--all"], check=True)
+            checkout_result = run(["git", "checkout", mpsd_release], check=True)
+
+            if checkout_result.returncode != 0:
+                raise Exception(
+                    "Release branch does not exist in spack-environment repo \n."
+                    "Check for typos."
+                )
+            run(["git", "pull"], check=True)
+
+
+def get_release_info(mpsd_release: str, script_dir: Path) -> Tuple[str, str, List[str]]:
+    """
+    Get information about the specified release.
+
+    Get information about the specified release, such as the branch and commit hash
+    of the Spack environments repository and the available toolchains.
+
+    Parameters
+    ----------
+    mpsd_release : str
+        The name of the release to get information for.
+    script_dir : pathlib.Path
+        The base directory where releases are stored.
+
+    Returns
+    -------
+    spe_branch : str
+        The name of the branch for the Spack environments repository.
+    spe_commit_hash : str
+        The commit hash for the Spack environments repository.
+    available_toolchains : list
+        A list of strings representing the available toolchains for the release.
+
+    Raises
+    ------
+    FileNotFoundError
+        If the release directory does not exist. Run `create_dir_structure()` first.
+    """
+    # Get the info for release
+    release_base_dir = script_dir / mpsd_release
+    if not os.path.exists(release_base_dir):
+        raise FileNotFoundError(
+            "Release directory does not exist. Run create_dir_structure() first."
+        )
+    with os_chdir(release_base_dir):
+        with os_chdir("spack-environments"):
+            # Get the branch and commit hash of the spack-environments repo
+            spe_commit_hash = (
+                run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, check=True)
+                .stdout.decode()
+                .strip()
+            )
+            spe_branch = (
+                run(
+                    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
+                    stdout=subprocess.PIPE,
+                    check=True,
+                )
+                .stdout.decode()
+                .strip()
+            )
+            available_toolchains = os.listdir("toolchains")
+    return spe_branch, spe_commit_hash, available_toolchains
+
+
+def prepare_environment(mpsd_release: str, script_dir: Path) -> List[str]:
+    """
+    Create the directory structure for the given MPSD release.
+
+    It does the following steps:
+    Clones the spack-environments repository.
+    Determines the branch and commit hash of the spack-environments repository
+    and the available toolchains.
+    Logs the command usage.
+
+    Parameters
+    ----------
+    mpsd_release : str
+        The name of the MPSD release to prepare the environment for.
+    script_dir : pathlib.Path
+        The base directory to create the release folder and
+        clone the spack-environments repository into.
+
+    Returns
+    -------
+    available_toolchains : list
+        A list of available toolchains for the given MPSD release.
+    """
+    create_dir_structure(mpsd_release, script_dir)
+    spe_branch, spe_commit_hash, available_toolchains = get_release_info(
+        mpsd_release, script_dir
+    )
+    setup_log_cmd(
+        mpsd_release, script_dir, spe_branch=spe_branch, spe_commit_hash=spe_commit_hash
+    )
+    return available_toolchains
+
+
+def install_environment(
+    mpsd_release: str,
+    toolchains: List[str],
+    script_dir: Path,
+    force_reinstall: bool = False,
+    enable_build_cache: bool = False,
+) -> None:
+    """
+    Install the specified MPSD release and toolchains.
+
+    The function installs the toolchain to the specified directory, using Spack.
+
+    Parameters
+    ----------
+    mpsd_release : str
+        A string representing the MPSD release version.
+    toolchains : list of str
+        A list of strings representing the toolchains to install
+        (e.g., "foss2021a-mpi", "global_generic", "ALL").
+    script_dir : pathlib.Path
+        A Path object representing the path to the directory where
+        the release and toolchains will be installed.
+    force_reinstall : bool, optional
+        A boolean indicating whether to force a reinstallation
+        even if the release and toolchains already exist. Defaults to False.
+    enable_build_cache : bool, optional
+        A boolean indicating whether to build the build cache
+        when installing toolchains. Defaults to False.
+
+    Raises
+    ------
+    ValueError
+        If a requested toolchain is not available in the specified release.
+
+    Returns
+    -------
+    None
+    """
+    print(
+        f"Installing release {mpsd_release} with toolchains {toolchains} "
+        f"to {script_dir}"
+    )
+
+    # Set required variables
+    release_base_dir = script_dir / mpsd_release
+    os.environ.get("MPSD_OS", "UNKNOWN_OS")
+    mpsd_microarch = os.environ.get("MPSD_MICROARCH", "UNKNOWN_MICROARCH")
+    toolchain_dir = release_base_dir / mpsd_microarch
+    toolchain_dir.mkdir(parents=True, exist_ok=True)
+    spack_setup_script = release_base_dir / "spack-environments" / "spack_setup.sh"
+    install_flags = []
+    if not enable_build_cache:
+        install_flags.append("-b")
+
+    # run the prepare_environment function
+    available_toolchains = prepare_environment(mpsd_release, script_dir)
+    # Ensure that the requested toolchains are available in the release
+    if toolchains == "ALL":
+        toolchains = available_toolchains
+    elif toolchains == "NONE":
+        # No toolchains requested, so we only create the env and print the
+        # list of available toolchains
+        print(
+            "No toolchains requested. Available toolchains for release "
+            f"{mpsd_release} are: \n {available_toolchains}"
+        )
+        return
+
+    for toolchain in toolchains:
+        if toolchain not in available_toolchains:
+            raise ValueError(
+                f"Toolchain '{toolchain}' is not available in release {mpsd_release}."
+            )
+
+    # Install the toolchains
+    with os_chdir(toolchain_dir):
+        # run spack_setup_script with the toolchains as arguments
+        # if the log folder doesn't exist, create it
+        if not os.path.exists("logs"):
+            os.mkdir("logs")
+        for toolchain in toolchains:
+            # Set the install log file name to config_vars["install_log_file"]
+            # and replace _toolchains_ with the toolchain name and
+            # _mpsd_spack_ver_ with mpsd_release
+
+            print(f"Installing toolchain {toolchain} to {toolchain_dir}")
+            install_log_file = (
+                config_vars["build_log_file"]
+                .replace("mpsd_spack_ver_", f"{mpsd_release}_")
+                .replace("_toolchains_", f"_{toolchain}_")
+            )
+            # log the command
+            setup_log_cmd(
+                mpsd_release,
+                script_dir,
+                msg=f"installing {toolchain} and logging at {install_log_file}",
+            )
+            setup_log_cmd(
+                mpsd_release,
+                script_dir,
+                msg=(
+                    f"CMD: bash {spack_setup_script} {' '.join(install_flags)}"
+                    "{toolchain}"
+                ),
+            )
+            run(
+                f"bash {spack_setup_script} {' '.join(install_flags)} {toolchain} 2>&1 "
+                f"| tee -a {install_log_file} ",
+                shell=True,
+                check=True,
+            )
+
+
+def remove_environment(release, toolchains, target_dir):
+    """Remove release from installation."""
+    msg = f"Removing release {release} with toolchains {toolchains} from {target_dir}"
+    print(msg)
+    raise NotImplementedError(msg)
+
+
+def start_new_environment(release, from_release, target_dir):
+    """Start new MPSD software environment version."""
+    msg = f"Starting new release {release} from {from_release} to {target_dir}"
+    print(msg)
+    raise NotImplementedError(msg)
+
+
+def main():
+    """Execute main entry point."""
+    parser = argparse.ArgumentParser(description=about_tool)
+    subparsers = parser.add_subparsers(
+        dest="action", title="actions", description="valid actions", required=True
+    )
+    subparsers.required = True
+    list_of_cmds = [
+        ("prepare", "Prepare the environment for installation on the disk"),
+        ("install", "Install a software environment"),
+        ("reinstall", "Reinstall a software environment"),
+        ("remove", "Remove a software environment or toolchains from an environment"),
+        ("start-new", "Start a new software environment version"),
+    ]
+    for cmd, help_text in list_of_cmds:
+        subp = subparsers.add_parser(cmd, help=help_text)
+
+        if cmd == "start-new":
+            subp.add_argument(
+                "--from-release",
+                dest="from_release",
+                type=str,
+                required=True,
+                help="Release version to start from",
+            )
+            subp.add_argument(
+                "--to-release",
+                dest="to_release",
+                type=str,
+                required=True,
+                help="Release version to create",
+            )
+
+        else:
+            subp.add_argument(
+                "release",
+                type=str,
+                help="Release version to prepare, install, reinstall or remove",
+            )
+            if cmd in ["install", "reinstall", "remove"]:
+                # "install" command needs additional documentation
+                tool_chain_help = (
+                    f"Pass a list of toolchains to command {cmd}. "
+                    "Use '--toolchains ALL' to "
+                    f"{cmd} all toolchains. If '--toolchain' is not "
+                    "specified, list available toolchains for the release "
+                    "(after environment has been prepared if not done yet)."
+                )
+
+                subp.add_argument(
+                    "--toolchains",  # first option defines attribute
+                    # name `args.toolchains` in `args = parser_args()`
+                    "--toolchain",  # allow singular as alternative
+                    # (-> creates attribute `args.toolchains` if used)
+                    type=str,
+                    dest="toolchains",
+                    nargs="+",
+                    default="NONE",
+                    help=tool_chain_help,
+                )
+                subp.add_argument(
+                    "--enable-build-cache",
+                    action="store_true",
+                    help=(
+                        "Enable Spack build cache. Useful for reinstallation but "
+                        "consumes time and disk space."
+                    ),
+                )
+    # Carry out the action
+    args = parser.parse_args()
+
+    # target dir is the place where this script exists. the
+    # release `dev` in script_dir/dev-23a
+    script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+
+    # Check the command and run related function
+    if args.action == "remove":
+        remove_environment(args.release, args.toolchains, script_dir)
+    elif args.action == "start-new":
+        start_new_environment(args.from_release, args.to_release, script_dir)
+    elif args.action == "install":
+        install_environment(
+            args.release, args.toolchains, script_dir, False, args.enable_build_cache
+        )
+    elif args.action == "prepare":
+        prepare_environment(args.release, script_dir)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..43d08a7be8cc80320ff7006f4718eda7eb4d8268
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,319 @@
+"""Tests for mpsd-software-environment.py."""
+
+import importlib
+import os
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+mod = importlib.import_module("mpsd-software-environment")
+
+
+def create_mock_git_repository(target_directory, create_directory=True):
+    """
+    Create a git repository in the directory `target_directory`.
+
+    Arguments
+    ---------
+    target_directory : pathlib.Path
+      - path at which the root of the repository should be located (i.e. `.git` folder)
+
+    create_directory : bool
+      - create `target_directory` and parent directories if True
+
+    """
+    # create directory first
+    if create_directory:
+        target_directory.mkdir(parents=True)
+
+    # then create git repository:
+    with mod.os_chdir(str(target_directory)):
+        subprocess.run("git init .", shell=True, check=True)
+        subprocess.run("echo 'fake content' > readme.txt", shell=True, check=True)
+        subprocess.run("git add readme.txt", shell=True, check=True)
+        subprocess.run("pwd", shell=True)
+
+        # if email and username are not available (such as on naked test container),
+        # git may complain. We set a temporary user for this one commit to work around
+        # that.
+        user_details = "-c user.name='Tes Ta' -c user.email='tester@some-ci.org'"
+        subprocess.run(
+            f'git {user_details} commit -m "first commit" readme.txt',
+            shell=True,
+            check=True,
+        )
+
+
+def test_os_chdir(tmp_path):
+    """Test the os_chdir context manager."""
+    # create a temporary directory for testing
+    temp_dir = tmp_path / "test_os_chdir"
+    temp_dir.mkdir()
+
+    # initial current working directory
+    initial_cwd = os.getcwd()
+
+    # change to the temporary directory using os_chdir
+    with mod.os_chdir(str(temp_dir)):
+        assert os.getcwd() == str(temp_dir)
+
+    # current working directory should be back to initial directory
+    assert os.getcwd() == initial_cwd
+
+
+def test_run_method(tmp_path):
+    """Run tests for run method."""
+    run = mod.run
+
+    # test a command with options:
+    assert run(["date", "+%Y-%m-%d"]).returncode == 0
+    assert run("date +%Y-%m-%d", shell=True).returncode == 0
+
+    # tests interacting with the file system
+    with mod.os_chdir(str(tmp_path)):
+        # ensure single string command works
+        assert run(("ls -l"), shell=True).returncode == 0
+        # test spaces are handled correctly:
+        assert run(["touch", "file1", "file2"]).returncode == 0
+        assert os.path.exists("file1")
+        assert os.path.exists("file2")
+        # test output is captured:
+        assert (
+            b"Hello, world!\n"
+            in run(["echo", "Hello, world!"], capture_output=True).stdout
+        )
+
+    # check exceptions
+    with pytest.raises(FileNotFoundError):
+        run(["doesnotexistcommand"])
+
+    # check error code is checked
+    # 1. expect this to parse: return code is non-zero, but we don't check
+    run(["ls", "/doesnotexist"]),
+    # 2. expect this to fail:
+    with pytest.raises(subprocess.CalledProcessError):
+        run(["ls", "/doesnotexist"], check=True)
+
+
+def test_prepare_environment(tmp_path):
+    """Simulate running preparation of environment.
+    
+    Simulate running ./install-software-environment.py --release dev-23a \
+      --target-directory /tmp/test_prepare_env
+    prepare_env is run when cmd is not specified, we can test cmd='prepare'
+    and cmd=None to check both cases
+    """
+    script_dir = tmp_path / "mpsd_opt" / "linux_debian_11"
+    spack_environments = "spack-environments"
+    mpsd_release_to_test = "dev-23a"
+    release_base_dir = script_dir / mpsd_release_to_test
+    # check that the test directory does not exist
+    assert not script_dir.exists()
+
+    # prepare_environment expects to be executed in git repository
+    # (mpsd-software-environments). It queries the commit on which we are to
+    # log that information. For this to work, we need to execute the command
+    # within a directory tree that has a git repository at the same or high
+    # level. Let's create one:
+    create_mock_git_repository(script_dir)
+
+    # now call the function we want to test
+    result = mod.prepare_environment(
+        mpsd_release=mpsd_release_to_test, script_dir=script_dir
+    )
+
+    # check if the directory now is created
+    assert release_base_dir.exists()
+    # check for spack-environments directory
+    assert spack_environments in os.listdir(release_base_dir)
+
+    # check if the git branch is correctly checked out. We expect output such as
+    # git_branch_stdout = '* dev-23a\n  develop\n'
+    # The entry with the '* ' prefix is the active branch.
+    git_branch_output_raw = subprocess.run(
+        f"cd {str(release_base_dir/spack_environments)} && git branch",
+        shell=True,
+        capture_output=True,
+    )
+    git_branch_stdout = git_branch_output_raw.stdout.decode("utf-8")
+    assert f"* {mpsd_release_to_test}" in git_branch_stdout
+
+    # check that result is a list and contains atleast ['global','foss2021a-mpi']
+    assert isinstance(result, list)
+    assert "global" in result
+    assert "foss2021a-mpi" in result
+
+    # Expect an Exception when wrong mpsd_release is provided
+    with pytest.raises(Exception):
+        result = mod.prepare_environment(
+            mpsd_release="wrong-mpsd-release", script_dir=(script_dir)
+        )
+
+
+def test_setup_log_cmd(tmp_path):
+    """Check that log is updated.
+
+    Check that logs/install-software-environment.log is updated when the module is run
+    """
+    log_file = "install.log"
+
+    script_dir = tmp_path / "test_prepare_env"
+    mpsd_release_to_test = "dev-23a"
+    release_base_dir = script_dir / mpsd_release_to_test
+    if os.path.exists(release_base_dir / log_file):
+        initial_bytes = os.path.getsize(log_file)
+    else:
+        initial_bytes = 0
+
+    # run the prepare_env functionality
+    create_mock_git_repository(target_directory=script_dir, create_directory=True)
+    mod.prepare_environment(mpsd_release=mpsd_release_to_test, script_dir=(script_dir))
+
+    # check that logs/install-software-environment.log is updated
+    assert os.path.exists(release_base_dir / log_file)
+    assert os.path.getsize(release_base_dir / log_file) > initial_bytes
+
+    # Check that the log file has "Spack environments branch: dev-23a " in the last line
+    with open(release_base_dir / log_file, "r") as f:
+        last_line = f.readlines()[-1]
+        assert "Spack environments branch: dev-23a " in last_line
+
+
+def test_install_environment_wrong_toolchain(tmp_path):
+    """Test exception is raised for non-existing toolchain."""
+    # Expect an Exception when wrong toolchains are provided
+    with pytest.raises(Exception):
+        mod.install_environment(
+            mpsd_release="dev-23a",
+            toolchains=["wrong-toolchain"],
+            script_dir=(tmp_path),
+        )
+
+
+def test_install_environment_wrong_mpsd_release(tmp_path):
+    """Test exception is raised for non-existing mpsd release."""
+    # Expect an Exception when wrong mpsd_release is provided (part of
+    # prepare_environment)
+    with pytest.raises(Exception):
+        mod.install_environment(
+            mpsd_release="wrong-mpsd-release",
+            toolchains=["foss2021a-mpi"],
+            script_dir=(tmp_path),
+        )
+
+
+def test_install_environment_zlib():
+    """Test installation of toolchain."""
+    # Prepare a test installation of global generic
+    # with only zlib to test the installation
+    # This is a long test,
+    # its handy to test this with print statements printed to
+    # stdout, use:
+    #   pytest -s
+    # for this installation avoid tmp_path as
+    # the length of the path becomes too long and spack complains
+    script_dir = Path("/tmp/test_global_generic")
+    if script_dir.exists():
+        shutil.rmtree(script_dir)
+    script_dir.mkdir(exist_ok=True, parents=True)
+    mpsd_release_to_test = "dev-23a"
+    toolchain_to_test = "global_generic"
+    mpsd_microarch = os.getenv("MPSD_MICROARCH", "UNKNOWN_MICROARCH")
+    release_base_dir = script_dir / mpsd_release_to_test
+    create_mock_git_repository(target_directory=script_dir, create_directory=False)
+    mod.prepare_environment(mpsd_release=mpsd_release_to_test, script_dir=(script_dir))
+    # Patch the spack environments to create a fake global_generic
+    # create a test toolchain
+    toolchain_src_dir = release_base_dir / "spack-environments" / "toolchains"
+    # with mod.os_chdir(toolchain_src_dir):
+    #     subprocess.run(
+    #         "cp -r foss2021a-mpi fuss1999a", shell=True, capture_output=True
+    #     )
+    # add zlib as a spec to global_generic
+    with open(toolchain_src_dir / "global_generic" / "global_packages.list", "w") as f:
+        f.write("zlib@1.2.13 \n")
+
+    # add zlib to whitelist of module creation file by replacing anaconda3%gcc@10.2.1
+    # with zlib@1.2.13
+    # in release_base_dir / "spack-environments/spack_overlay/etc/spack/modules.yaml"
+    module_file = (
+        release_base_dir / "spack-environments/spack_overlay/etc/spack/modules.yaml"
+    )
+    with open(module_file, "r") as f:
+        lines = f.read().replace("anaconda3%gcc@10.2.1", "zlib@1.2.13")
+    with open(module_file, "w") as f:
+        f.write(lines)
+
+    # Replace gcc@10.2.1 with gcc#13.1.1 or available system gcc for testing on laptop
+    gcc_ver = (
+        subprocess.run(["gcc -dumpfullversion"], shell=True, capture_output=True)
+        .stdout.decode("utf-8")
+        .strip()
+    )
+    assert len(gcc_ver) > 3, f"Couldn't find gcc {gcc_ver=}"
+
+    setup_file = release_base_dir / "spack-environments/spack_setup.sh"
+    with open(setup_file, "r") as f:
+        lines = f.read().replace(
+            'system_compiler="gcc@10.2.1"', f'system_compiler="gcc@{gcc_ver}"'
+        )
+    with open(setup_file, "w") as f:
+        f.write(lines)
+    # install global_generic toolchain
+    mod.install_environment(
+        mpsd_release=mpsd_release_to_test,
+        toolchains=[toolchain_to_test],
+        script_dir=script_dir,
+        enable_build_cache=False,
+    )
+    # test that the build log is created correctly
+    # check that a file with glob build_globale_generic_dev-23a*.log exists at
+    # release_base_dir/mpsd_microarch
+    # print("Debug here ")
+    # time.sleep(10)
+    build_log = list(
+        (release_base_dir / mpsd_microarch / "logs").glob(
+            f"{mpsd_release_to_test}_{toolchain_to_test}_*.log"
+        )
+    )
+    assert len(build_log) > 0
+    # take the most recent build log
+    build_log = sorted(build_log)[0]
+    # check that the build log contains statement ##### Installation finished
+    with open(build_log, "r") as f:
+        lines = f.read()
+        assert "##### Installation finished" in lines
+    build_log_file_name = os.path.basename(build_log)
+
+    # assert that install log files exists
+    assert os.path.exists(release_base_dir / "install.log")
+
+    # assert that the build log is written to the install log file
+    os.path.basename(build_log)
+    with open(release_base_dir / "install.log", "r") as f:
+        lines = f.read()
+        assert (
+            f"installing {toolchain_to_test} and logging at logs/{build_log_file_name}"
+            in lines
+        )
+    # assert that the module files are created correctly
+    assert os.path.exists(release_base_dir / mpsd_microarch)
+    assert os.path.exists(release_base_dir / mpsd_microarch / "lmod")
+    # assert that lmod/module-index.yaml contains zlib
+    with open(
+        release_base_dir / mpsd_microarch / "lmod" / "module-index.yaml", "r"
+    ) as f:
+        lines = f.read()
+        assert "zlib" in lines
+
+
+def test_interface(tmp_path):
+    """Test other things (not implemented yet)."""
+    pass
+    # ensure that installing without toolchains only passes the available toolchains
+    # check that the script branch and hash are correct when running the script
+    # check that the help message is printed when no arguments are provided
+    # check that the help message is printed when -h is provided
+    # check that the error messages are also logged to the log file