Repository: reframe-hpc/reframe

Issue:
Make tests container-runtime agnostic
Currently, the test has to define the container platform and the configuration simply states how that platform is made available. Ideally, we want the test to be able to run without changes on a container platform that is entirely configured at the system level.

Discussion:

Patch:
diff --git a/docs/config_reference.rst b/docs/config_reference.rst
index f24b4d9f..d4f2f41c 100644
--- a/docs/config_reference.rst
+++ b/docs/config_reference.rst
@@ -472,6 +472,16 @@ ReFrame can launch containerized applications, but you need to configure properl
    - ``Singularity``: The `Singularity <https://sylabs.io/>`__ container runtime.
 
 
+.. js:attribute:: .systems[].partitions[].container_platforms[].default
+
+   :required: No
+
+   If set to ``true``, this is the default container platform of this partition.
+   If not specified, the default container platform is assumed to be the first in the list of :js:attr:`container_platforms`.
+
+   .. versionadded:: 3.12.0
+
+
 .. js:attribute:: .systems[].partitions[].container_platforms[].modules
 
    :required: No
diff --git a/docs/tutorial_advanced.rst b/docs/tutorial_advanced.rst
index 0316c813..f23c7fd1 100644
--- a/docs/tutorial_advanced.rst
+++ b/docs/tutorial_advanced.rst
@@ -683,6 +683,9 @@ First, we need to enable the container platform support in ReFrame's configurati
 For each partition, users can define a list of container platforms supported using the :js:attr:`container_platforms` `configuration parameter <config_reference.html#.systems[].partitions[].container_platforms>`__.
 In this case, we define the `Sarus <https://github.com/eth-cscs/sarus>`__ platform for which we set the :js:attr:`modules` parameter in order to instruct ReFrame to load the ``sarus`` module, whenever it needs to run with this container platform.
 Similarly, we add an entry for the `Singularity <https://sylabs.io>`__ platform.
+Optionally, users are allowed to set the ``default`` attribute to :obj:`True` in order to mark a specific container platform as the default of that partition (see below on how this information is being used).
+If no default container platform is specified explicitly, then always the first in the list will be considered as successful.
+
 
 The following parameterized test, will create two tests, one for each of the supported container platforms:
 
@@ -698,14 +701,17 @@ The following parameterized test, will create two tests, one for each of the sup
 
 A container-based test can be written as :class:`~reframe.core.pipeline.RunOnlyRegressionTest` that sets the :attr:`~reframe.core.pipeline.RegressionTest.container_platform` attribute.
 This attribute accepts a string that corresponds to the name of the container platform that will be used to run the container for this test.
-If such a platform is not `configured <config_reference.html#container-platform-configuration>`__ for the current system, the test will fail.
+It is not necessary to specify this attribute, in which case, the default container platform of the current partition will be used.
+You can still differentiate your test based on the actual container platform that is being used by checking the ``self.container_platform.name`` variable.
+
+As soon as the container platform to be used is determined, you need to specify the container image to use by setting the :attr:`~reframe.core.containers.ContainerPlatform.image`.
+If the image is not specified, then the container logic is skipped and the test executes as if the :attr:`~reframe.core.pipeline.RegressionTest.container_platform` was never set.
 
-As soon as the container platform to be used is defined, you need to specify the container image to use by setting the :attr:`~reframe.core.containers.ContainerPlatform.image`.
 In the ``Singularity`` test variant, we add the ``docker://`` prefix to the image name, in order to instruct ``Singularity`` to pull the image from `DockerHub <https://hub.docker.com/>`__.
 The default command that the container runs can be overwritten by setting the :attr:`~reframe.core.containers.ContainerPlatform.command` attribute of the container platform.
 
 The :attr:`~reframe.core.containers.ContainerPlatform.image` is the only mandatory attribute for container-based checks.
-It is important to note that the :attr:`~reframe.core.pipeline.RegressionTest.executable` and :attr:`~reframe.core.pipeline.RegressionTest.executable_opts` attributes of the actual test are ignored in case of container-based tests.
+It is important to note that the :attr:`~reframe.core.pipeline.RegressionTest.executable` and :attr:`~reframe.core.pipeline.RegressionTest.executable_opts` attributes of the actual test are ignored if the containerized code path is taken, i.e., when :attr:`~reframe.core.containers.ContainerPlatform.image` is not :obj:`None`.
 
 ReFrame will run the container according to the given platform as follows:
 
@@ -759,6 +765,117 @@ For a complete list of the available attributes of a specific container platform
 On how to configure ReFrame for running containerized tests, please have a look at the :ref:`container-platform-configuration` section of the :doc:`config_reference`.
 
 
+.. versionchanged:: 3.12.0
+   There is no need any more to explicitly set the :attr:`container_platform` in the test.
+   This is automatically initialized from the default platform of the current partition.
+
+
+
+Combining containerized and native application tests
+====================================================
+
+.. versionadded:: 3.12.0
+
+It is very easy in ReFrame to have a single run-only test to either test the native or the containerized version of an application.
+This is possible, since the framework will only take the "containerized" code path only if the :attr:`~reframe.core.containers.ContainerPlatform.image` attribute of the :attr:`~reframe.core.pipeline.RegressionTest.container_platform` is not :obj:`None`.
+Otherwise, the *bare metal* version of the tested application will be run.
+The following test uses exactly this trick to test a series of GROMACS images as well as the native one provided on the Piz Daint supercomputer.
+It also extends the GROMACS benchmark tests that are provided with ReFrame's test library (see :doc:`hpctestlib`).
+For simplicity, we are assuming a single system here (the hybrid partition of Piz Daint) and we set fixed values for the :attr:`num_cpus_per_task` as well as the ``-ntomp`` option of GROMACS (NB: in a real-world test we would use the auto-detected processor topology information to set these values; see :ref:`proc-autodetection` for more information).
+We also redefine and restrict the benchmark's parameters ``benchmark_info`` and ``nb_impl`` to the values that are of interest for the demonstration of this test.
+Finally, we also reset the executable to use ``gmx`` instead of the ``gmx_mpi`` that is used from the library test.
+
+
+.. literalinclude:: ../tutorials/advanced/containers/gromacs_test.py
+   :start-after: # rfmdocstart: gromacstest
+
+All this test does in addition to the library test it inherits from is to set the :attr:`~reframe.core.containers.ContainerPlatform.image` and the :attr:`~reframe.core.containers.ContainerPlatform.command` attributes of the :attr:`~reframe.core.pipeline.RegressionTest.container_platform`.
+The former is set from the ``gromacs_image`` test parameter whereas the latter from the test's :attr:`~reframe.core.pipeline.RegressionTest.executable` and :attr:`~reframe.core.pipeline.RegressionTest.executable_opts` attributes.
+Remember that these attributes are ignored if the framework takes the path of launching  a container.
+Finally, if the image is :obj:`None` we handle the case of the native run, in which case we load the modules required to run GROMACS natively on the target system.
+
+In the following, we run the GPU version of a single benchmark with a series of images from NVIDIA and natively:
+
+.. code-block:: console
+
+   $ ./bin/reframe -C tutorials/config/settings.py -c tutorials/advanced/containers/gromacs_test.py -r
+
+.. code-block:: console
+
+   [==========] Running 6 check(s)
+   [==========] Started on Fri Jun 17 16:20:16 2022
+
+   [----------] start processing checks
+   [ RUN      ] gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2022.1 @daint:gpu+gnu
+   [ RUN      ] gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2021.3 @daint:gpu+gnu
+   [ RUN      ] gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2021 @daint:gpu+gnu
+   [ RUN      ] gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2020.2 @daint:gpu+gnu
+   [ RUN      ] gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2020 @daint:gpu+gnu
+   [ RUN      ] gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=None @daint:gpu+gnu
+   [       OK ] (1/6) gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2020.2 @daint:gpu+gnu
+   [       OK ] (2/6) gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2020 @daint:gpu+gnu
+   [       OK ] (3/6) gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=None @daint:gpu+gnu
+   [       OK ] (4/6) gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2022.1 @daint:gpu+gnu
+   [       OK ] (5/6) gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2021 @daint:gpu+gnu
+   [       OK ] (6/6) gromacs_containerized_test %benchmark_info=HECBioSim/hEGFRDimerSmallerPL %nb_impl=gpu %gromacs_image=nvcr.io/hpc/gromacs:2021.3 @daint:gpu+gnu
+   [----------] all spawned checks have finished
+
+   [  PASSED  ] Ran 6/6 test case(s) from 6 check(s) (0 failure(s), 0 skipped)
+   [==========] Finished on Fri Jun 17 16:23:47 2022
+
+
+We can also inspect the generated job scripts for the native and a containerized run:
+
+.. code-block:: console
+
+   cat output/daint/gpu/gnu/gromacs_containerized_test_0/rfm_gromacs_containerized_test_0_job.sh
+
+.. code-block:: bash
+
+   #!/bin/bash
+   #SBATCH --job-name="rfm_gromacs_containerized_test_0_job"
+   #SBATCH --ntasks=1
+   #SBATCH --ntasks-per-node=1
+   #SBATCH --cpus-per-task=12
+   #SBATCH --output=rfm_gromacs_containerized_test_0_job.out
+   #SBATCH --error=rfm_gromacs_containerized_test_0_job.err
+   #SBATCH -A csstaff
+   #SBATCH --constraint=gpu
+   #SBATCH --hint=nomultithread
+   module unload PrgEnv-cray
+   module load PrgEnv-gnu
+   module load daint-gpu
+   module load GROMACS
+   curl -LJO https://github.com/victorusu/GROMACS_Benchmark_Suite/raw/1.0.0/HECBioSim/hEGFRDimerSmallerPL/benchmark.tpr
+   srun gmx mdrun -dlb yes -ntomp 12 -npme -1 -v -nb gpu -s benchmark.tpr
+
+And the containerized run:
+
+.. code-block:: console
+
+   cat output/daint/gpu/gnu/gromacs_containerized_test_1/rfm_gromacs_containerized_test_1_job.sh
+
+.. code-block:: bash
+
+   #!/bin/bash
+   #SBATCH --job-name="rfm_gromacs_containerized_test_1_job"
+   #SBATCH --ntasks=1
+   #SBATCH --ntasks-per-node=1
+   #SBATCH --cpus-per-task=12
+   #SBATCH --output=rfm_gromacs_containerized_test_1_job.out
+   #SBATCH --error=rfm_gromacs_containerized_test_1_job.err
+   #SBATCH -A csstaff
+   #SBATCH --constraint=gpu
+   #SBATCH --hint=nomultithread
+   module unload PrgEnv-cray
+   module load PrgEnv-gnu
+   module load sarus
+   curl -LJO https://github.com/victorusu/GROMACS_Benchmark_Suite/raw/1.0.0/HECBioSim/hEGFRDimerSmallerPL/benchmark.tpr
+   sarus pull nvcr.io/hpc/gromacs:2020
+   srun sarus run --mount=type=bind,source="/users/user/Devel/reframe/stage/daint/gpu/gnu/gromacs_containerized_test_43",destination="/rfm_workdir" -w /rfm_workdir nvcr.io/hpc/gromacs:2020 gmx mdrun -dlb yes -ntomp 12 -npme -1 -v -nb gpu -s benchmark.tpr
+
+
+
 Writing reusable tests
 ----------------------
 
diff --git a/reframe/core/containers.py b/reframe/core/containers.py
index 5ad5ad19..b35f54b7 100644
--- a/reframe/core/containers.py
+++ b/reframe/core/containers.py
@@ -6,8 +6,9 @@
 import abc
 
 import reframe.core.fields as fields
+import reframe.core.warnings as warn
+import reframe.utility as util
 import reframe.utility.typecheck as typ
-from reframe.core.exceptions import ContainerError
 
 
 _STAGEDIR_MOUNT = '/rfm_workdir'
@@ -80,24 +81,18 @@ class ContainerPlatform(abc.ABC):
     #: :default: ``[]``
     options = fields.TypedField(typ.List[str])
 
-    _workdir = fields.TypedField(str, type(None))
     #: The working directory of ReFrame inside the container.
     #:
     #: This is the directory where the test's stage directory is mounted inside
     #: the container. This directory is always mounted regardless if
     #: :attr:`mount_points` is set or not.
     #:
-    #: .. deprecated:: 3.5
-    #:    Please use the `options` field to set the working directory.
-    #:
     #: :type: :class:`str`
     #: :default: ``/rfm_workdir``
-    workdir = fields.DeprecatedField(
-        _workdir,
-        'The `workdir` field is deprecated, please use the `options` field to '
-        'set the container working directory',
-        fields.DeprecatedField.OP_SET, from_version='3.5.0'
-    )
+    #:
+    #: .. versionchanged:: 3.12.0
+    #:    This attribute is no more deprecated.
+    workdir = fields.TypedField(str, type(None))
 
     def __init__(self):
         self.image = None
@@ -106,8 +101,8 @@ class ContainerPlatform(abc.ABC):
         # NOTE: Here we set the target fields directly to avoid the deprecation
         # warnings
         self._commands = []
-        self._workdir = _STAGEDIR_MOUNT
 
+        self.workdir = _STAGEDIR_MOUNT
         self.mount_points  = []
         self.options = []
         self.pull_image = True
@@ -143,12 +138,37 @@ class ContainerPlatform(abc.ABC):
         :arg stagedir: The stage directory of the test.
         '''
 
-    def validate(self):
-        if self.image is None:
-            raise ContainerError('no image specified')
+    @classmethod
+    def create(cls, name):
+        '''Factory method to create a new container by name.'''
+        name = name.capitalize()
+        try:
+            return globals()[name]()
+        except KeyError:
+            raise ValueError(f'unknown container platform: {name}') from None
+
+    @classmethod
+    def create_from(cls, name, other):
+        new = cls.create(name)
+        new.image = other.image
+        new.command = other.command
+        new.mount_points = other.mount_points
+        new.options = other.options
+        new.pull_image = other.pull_image
+        new.workdir = other.workdir
+
+        # Update deprecated fields
+        with warn.suppress_deprecations():
+            new.commands = other.commands
+
+        return new
+
+    @property
+    def name(self):
+        return type(self).__name__
 
     def __str__(self):
-        return type(self).__name__
+        return self.name
 
     def __rfm_json_encode__(self):
         return str(self)
@@ -165,15 +185,17 @@ class Docker(ContainerPlatform):
         super().launch_command(stagedir)
         mount_points = self.mount_points + [(stagedir, _STAGEDIR_MOUNT)]
         run_opts = [f'-v "{mp[0]}":"{mp[1]}"' for mp in mount_points]
-        run_opts += self.options
+        if self.workdir:
+            run_opts.append(f'-w {self.workdir}')
 
+        run_opts += self.options
         if self.command:
             return (f'docker run --rm {" ".join(run_opts)} '
                     f'{self.image} {self.command}')
 
         if self.commands:
             return (f"docker run --rm {' '.join(run_opts)} {self.image} "
-                    f"bash -c 'cd {self.workdir}; {'; '.join(self.commands)}'")
+                    f"bash -c '{'; '.join(self.commands)}'")
 
         return f'docker run --rm {" ".join(run_opts)} {self.image}'
 
@@ -197,7 +219,8 @@ class Sarus(ContainerPlatform):
         # The format that Sarus uses to call the images is
         # <reposerver>/<user>/<image>:<tag>. If an image was loaded
         # locally from a tar file, the <reposerver> is 'load'.
-        if not self.pull_image or self.image.startswith('load/'):
+        if (not self.pull_image or not self.image or
+            self.image.startswith('load/')):
             return []
         else:
             return [f'{self._command} pull {self.image}']
@@ -210,15 +233,17 @@ class Sarus(ContainerPlatform):
         if self.with_mpi:
             run_opts.append('--mpi')
 
-        run_opts += self.options
+        if self.workdir:
+            run_opts.append(f'-w {self.workdir}')
 
+        run_opts += self.options
         if self.command:
             return (f'{self._command} run {" ".join(run_opts)} {self.image} '
                     f'{self.command}')
 
         if self.commands:
             return (f"{self._command} run {' '.join(run_opts)} {self.image} "
-                    f"bash -c 'cd {self.workdir}; {'; '.join(self.commands)}'")
+                    f"bash -c '{'; '.join(self.commands)}'")
 
         return f'{self._command} run {" ".join(run_opts)} {self.image}'
 
@@ -232,6 +257,12 @@ class Shifter(Sarus):
         super().__init__()
         self._command = 'shifter'
 
+    def launch_command(self, stagedir):
+        # Temporarily change `workdir`, since Sarus and Shifter have otherwise
+        # the same interface
+        with util.temp_setattr(self, 'workdir', None):
+            return super().launch_command(stagedir)
+
 
 class Singularity(ContainerPlatform):
     '''Container platform backend for running containers with `Singularity
@@ -257,6 +288,9 @@ class Singularity(ContainerPlatform):
         if self.with_cuda:
             run_opts.append('--nv')
 
+        if self.workdir:
+            run_opts.append(f'-W {self.workdir}')
+
         run_opts += self.options
         if self.command:
             return (f'singularity exec {" ".join(run_opts)} '
@@ -264,7 +298,7 @@ class Singularity(ContainerPlatform):
 
         if self.commands:
             return (f"singularity exec {' '.join(run_opts)} {self.image} "
-                    f"bash -c 'cd {self.workdir}; {'; '.join(self.commands)}'")
+                    f"bash -c '{'; '.join(self.commands)}'")
 
         return f'singularity run {" ".join(run_opts)} {self.image}'
 
@@ -275,10 +309,6 @@ class ContainerPlatformField(fields.TypedField):
 
     def __set__(self, obj, value):
         if isinstance(value, str):
-            try:
-                value = globals()[value]()
-            except KeyError:
-                raise ValueError(
-                    f'unknown container platform: {value}') from None
+            value = ContainerPlatform.create(value)
 
         super().__set__(obj, value)
diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py
index b711749c..9a8f94f6 100644
--- a/reframe/core/pipeline.py
+++ b/reframe/core/pipeline.py
@@ -22,7 +22,6 @@ import numbers
 import os
 import shutil
 
-import reframe.core.environments as env
 import reframe.core.fields as fields
 import reframe.core.hooks as hooks
 import reframe.core.logging as logging
@@ -35,9 +34,10 @@ import reframe.utility.typecheck as typ
 import reframe.utility.udeps as udeps
 from reframe.core.backends import getlauncher, getscheduler
 from reframe.core.buildsystems import BuildSystemField
-from reframe.core.containers import ContainerPlatformField
+from reframe.core.containers import (ContainerPlatform, ContainerPlatformField)
 from reframe.core.deferrable import (_DeferredExpression,
                                      _DeferredPerformanceExpression)
+from reframe.core.environments import Environment
 from reframe.core.exceptions import (BuildError, DependencyError,
                                      PerformanceError, PipelineError,
                                      SanityError, SkipTestError,
@@ -48,6 +48,20 @@ from reframe.core.variables import DEPRECATE_WR
 from reframe.core.warnings import user_deprecation_warning
 
 
+class _NoRuntime(ContainerPlatform):
+    '''Proxy container runtime for storing container platform info early enough.
+
+    This will be replaced by the framework with a concrete implementation
+    based on the current partition info.
+    '''
+
+    def emit_prepare_commands(self, stagedir):
+        raise NotImplementedError
+
+    def launch_command(self, stagedir):
+        raise NotImplementedError
+
+
 # Dependency kinds
 
 #: Constant to be passed as the ``how`` argument of the
@@ -458,26 +472,41 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
     #:
     #: The container platform to be used for launching this test.
     #:
-    #: If this field is set, the test will run inside a container using the
-    #: specified container runtime. Container-specific options must be defined
-    #: additionally after this field is set:
+    #: This field is set automatically by the default container runtime
+    #: associated with the current system partition. Users may also set this,
+    #: explicitly overriding any partition setting. If the
+    #: :attr:`~reframe.core.containers.ContainerPlatform.image` attribute of
+    #: :attr:`container_platform` is set, then the test will run inside a
+    #: container using the specified container runtime.
     #:
     #: .. code:: python
     #:
     #:    self.container_platform = 'Singularity'
     #:    self.container_platform.image = 'docker://ubuntu:18.04'
-    #:    self.container_platform.commands = ['cat /etc/os-release']
+    #:    self.container_platform.command = 'cat /etc/os-release'
     #:
-    #: If this field is set, :attr:`executable` and :attr:`executable_opts`
-    #: attributes are ignored. The container platform's :attr:`commands
-    #: <reframe.core.containers.ContainerPlatform.commands>` will be used
+    #: If the test will run inside a container, the :attr:`executable` and
+    #: :attr:`executable_opts` attributes are ignored. The container platform's
+    #: :attr:`~reframe.core.containers.ContainerPlatform.command` will be used
     #: instead.
     #:
+    #: .. note::
+    #:
+    #:    Only the run phase of the test will run inside the container.
+    #:    If you enable the containerized run in a non run-only test, the
+    #:    compilation phase will still run natively.
+    #:
     #: :type: :class:`str` or
-    #:     :class:`reframe.core.containers.ContainerPlatform`.
-    #: :default: :class:`None`.
-    container_platform = variable(type(None),
-                                  field=ContainerPlatformField, value=None)
+    #:     :class:`~reframe.core.containers.ContainerPlatform`.
+    #: :default: the container runtime specified in the current system
+    #:   partition's configuration (see also
+    #:   :ref:`container-platform-configuration`).
+    #:
+    #: .. versionchanged:: 3.12.0
+    #:    This field is now set automatically from the current partition's
+    #:    configuration.
+    container_platform = variable(field=ContainerPlatformField,
+                                  value=_NoRuntime())
 
     #: .. versionadded:: 3.0
     #:
@@ -1054,7 +1083,7 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
         self._case = None
 
         if rt.runtime().get_option('general/0/non_default_craype'):
-            self._cdt_environ = env.Environment(
+            self._cdt_environ = Environment(
                 name='__rfm_cdt_environ',
                 variables={
                     'LD_LIBRARY_PATH': '$CRAY_LD_LIBRARY_PATH:$LD_LIBRARY_PATH'
@@ -1062,7 +1091,7 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
             )
         else:
             # Just an empty environment
-            self._cdt_environ = env.Environment('__rfm_cdt_environ')
+            self._cdt_environ = Environment('__rfm_cdt_environ')
 
         # Disabled hooks
         self._disabled_hooks = set()
@@ -1564,7 +1593,7 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
         except OSError as e:
             raise PipelineError('failed to set up paths') from e
 
-    def _setup_job(self, name, force_local=False, **job_opts):
+    def _create_job(self, name, force_local=False, **job_opts):
         '''Setup the job related to this check.'''
 
         if force_local:
@@ -1586,9 +1615,31 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
                           sched_access=self._current_partition.access,
                           **job_opts)
 
+    def _setup_build_job(self, **job_opts):
+        self._build_job = self._create_job(f'rfm_{self.unique_name}_build',
+                                           self.local or self.build_locally,
+                                           **job_opts)
+
+    def _setup_run_job(self, **job_opts):
+        self._job = self._create_job(f'rfm_{self.unique_name}_job',
+                                     self.local, **job_opts)
+
     def _setup_perf_logging(self):
         self._perf_logger = logging.getperflogger(self)
 
+    def _setup_container_platform(self):
+        try:
+            self.container_platform.emit_prepare_commands(self.stagedir)
+        except NotImplementedError:
+            cplatf_name = self.current_partition.container_runtime
+            if cplatf_name:
+                try:
+                    self.container_platform = ContainerPlatform.create_from(
+                        cplatf_name, self.container_platform
+                    )
+                except ValueError:
+                    pass
+
     @final
     def setup(self, partition, environ, **job_opts):
         '''The setup phase of the regression test pipeline.
@@ -1617,13 +1668,10 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
         self._current_partition = partition
         self._current_environ = environ
         self._setup_paths()
+        self._setup_build_job(**job_opts)
+        self._setup_run_job(**job_opts)
+        self._setup_container_platform()
         self._resolve_fixtures()
-        self._job = self._setup_job(f'rfm_{self.unique_name}_job',
-                                    self.local,
-                                    **job_opts)
-        self._build_job = self._setup_job(f'rfm_{self.unique_name}_build',
-                                          self.local or self.build_locally,
-                                          **job_opts)
 
     def _copy_to_stagedir(self, path):
         self.logger.debug(f'Copying {path} to stage directory')
@@ -1662,8 +1710,6 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
               more details.
 
         '''
-        if not self._current_environ:
-            raise PipelineError('no programming environment set')
 
         # Copy the check's resources to the stage directory
         if self.sourcesdir:
@@ -1724,8 +1770,8 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
             self.build_system.srcfile = self.sourcepath
             self.build_system.executable = self.executable
 
-        user_environ = env.Environment(type(self).__name__,
-                                       self.modules, self.variables.items())
+        user_environ = Environment(self.unique_name,
+                                   self.modules, self.variables.items())
         environs = [self._current_partition.local_env, self._current_environ,
                     user_environ, self._cdt_environ]
         self._build_job.time_limit = (
@@ -1803,28 +1849,30 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
               more details.
 
         '''
-        if not self.current_system or not self._current_partition:
-            raise PipelineError('no system or system partition is set')
 
-        if self.container_platform:
+        def _get_cp_env():
+            '''Retrieve the container platform environment.'''
             try:
-                cp_name = type(self.container_platform).__name__
-                cp_env = self._current_partition.container_environs[cp_name]
-            except KeyError as e:
-                raise PipelineError(
-                    'container platform not configured '
-                    'on the current partition: %s' % e) from None
-
-            self.container_platform.validate()
+                cp_name = self.container_platform.name
+                return self.current_partition.container_environs[cp_name]
+            except KeyError:
+                return None
 
+        cp = self.container_platform
+        if cp.image:
             # We replace executable and executable_opts in case of containers
-            self.executable = self.container_platform.launch_command(
-                self.stagedir)
-            self.executable_opts = []
-            prepare_container = self.container_platform.emit_prepare_commands(
-                self.stagedir)
-            if prepare_container:
-                self.prerun_cmds += prepare_container
+            try:
+                self.executable = cp.launch_command(self.stagedir)
+                self.executable_opts = []
+                prepare_container = cp.emit_prepare_commands(self.stagedir)
+                if prepare_container:
+                    self.prerun_cmds += prepare_container
+            except NotImplementedError:
+                raise PipelineError(
+                    "no container runtime was configured; "
+                    "consider setting the 'container_platform' test attribute "
+                    "or the corresponding partition configuration setting"
+                )
 
         self.job.num_tasks = self.num_tasks
         self.job.num_tasks_per_node = self.num_tasks_per_node
@@ -1851,22 +1899,18 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
             ' '.join(exec_cmd).strip(),
             *self.postrun_cmds
         ]
-        user_environ = env.Environment(type(self).__name__,
-                                       self.modules, self.variables.items())
+        user_environ = Environment(self.unique_name,
+                                   self.modules, self.variables.items())
         environs = [
             self._current_partition.local_env,
             self._current_environ,
             user_environ,
             self._cdt_environ
         ]
-        if self.container_platform and cp_env:
-            environs = [
-                self._current_partition.local_env,
-                self._current_environ,
-                cp_env,
-                user_environ,
-                self._cdt_environ
-            ]
+        if self.container_platform.image:
+            cp_env = _get_cp_env()
+            if cp_env:
+                environs.insert(2, cp_env)
 
         # num_gpus_per_node is a managed resource
         if self.num_gpus_per_node > 0:
@@ -2495,8 +2539,8 @@ class RunOnlyRegressionTest(RegressionTest, special=True):
         self._current_partition = partition
         self._current_environ = environ
         self._setup_paths()
-        self._job = self._setup_job(f'rfm_{self.unique_name}_job',
-                                    self.local, **job_opts)
+        self._setup_run_job(**job_opts)
+        self._setup_container_platform()
         self._resolve_fixtures()
 
     def compile(self):
@@ -2552,9 +2596,8 @@ class CompileOnlyRegressionTest(RegressionTest, special=True):
         self._current_partition = partition
         self._current_environ = environ
         self._setup_paths()
-        self._build_job = self._setup_job(f'rfm_{self.unique_name}_build',
-                                          self.local or self.build_locally,
-                                          **job_opts)
+        self._setup_build_job(**job_opts)
+        self._setup_container_platform()
         self._resolve_fixtures()
 
     @property
diff --git a/reframe/core/systems.py b/reframe/core/systems.py
index 2312a3a9..4a5d4682 100644
--- a/reframe/core/systems.py
+++ b/reframe/core/systems.py
@@ -164,8 +164,8 @@ class SystemPartition(jsonext.JSONSerializable):
     '''
 
     def __init__(self, *, parent, name, sched_type, launcher_type,
-                 descr, access, container_environs, resources,
-                 local_env, environs, max_jobs, prepare_cmds,
+                 descr, access, container_runtime, container_environs,
+                 resources, local_env, environs, max_jobs, prepare_cmds,
                  processor, devices, extras, features, time_limit):
         getlogger().debug(f'Initializing system partition {name!r}')
         self._parent_system = parent
@@ -175,6 +175,7 @@ class SystemPartition(jsonext.JSONSerializable):
         self._launcher_type = launcher_type
         self._descr = descr
         self._access = access
+        self._container_runtime = container_runtime
         self._container_environs = container_environs
         self._local_env = local_env
         self._environs = environs
@@ -212,6 +213,14 @@ class SystemPartition(jsonext.JSONSerializable):
 
         return util.SequenceView(self._environs)
 
+    @property
+    def container_runtime(self):
+        '''The default container runtime of this partition.
+
+        :type: :class:`str` or ``None``
+        '''
+        return self._container_runtime
+
     @property
     def container_environs(self):
         '''Environments associated with the different container platforms.
@@ -494,9 +503,10 @@ class System(jsonext.JSONSerializable):
             part_sched = getscheduler(site_config.get(f'{partid}/scheduler'))
             part_launcher = getlauncher(site_config.get(f'{partid}/launcher'))
             part_container_environs = {}
-            for i, p in enumerate(
-                    site_config.get(f'{partid}/container_platforms')
-            ):
+            part_container_runtime = None
+            container_platforms = site_config.get(
+                f'{partid}/container_platforms')
+            for i, p in enumerate(container_platforms):
                 ctype = p['type']
                 part_container_environs[ctype] = Environment(
                     name=f'__rfm_env_{ctype}',
@@ -507,6 +517,12 @@ class System(jsonext.JSONSerializable):
                         f'{partid}/container_platforms/{i}/variables'
                     )
                 )
+                if p.get('default', None):
+                    part_container_runtime = ctype
+
+            if not part_container_runtime and container_platforms:
+                # No default set, pick the first one
+                part_container_runtime = container_platforms[0]['type']
 
             env_patt = site_config.get('general/0/valid_env_names') or [r'.*']
             part_environs = [
@@ -537,6 +553,7 @@ class System(jsonext.JSONSerializable):
                     access=site_config.get(f'{partid}/access'),
                     resources=site_config.get(f'{partid}/resources'),
                     environs=part_environs,
+                    container_runtime=part_container_runtime,
                     container_environs=part_container_environs,
                     local_env=Environment(
                         name=f'__rfm_env_{part_name}',
diff --git a/reframe/schemas/config.json b/reframe/schemas/config.json
index 2dcf1930..15030360 100644
--- a/reframe/schemas/config.json
+++ b/reframe/schemas/config.json
@@ -271,19 +271,14 @@
                                     "items": {
                                         "type": "object",
                                         "properties": {
-                                            "type": {
-                                                "type": "string",
-                                                "enum": [
-                                                    "Docker", "Sarus",
-                                                    "Shifter", "Singularity"
-                                                ]
-                                            },
+                                            "type": {"type": "string"},
                                             "modules": {
                                                 "$ref": "#/defs/modules_list"
                                             },
                                             "variables": {
                                                 "$ref": "#/defs/envvar_list"
-                                            }
+                                            },
+                                            "default": {"type": "boolean"}
                                         },
                                         "required": ["type"]
                                     }
@@ -578,6 +573,7 @@
         "systems/partitions/descr": "",
         "systems/partitions/access": [],
         "systems/partitions/environs": [],
+        "systems/partitions/container_runtime": null,
         "systems/partitions/container_platforms": [],
         "systems/partitions/container_platforms/*modules": [],
         "systems/partitions/container_platforms/*variables": [],
diff --git a/reframe/utility/__init__.py b/reframe/utility/__init__.py
index 2fa3255a..0358c525 100644
--- a/reframe/utility/__init__.py
+++ b/reframe/utility/__init__.py
@@ -925,6 +925,23 @@ def nodelist_abbrev(nodes):
     return ','.join(str(ng) for ng in node_groups)
 
 
+class temp_setattr:
+    '''Context manager to temporarily change the attribute value of an
+    object.'''
+
+    def __init__(self, obj, attr, val):
+        self._obj = obj
+        self._attr = attr
+        self._newval = val
+        self._saved = getattr(obj, attr)
+
+    def __enter__(self):
+        setattr(self._obj, self._attr, self._newval)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        setattr(self._obj, self._attr, self._saved)
+
+
 class ScopedDict(UserDict):
     '''This is a special dictionary that imposes scopes on its keys.
 


Test patch:
diff --git a/tutorials/advanced/containers/gromacs_test.py b/tutorials/advanced/containers/gromacs_test.py
new file mode 100644
index 00000000..bd799d83
--- /dev/null
+++ b/tutorials/advanced/containers/gromacs_test.py
@@ -0,0 +1,49 @@
+# Copyright 2016-2022 Swiss National Supercomputing Centre (CSCS/ETH Zurich)
+# ReFrame Project Developers. See the top-level LICENSE file for details.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# rfmdocstart: gromacstest
+import reframe as rfm
+from hpctestlib.sciapps.gromacs.benchmarks import gromacs_check
+
+
+def _hecbiosim_bench(params):
+    for p in params:
+        if p[0] == 'HECBioSim/hEGFRDimerSmallerPL':
+            return [p]
+
+
+@rfm.simple_test
+class gromacs_containerized_test(gromacs_check):
+    # Restrict library test parameters to only those relevant for this example
+    benchmark_info = parameter(inherit_params=True,
+                               filter_params=_hecbiosim_bench,
+                               fmt=lambda x: x[0])
+    nb_impl = parameter(['gpu'])
+
+    # New parameter for testing the various images
+    gromacs_image = parameter([
+        None,
+        'nvcr.io/hpc/gromacs:2020',
+        'nvcr.io/hpc/gromacs:2020.2',
+        'nvcr.io/hpc/gromacs:2021',
+        'nvcr.io/hpc/gromacs:2021.3',
+        'nvcr.io/hpc/gromacs:2022.1'
+    ])
+    valid_systems = ['daint:gpu']
+    valid_prog_environs = ['gnu']
+    use_multithreading = False
+    executable = 'gmx mdrun'
+    executable_opts += ['-dlb yes', '-ntomp 12', '-npme -1', '-v']
+    num_tasks = 1
+    num_tasks_per_node = 1
+    num_cpus_per_task = 12
+
+    @run_after('init')
+    def setup_container_run(self):
+        exec_cmd = ' '.join([self.executable, *self.executable_opts])
+        self.container_platform.image = self.gromacs_image
+        self.container_platform.command = exec_cmd
+        if self.gromacs_image is None:
+            self.modules = ['daint-gpu', 'GROMACS']
diff --git a/unittests/resources/settings.py b/unittests/resources/settings.py
index 5d2d349c..0e10da74 100644
--- a/unittests/resources/settings.py
+++ b/unittests/resources/settings.py
@@ -59,7 +59,12 @@ site_configuration = {
                     'launcher': 'local',
                     'environs': ['PrgEnv-cray', 'PrgEnv-gnu'],
                     'descr': 'Login nodes',
-                    'features': ['cross_compile']
+                    'features': ['cross_compile'],
+                    'container_platforms': [
+                        {'type': 'Sarus'},
+                        {'type': 'Docker', 'default': True},
+                        {'type': 'Singularity'}
+                    ]
                 },
                 {
                     'name': 'gpu',
@@ -89,6 +94,7 @@ site_configuration = {
                     'extras': {
                         'gpu_arch': 'a100'
                     },
+                    'container_platforms': [{'type': 'Sarus'}],
                     'environs': ['PrgEnv-gnu', 'builtin'],
                     'max_jobs': 10,
                     'processor': {
diff --git a/unittests/test_config.py b/unittests/test_config.py
index 68be66ec..57fc912f 100644
--- a/unittests/test_config.py
+++ b/unittests/test_config.py
@@ -244,11 +244,16 @@ def test_select_subconfig():
     assert site_config.get('systems/0/partitions/0/scheduler') == 'local'
     assert site_config.get('systems/0/partitions/0/launcher') == 'local'
     assert (site_config.get('systems/0/partitions/0/environs') ==
-            ['PrgEnv-cray', 'PrgEnv-gnu'])
+            ['PrgEnv-cray', 'PrgEnv-gnu']
+            )
     assert site_config.get('systems/0/partitions/0/descr') == 'Login nodes'
     assert site_config.get('systems/0/partitions/0/resources') == []
     assert site_config.get('systems/0/partitions/0/access') == []
-    assert site_config.get('systems/0/partitions/0/container_platforms') == []
+    assert site_config.get('systems/0/partitions/0/container_platforms') == [
+        {'type': 'Sarus'},
+        {'type': 'Docker', 'default': True},
+        {'type': 'Singularity'}
+    ]
     assert site_config.get('systems/0/partitions/0/modules') == []
     assert site_config.get('systems/0/partitions/0/variables') == []
     assert site_config.get('systems/0/partitions/0/max_jobs') == 8
@@ -258,7 +263,8 @@ def test_select_subconfig():
     assert site_config.get('environments/@PrgEnv-cray/cc') == 'cc'
     assert site_config.get('environments/1/cxx') == 'CC'
     assert (site_config.get('environments/@PrgEnv-cray/modules') ==
-            [{'name': 'PrgEnv-cray', 'collection': False, 'path': None}])
+            [{'name': 'PrgEnv-cray', 'collection': False, 'path': None}]
+            )
     assert site_config.get('environments/@PrgEnv-gnu/extras') == {'foo': 1,
                                                                   'bar': 'x'}
     assert site_config.get('environments/@PrgEnv-gnu/features') == ['cxx14']
@@ -355,7 +361,8 @@ def test_system_create():
     assert partition.scheduler.registered_name == 'slurm'
     assert partition.launcher_type.registered_name == 'srun'
     assert partition.access == []
-    assert partition.container_environs == {}
+    assert len(partition.container_environs) == 1
+    assert partition.container_runtime == 'Sarus'
     assert partition.local_env.modules == ['foogpu']
     assert partition.local_env.modules_detailed == [{
         'name': 'foogpu', 'collection': False, 'path': '/foo'
@@ -391,6 +398,12 @@ def test_system_create():
     assert partition.processor.num_numa_nodes == 1
     assert partition.processor.num_cores_per_numa_node == 4
 
+    # Select another subconfig and check that the default selection of
+    # container runtime is done properly
+    site_config.select_subconfig('testsys:login')
+    system = System.create(site_config)
+    assert system.partitions[0].container_runtime == 'Docker'
+
 
 def test_hostname_autodetection():
     # This exercises only the various execution paths
diff --git a/unittests/test_containers.py b/unittests/test_containers.py
index 830f2daf..6573bdec 100644
--- a/unittests/test_containers.py
+++ b/unittests/test_containers.py
@@ -7,7 +7,6 @@ import pytest
 
 import reframe.core.containers as containers
 import reframe.core.warnings as warn
-from reframe.core.exceptions import ContainerError
 
 
 @pytest.fixture(params=[
@@ -50,35 +49,35 @@ def expected_cmd_mount_points(container_variant):
     if container_variant in {'Docker', 'Docker+nopull'}:
         return ('docker run --rm -v "/path/one":"/one" '
                 '-v "/path/two":"/two" '
-                '-v "/foo":"/rfm_workdir" image:tag cmd')
+                '-v "/foo":"/rfm_workdir" -w /rfm_workdir image:tag cmd')
     elif container_variant == 'Docker+nocommand':
         return ('docker run --rm -v "/path/one":"/one" '
                 '-v "/path/two":"/two" '
-                '-v "/foo":"/rfm_workdir" image:tag')
+                '-v "/foo":"/rfm_workdir" -w /rfm_workdir image:tag')
     elif container_variant in {'Sarus', 'Sarus+nopull'}:
         return ('sarus run '
                 '--mount=type=bind,source="/path/one",destination="/one" '
                 '--mount=type=bind,source="/path/two",destination="/two" '
                 '--mount=type=bind,source="/foo",destination="/rfm_workdir" '
-                'image:tag cmd')
+                '-w /rfm_workdir image:tag cmd')
     elif container_variant == 'Sarus+nocommand':
         return ('sarus run '
                 '--mount=type=bind,source="/path/one",destination="/one" '
                 '--mount=type=bind,source="/path/two",destination="/two" '
                 '--mount=type=bind,source="/foo",destination="/rfm_workdir" '
-                'image:tag')
+                '-w /rfm_workdir image:tag')
     elif container_variant == 'Sarus+mpi':
         return ('sarus run '
                 '--mount=type=bind,source="/path/one",destination="/one" '
                 '--mount=type=bind,source="/path/two",destination="/two" '
                 '--mount=type=bind,source="/foo",destination="/rfm_workdir" '
-                '--mpi image:tag cmd')
+                '--mpi -w /rfm_workdir image:tag cmd')
     elif container_variant == 'Sarus+load':
         return ('sarus run '
                 '--mount=type=bind,source="/path/one",destination="/one" '
                 '--mount=type=bind,source="/path/two",destination="/two" '
                 '--mount=type=bind,source="/foo",destination="/rfm_workdir" '
-                'load/library/image:tag cmd')
+                '-w /rfm_workdir load/library/image:tag cmd')
     elif container_variant in {'Shifter', 'Shifter+nopull'}:
         return ('shifter run '
                 '--mount=type=bind,source="/path/one",destination="/one" '
@@ -105,13 +104,16 @@ def expected_cmd_mount_points(container_variant):
                 'load/library/image:tag cmd')
     elif container_variant in {'Singularity', 'Singularity+nopull'}:
         return ('singularity exec -B"/path/one:/one" '
-                '-B"/path/two:/two" -B"/foo:/rfm_workdir" image:tag cmd')
+                '-B"/path/two:/two" -B"/foo:/rfm_workdir" '
+                '-W /rfm_workdir image:tag cmd')
     elif container_variant == 'Singularity+cuda':
         return ('singularity exec -B"/path/one:/one" '
-                '-B"/path/two:/two" -B"/foo:/rfm_workdir" --nv image:tag cmd')
+                '-B"/path/two:/two" -B"/foo:/rfm_workdir" --nv '
+                '-W /rfm_workdir image:tag cmd')
     elif container_variant == 'Singularity+nocommand':
         return ('singularity run -B"/path/one:/one" '
-                '-B"/path/two:/two" -B"/foo:/rfm_workdir" image:tag')
+                '-B"/path/two:/two" -B"/foo:/rfm_workdir" '
+                '-W /rfm_workdir image:tag')
 
 
 @pytest.fixture
@@ -197,12 +199,6 @@ def test_mount_points(container_platform, expected_cmd_mount_points):
     assert cmd == expected_cmd_mount_points
 
 
-def test_missing_image(container_platform):
-    container_platform.image = None
-    with pytest.raises(ContainerError):
-        container_platform.validate()
-
-
 def test_prepare_command(container_platform, expected_cmd_prepare):
     commands = container_platform.emit_prepare_commands('/foo')
     assert commands == expected_cmd_prepare
@@ -211,6 +207,7 @@ def test_prepare_command(container_platform, expected_cmd_prepare):
 def test_run_opts(container_platform, expected_cmd_run_opts):
     container_platform.mount_points = [('/path/one', '/one')]
     container_platform.options = ['--foo', '--bar']
+    container_platform.workdir = None
     assert container_platform.launch_command('/foo') == expected_cmd_run_opts
 
 
@@ -222,7 +219,7 @@ def container_variant_noopt(request):
 
 
 @pytest.fixture
-def container_platform_noopt(container_variant_noopt):
+def container_platform_with_opts(container_variant_noopt):
     ret = containers.__dict__[container_variant_noopt]()
     ret.image = 'image:tag'
     ret.options = ['--foo']
@@ -233,61 +230,59 @@ def container_platform_noopt(container_variant_noopt):
 def expected_run_with_commands(container_variant_noopt):
     if container_variant_noopt == 'Docker':
         return ("docker run --rm -v \"/foo\":\"/rfm_workdir\" "
-                "--foo image:tag bash -c 'cd /rfm_workdir; cmd1; cmd2'")
+                "--foo image:tag bash -c 'cmd1; cmd2'")
     elif container_variant_noopt == 'Sarus':
         return (
             "sarus run "
             "--mount=type=bind,source=\"/foo\",destination=\"/rfm_workdir\" "
-            "--foo image:tag bash -c 'cd /rfm_workdir; cmd1; cmd2'"
+            "--foo image:tag bash -c 'cmd1; cmd2'"
         )
     elif container_variant_noopt == 'Shifter':
         return (
             "shifter run "
             "--mount=type=bind,source=\"/foo\",destination=\"/rfm_workdir\" "
-            "--foo image:tag bash -c 'cd /rfm_workdir; cmd1; cmd2'"
+            "--foo image:tag bash -c 'cmd1; cmd2'"
         )
     elif container_variant_noopt == 'Singularity':
         return ("singularity exec -B\"/foo:/rfm_workdir\" "
-                "--foo image:tag bash -c 'cd /rfm_workdir; cmd1; cmd2'")
+                "--foo image:tag bash -c 'cmd1; cmd2'")
 
 
 @pytest.fixture
 def expected_run_with_workdir(container_variant_noopt):
     if container_variant_noopt == 'Docker':
-        return ("docker run --rm -v \"/foo\":\"/rfm_workdir\" "
-                "--foo image:tag bash -c 'cd foodir; cmd1; cmd2'")
+        return ('docker run --rm -v "/foo":"/rfm_workdir" '
+                '-w foodir --foo image:tag cmd1')
     elif container_variant_noopt == 'Sarus':
         return (
-            "sarus run "
-            "--mount=type=bind,source=\"/foo\",destination=\"/rfm_workdir\" "
-            "--foo image:tag bash -c 'cd foodir; cmd1; cmd2'"
+            'sarus run '
+            '--mount=type=bind,source="/foo",destination="/rfm_workdir" '
+            '-w foodir --foo image:tag cmd1'
         )
     elif container_variant_noopt == 'Shifter':
         return (
-            "shifter run "
-            "--mount=type=bind,source=\"/foo\",destination=\"/rfm_workdir\" "
-            "--foo image:tag bash -c 'cd foodir; cmd1; cmd2'"
+            'shifter run '
+            '--mount=type=bind,source="/foo",destination="/rfm_workdir" '
+            '--foo image:tag cmd1'
         )
     elif container_variant_noopt == 'Singularity':
-        return ("singularity exec -B\"/foo:/rfm_workdir\" --foo image:tag "
-                "bash -c 'cd foodir; cmd1; cmd2'")
+        return ('singularity exec -B\"/foo:/rfm_workdir\" -W foodir '
+                '--foo image:tag cmd1')
 
 
-def test_run_with_commands(container_platform_noopt,
+def test_run_with_commands(container_platform_with_opts,
                            expected_run_with_commands):
+    container_platform_with_opts.workdir = None
     with pytest.warns(warn.ReframeDeprecationWarning):
-        container_platform_noopt.commands = ['cmd1', 'cmd2']
+        container_platform_with_opts.commands = ['cmd1', 'cmd2']
 
-    found_commands = container_platform_noopt.launch_command('/foo')
+    found_commands = container_platform_with_opts.launch_command('/foo')
     assert found_commands == expected_run_with_commands
 
 
-def test_run_with_workdir(container_platform_noopt, expected_run_with_workdir):
-    with pytest.warns(warn.ReframeDeprecationWarning):
-        container_platform_noopt.commands = ['cmd1', 'cmd2']
-
-    with pytest.warns(warn.ReframeDeprecationWarning):
-        container_platform_noopt.workdir = 'foodir'
-
-    found_commands = container_platform_noopt.launch_command('/foo')
+def test_run_with_workdir(container_platform_with_opts,
+                          expected_run_with_workdir):
+    container_platform_with_opts.command = 'cmd1'
+    container_platform_with_opts.workdir = 'foodir'
+    found_commands = container_platform_with_opts.launch_command('/foo')
     assert found_commands == expected_run_with_workdir
diff --git a/unittests/test_pipeline.py b/unittests/test_pipeline.py
index 251fb0da..f3ba74ed 100644
--- a/unittests/test_pipeline.py
+++ b/unittests/test_pipeline.py
@@ -1715,7 +1715,9 @@ def container_test(tmp_path):
 
             @run_after('init')
             def setup_container_platf(self):
-                self.container_platform = platform
+                if platform:
+                    self.container_platform = platform
+
                 self.container_platform.image = image
                 self.container_platform.command = (
                     f"bash -c 'cd {_STAGEDIR_MOUNT}; pwd; ls; "
@@ -1776,17 +1778,9 @@ def test_unknown_container_platform(container_test, local_exec_ctx):
 
 def test_not_configured_container_platform(container_test, local_exec_ctx):
     partition, environ = local_exec_ctx
-    platform = None
-    for cp in ['Docker', 'Singularity', 'Sarus', 'ShifterNG']:
-        if cp not in partition.container_environs.keys():
-            platform = cp
-            break
-
-    if platform is None:
-        pytest.skip('cannot find a supported platform that is not configured')
 
     with pytest.raises(PipelineError):
-        _run(container_test(platform, 'ubuntu:18.04'), *local_exec_ctx)
+        _run(container_test(None, 'ubuntu:18.04'), *local_exec_ctx)
 
 
 def test_skip_if_no_topo(HelloTest, local_exec_ctx):