-
Notifications
You must be signed in to change notification settings - Fork 117
[feat] Add support for class directives #1868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1868 +/- ##
==========================================
+ Coverage 87.61% 87.68% +0.06%
==========================================
Files 49 50 +1
Lines 8141 8207 +66
==========================================
+ Hits 7133 7196 +63
- Misses 1008 1011 +3
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've got a different suggestion for the syntax. What do you think of something like
class Foo(rfm.RegressionTest):
t0 = dependency(MyOtherTest, by=...)where t0 is the instance of a class that manages the dependencies. For example, the test Foo from above could have a hook that requires the dependency and one would just do
@rfm.run_before('compile')
def a_random_hook(self):
my_dependency = self.t0.get()
# Do some stuff with the dependency
...This would be more consistent with what we have right now for the other built-ins and it would avoid the name mangling in the pipeline (which I would try to avoid at all costs). I do get that having that general method to export any member function as a directive is far more flexible, but I feel like it's a bit of an overkill for just one directive.
Currently, it's just for one.. The plan is to use this mechanism for the skip function functionality, with |
|
Regarding the syntax for dependencies, that would require a complete redesign of the dependencies. I see your point, but I'm not sure it's worth the effort. The fact also that you name the dependencies, I find it a bit confusing as a user. It fits better for an internal implementation rather than a user API, imho. I prefer the |
The name mangling does not interfere with the pipeline. We're not mangling any pipeline method. |
Perhaps that's something for the What about inheritance? As far as I understand from this, a test dependency would not be inherited along by any child class. Is this right? I feel like that we're blurring the line between the test class and the test instance here. My view is that things which get declared at the class level MUST be inherited by any child class; whereas things that get specified at the instance level are just "private" to that instance (which is what happens for both variables and parameters). This is why I was thinking of different functions for specifying the dependencies at the class level and the instance level. The way I see this, a class may define dependencies for its tests, but the class itself does not depend on these dependencies; the tests themselves do. This is why I suggested the name
I meant having |
|
Dependencies are inherited properly. Check this out: import reframe as rfm
import reframe.utility.sanity as sn
class Base(rfm.RunOnlyRegressionTest):
valid_systems = ['*']
valid_prog_environs = ['*']
executable = 'echo'
@rfm.run_before('run')
def set_exec_opts(self):
self.executable_opts = [self.name]
@rfm.run_before('sanity')
def set_sanity_check(self):
self.sanity_patterns = sn.assert_found(rf'{self.name}', self.stdout)
@rfm.simple_test
class T0(Base):
pass
class TX(Base):
depends_on('T0')
@rfm.simple_test
class T1(TX):
pass
@rfm.simple_test
class T2(TX):
passThis will make T1 and T2 depend on T0: But you spotted a very subtle detail! This works nicely because none of T1, T2 defines @rfm.simple_test
class T1(TX):
def __init__(self):
passThen T1 is independent. I would have to call This PR has a meaning if we want to eliminate the use of |
|
Instead of augmenting the |
|
Indeed, augmenting |
This avoids side effects when users override `__init__()` without calling `super()`. Also renamed `_rfm_init()` to `__rfm_init__()` and added a couple more test cases in the unit tests.
I don't see this as a problem, users will not ever call the esoteric function. Also, after the initialization |
- Also fix post-merge problems.
victorusu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! Can we merge it? Or is it for 4.0?
jjotero
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation is super clean!
However, I have the feeling that having directives is no-longer required now that the post-init hook is in. I struggle to see why any of the three directives would be used in the class body and not as a post-init hook. In fact, using these methods as directives only gives you an incomplete functionality respect to using them in a post-init hook.
I think the following example shows why these methods belong to the instance and not the class:
class Test(rfm.RunOnlyRegressionTest):
'''A base test'''
valid_systems = ['daint:login']
valid_prog_environs = ['PrgEnv-gnu']
executable = f'echo bananas'
@rfm.run_before('sanity')
def set_sanity(self):
self.sanity_patterns = sn.assert_found('bananas', self.stdout)
@rfm.simple_test
class A(Test):
p = parameter(range(10))
# Why would anyone skip a test in the class body?
skip('If I do not want this test I might as well not register it.')
# Skipping a test based on parameters, prgenv, or system will never work
# in the class body.
# For example, the following will break, because the value of the
# parameter `p` belongs to the instance, not the class.
# skip_if(p > 5, 'skipping test A from the class body') # Error: Can't access a parameter from the class body.
@rfm.run_after('init')
def skip_tests(self):
'''This can only be done in a post-init hook.
I'm using parameters here, but the same applies to prog_environs and systems.
'''
self.skip_if(self.p > 5, 'skipping A from a post-init hook')
@rfm.run_after('init')
def serialize_parameterized_tests(self):
'''This can only be done in a post-init hook
I'm using parameters here, but the same applies to prog_environs and systems.
'''
for params in self._rfm_param_space:
testname = 'A'
for val in params:
testname += f'_{val}'
if testname == self.name:
break
self.depends_on(testname)As a user, I would certainly be confused that I am given the options to use these methods in different places, but their behaviour is not the same in both places.
|
|
||
| Directives are special functions that are called at the class level but will be applied to the newly created test. | ||
| Directives can also be invoked as normal test methods once the test has been created. | ||
| Using directives and `builtins <#builtins>`__ together, it is possible to completely get rid of the :func:`__init__` method in the tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already true with the post-init hook, right? I think we should just avoid mentioning
__init__ in the new docs and just refer to the recommended syntax.
|
@jjotero For parametrised tests, it is true that you have to do it with hooks, but why should we restrict the |
Because that locks a class to a specific dependency and it will prevent you from deriving and extending the dependency if you want to. For example: @rfm.simple_test
class A(rfm.RegressionTest):
...
@rfm.simple_test
class B(rfm.RegressionTest):
depends_on('A')
...All good so far, but say that someone imports the above into another file and does import the.other.module as m
@rfm.simple_test
class A1(m.A):
'''Extend the functionality of A'''
@rfm.simple_test
class B1(m.B):
'''This class now depends on both A and A1, which doesn't make much sense.'''
depends_on('A1')Here if you want to run only With fixtures in mind, fixtures MUST be defined in the class body because a fixture is a building block for the test itself (the test requires of the fixture for it to be complete). But this is not the case with dependencies, where a test may or may not need the dependency to run. So in short, I think that if it belongs in the class body, it's because it is a fixture, else, it's a dependency and should be attached as a hook.
Sanity patterns are different because they do not depend on anything external to the class where they're defined. It doesn't matter what you do with it, a derived class can always override it.
The same applies to every other test variable that is used at some point along the pipeline, right? I think this is more than fine as long as we state clearly that dependencies and test skipping conditions must not be defined beyond the post-init stage. |
|
Ok, it agains boils down to the dependencies vs. fixtures thing. I think you are probably right. Dependencies should be something more dynamic, although this is not the actual case, since they are used as (and are shown to be used as) fixtures. I am a bit disappointed to have to close this PR, because I feel like throwing quite some effort out of the window... Anyway, I will keep my branch for future reference in case we need to implement directives. |
A directive makes available a method defined in a class to the execution of the class body during its creation.
A directive simply captures the arguments passed to it and all directives are stored in a registry inside the class. When the final object is created, they will be applied to that instance by calling the target method on the freshly created object. As soon as they are applied, they set to point to the actual object method, since they have served their purpose. This allows us to export functions such as
depends_on()at the class level. This is implemented by aliasing the directive name inRegressionTest.__getattribute__()to the actual object method that implements it.Currently, onlydepends_on()is exposed as a directive, but in the future the skip test functions would also be.This builds on top of #1865.The following methods are exposed as directives:
depends_on(),skip()andskip_if().Implementation details
The directive methods are defined in
RegressionTestand are prefixed with_D_.For consistency, I changed the prefix of pipeline hook methods to
_P_.Todos
Fixes #1864.