Repository: reframe-hpc/reframe

Issue:
ReFrame detects the test has undefined parameters, but doesn't say which ones
I sometimes get 

```
WARNING: skipping test 'X': test has one or more undefined parameters
```

And recently got the same question from a user who was trying to run one of my tests. I found https://github.com/reframe-hpc/reframe/issues/2240 which is related, and read the answer, but... I don't fully understand the issue here. Wouldn't a piece of code like:

```
if (cls.is_abstract()):
    params = cls.param_space.params
    undefined_params = []
    for param in params:
        if not cls.param_space.defines(param):
            undefined_params.append(param)
    # Raise more detailed warning
    getlogger().warning(
        f'skipping test {cls.__qualname__!r}: '
        f'test has one or more undefined parameters:'
        ", ".join(undefined_params)
    )
```

Replacing [this segment](https://github.com/reframe-hpc/reframe/blob/1bef008fb2f561795aa2b64f56074f0baa80bda3/reframe/core/decorators.py#L173) of code do the trick?

I've seen the argumentation in #2240 that tracing it back to the line might be difficult, and that's fine by me. But naming _which_ parameter is undefined can already be a big help. Sure, users would still have to see where that parameter is defined, and why it is possible undefined, but they'd have a clear starting point for the search :)

Disclaimer: I haven't _actually_ tested the code above, but it seems like this is how it _should_ work (famous last words :))

Discussion:

Patch:
diff --git a/reframe/core/decorators.py b/reframe/core/decorators.py
index b8fbc7e2..ff62aa5e 100644
--- a/reframe/core/decorators.py
+++ b/reframe/core/decorators.py
@@ -9,7 +9,6 @@
 
 __all__ = ['simple_test']
 
-
 import inspect
 import sys
 import traceback
@@ -171,10 +170,10 @@ def _validate_test(cls):
 
     if (cls.is_abstract()):
         getlogger().warning(
-            f'skipping test {cls.__qualname__!r}: '
-            f'test has one or more undefined parameters'
+            f'skipping test {cls.__qualname__!r}: ' +
+            'the following parameters are undefined: ' +
+            ', '.join(cls.param_space.undefined_params())
         )
-        return False
 
     conditions = [VersionValidator(v) for v in cls._rfm_required_version]
     if (cls._rfm_required_version and
diff --git a/reframe/core/fixtures.py b/reframe/core/fixtures.py
index e2854679..3e790d3f 100644
--- a/reframe/core/fixtures.py
+++ b/reframe/core/fixtures.py
@@ -325,12 +325,6 @@ class FixtureRegistry:
         '''Get the uninstantiated tests of this registry'''
         return self._registry.keys()
 
-    def _filter_valid_partitions(self, candidate_parts):
-        return [p for p in candidate_parts if p in self._env_by_part]
-
-    def _filter_valid_environs(self, part, candidate_environs):
-        return [e for e in cadidate_environs if e in self._env_by_part[part]]
-
     def _is_registry(self, other):
         if not isinstance(other, FixtureRegistry):
             raise TypeError('other is not a FixtureRegistry')
@@ -776,7 +770,8 @@ class TestFixture:
         # Check that the fixture class is not an abstract test.
         if cls.is_abstract():
             raise ValueError(
-                f'class {cls.__qualname__!r} has undefined parameters'
+                f'fixture {cls.__qualname__!r} has undefined parameters: ' +
+                ', '.join(cls.param_space.undefined_params())
             )
 
         # Validate the scope
diff --git a/reframe/core/parameters.py b/reframe/core/parameters.py
index 16335419..f829b20e 100644
--- a/reframe/core/parameters.py
+++ b/reframe/core/parameters.py
@@ -198,7 +198,7 @@ class TestParam:
                 self.values = tuple(filt_vals) + self.values
             except TypeError:
                 raise ReframeSyntaxError(
-                    f"'filter_param' must return an iterable"
+                    "'filter_param' must return an iterable"
                 ) from None
 
     def is_abstract(self):
@@ -307,7 +307,7 @@ class ParamSpace(namespaces.Namespace):
             try:
                 # Get the parameter values for the specified variant
                 param_values = self.__param_combinations[params_index]
-            except IndexError as no_params:
+            except IndexError:
                 raise RuntimeError(
                     f'parameter space index out of range for '
                     f'{obj.__class__.__qualname__}'
@@ -333,6 +333,11 @@ class ParamSpace(namespaces.Namespace):
         '''
         return name in self.params and not self.params[name].is_abstract()
 
+    def undefined_params(self):
+        '''Return a list of all undefined parameters.'''
+        return [name for name, param in self.params.items()
+                if param.is_abstract()]
+
     def __iter__(self):
         '''Create a generator object to iterate over the parameter space
 


Test patch:
diff --git a/unittests/test_parameters.py b/unittests/test_parameters.py
index 118bba99..a818a3d8 100644
--- a/unittests/test_parameters.py
+++ b/unittests/test_parameters.py
@@ -10,6 +10,7 @@ import inspect
 
 import reframe as rfm
 from reframe.core.exceptions import ReframeSyntaxError
+import reframe.core.decorators as decorators
 
 
 class NoParams(rfm.RunOnlyRegressionTest):
@@ -48,10 +49,13 @@ def test_params_are_present():
 
 def test_abstract_param():
     class MyTest(Abstract):
-        pass
+        # Add another abstract parameter
+        P2 = parameter()
 
     assert MyTest.param_space['P0'] == ()
     assert MyTest.param_space['P1'] == ('b',)
+    assert MyTest.param_space['P2'] == ()
+    assert MyTest.param_space.undefined_params() == ['P0', 'P2']
 
 
 def test_param_override():