-
Notifications
You must be signed in to change notification settings - Fork 117
Support for version compatibility check #287
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
vkarak
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 details of the implementation look ok. Some more robust error checking is needed. I also propose an alternative design using a factory for nicely integrating the two different types of validation.
Since now the new style checks are merged in, we should expand this PR to totally implement the feature.
reframe/utility/versioning.py
Outdated
| self._operations = { | ||
| ">": lambda x, y: x > y, | ||
| ">=": lambda x, y: x >= y, | ||
| "<": lambda x, y: x < y, |
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.
Please align all lambdas here.
reframe/utility/versioning.py
Outdated
| def validate(self, version_ref): | ||
| try: | ||
| res = self._validate_interval(version_ref) | ||
| except ValueError: |
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 hides possible programming errors.
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 try/except here or an if/else is a bit complicated here. We could go for an alternative, more OOP, design using a factory:
class _ValidatorImpl:
@abc.abstractmethod
def validate(version):
pass
class _IntervalValidator(_ValidatorImpl):
def __init__(self, condition):
# parse and validate the interval condition here
def validate(version):
# validate version against interval
class _RelationalValidator(_ValidatorImpl):
def __init__(self, condition):
# parse and validate the relational condition here
def validate(version):
# validate version against the condition
class VersionValidator:
def __new__(cls, condition):
if '..' in condition:
return _IntervalValidator(condition)
else:
return _RelationalValidator(condition)
reframe/utility/versioning.py
Outdated
| return Version(str_version), operator | ||
|
|
||
| def _validate_interval(self, version_ref): | ||
| min_version_str, max_version_str = self.condition.split(',') |
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 prefer using .. for specifying intervals. I was confused when I've seen it in the unit 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 statement might also through a ValueError for strings such as 1,2,3,4. You should write it as follows:
min_version_str, max_version_str, *_ = self.condition.split(',')
reframe/utility/versioning.py
Outdated
| return base + '-dev%s' % self._dev_number | ||
|
|
||
|
|
||
| class VersionValidator: |
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.
Please add docstring describing the purpose of this class.
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.
Similarly, for the Version class.
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.
Using my proposed design, the docstring for this class should separated in the two implementations.
reframe/utility/versioning.py
Outdated
| except ValueError: | ||
| raise ValueError('invalid interval: "%s", ' | ||
| 'expecting "min_version, max_version"' | ||
| % self.condition.strip()) from None |
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 reraise here is not correct, because if ValueError is thrown, it means that the version strings are wrong, sot the message is deceiving. You should not catch it at all.
reframe/utility/versioning.py
Outdated
| % self.condition.strip()) from None | ||
|
|
||
| return ((Version(version_ref) > min_version) and | ||
| (Version(version_ref) < max_version)) |
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.
It is more intuitive for the intervals to be inclusive. When I saw the corresponding unit tests, I was expecting it to validate.
| self.assertTrue(any(c.validate('2.2') for c in conditions)) | ||
| self.assertFalse(any(c.validate('2.5') for c in conditions)) | ||
| self.assertTrue(any(c.validate('3.0') for c in conditions)) | ||
| self.assertFalse(any(c.validate('3.1') for c in conditions)) |
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.
There are no tests about things going wrong (invalid versions, invalid operators, invalid intervals).
reframe/utility/versioning.py
Outdated
| (Version(version_ref) < max_version)) | ||
|
|
||
| def _validate_relation(self, version_ref): | ||
| version, op = self._parse_condition(self.condition) |
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 too late to parse and validate the condition. It should fail early in the constructor.
|
@jenkins-cscs retry daint dom |
reframe/utility/versioning.py
Outdated
| min_version_str, max_version_str = condition.split('..') | ||
| except ValueError: | ||
| raise ValueError("invalid format of version interval: %s" | ||
| % condition) from None |
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.
We split after the binary operators.
reframe/utility/versioning.py
Outdated
| self.max_version = Version(max_version_str) | ||
| else: | ||
| raise ValueError("missing bound on version interval %s" | ||
| % condition) |
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.
We split after the binary operators.
reframe/utility/versioning.py
Outdated
|
|
||
| def validate(self, version): | ||
| return ((Version(version) >= self.min_version) and | ||
| (Version(version) <= self.max_version)) |
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.
Convert the version string once:
version = Version(version)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.
Another question: Is there a reason that min_version and max_version are public?
reframe/utility/versioning.py
Outdated
| raise ValueError('invalid condition: %s' | ||
| % condition.strip()) from None | ||
|
|
||
| self.ref_version = Version(str_version) |
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 think you can write this in a simpler way using regexes (not like the first version of this PR, though):
cond_match = re.match(r'(\W{1,2})(\S+)', condition)
if not cond_match:
raise ValueError('invalid condition: %s' % condition)
self.ref_version = Version(cond_match.group(2))
# First check, then assign. Otherwise, the self.operator will have an incorrect value.
op = cond_match.group(1)
if op not in self._op_actions.keys():
self.operator = opThere 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.
Another question: Is there a reason that operator and ref_version are public?
reframe/utility/versioning.py
Outdated
| ``True`` if a given version string satisfies the relation. | ||
| """ | ||
| def __init__(self, condition): | ||
| self._operations = { |
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.
Better name this as self._op_actions
|
@jenkins-cscs retry all |
|
@vkarak I wasn't very sure about which message to print when skipping a test and where to put it. |
Codecov Report
@@ Coverage Diff @@
## master #287 +/- ##
==========================================
+ Coverage 91.18% 91.27% +0.08%
==========================================
Files 67 68 +1
Lines 7941 8042 +101
==========================================
+ Hits 7241 7340 +99
- Misses 700 702 +2
Continue to review full report at Codecov.
|
reframe/core/decorators.py
Outdated
| import collections | ||
| import inspect | ||
|
|
||
| from reframe import VERSION |
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.
Use import reframe here and reference version as reframe.VERSION.
reframe/core/decorators.py
Outdated
| from reframe import VERSION | ||
| from reframe.core.exceptions import ReframeSyntaxError | ||
| from reframe.core.pipeline import RegressionTest | ||
| from reframe.frontend.printer import PrettyPrinter |
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.
A couple of things here:
- You break a key design concept of the whole framework. The core should never use the frontend.
- The
PrettyPrinteris meant to be used by the frontend and the execution policies for nicely formatting the output. Internally, it uses a logger to do the actual "printing". - The core uses the low-level logger to log stuff.
reframe/core/decorators.py
Outdated
| if not any(c.validate(VERSION) for c in conditions): | ||
| printer.status('SKIP', | ||
| 'skipping uncompatible class %s' % cls.__name__, | ||
| just='center') |
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.
You should not be using the pretty printer here. Use instead:
from reframe.core.logging import getlogger
getlogger().info('skipping incompatible test defined in class: %s' % cls.__name__)
reframe/core/decorators.py
Outdated
| 'skipping uncompatible class %s' % cls.__name__, | ||
| just='center') | ||
| try: | ||
| mod.__rfm_skip_tests |= {cls} |
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'd use add() here.
reframe/core/decorators.py
Outdated
|
|
||
|
|
||
| def required_version(*compat_versions): | ||
| printer = PrettyPrinter() |
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.
Statement before the docstring??
reframe/core/decorators.py
Outdated
|
|
||
| def required_version(*compat_versions): | ||
| printer = PrettyPrinter() | ||
| """Class decorator for skipping version-uncompatible 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 should be documented properly. It's a public function.
reframe/core/decorators.py
Outdated
| try: | ||
| mod.__rfm_skip_tests |= {cls} | ||
| except AttributeError: | ||
| mod.__rfm_skip_tests = {cls} |
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'd move this try/except outside this if:
mod = inspect.getmodule(cls)
if not hasattr(mod, '__rfm_skip_tests'):
mod.__rfm_skip_tests = set()
if not any(c.validate(reframe.VERSION) for c in conditions):
mod.__rfm_skip_tests.add(cls)
...
reframe/utility/versioning.py
Outdated
|
|
||
| self._ref_version = Version(cond_match.group(2)) | ||
| op = cond_match.group(1) | ||
| if op == '': |
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.
if not op:
reframe/utility/versioning.py
Outdated
|
|
||
| def validate(self, version): | ||
| return self._op_actions[self._operator](Version(version), | ||
| self._ref_version) |
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.
Better write this as follows:
do_validate = self._op_actions[self._operator]
return do_validate(Version(version), self._ref_version)| self.assertRaises(ValueError, VersionValidator, '2.0.0>') | ||
| self.assertRaises(ValueError, VersionValidator, '2.0.0>1.0.0') | ||
| self.assertRaises(ValueError, VersionValidator, '=>') | ||
| self.assertRaises(ValueError, VersionValidator, '>1') |
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.
We will need more unit tests for the new decorator.
| pass | ||
|
|
||
|
|
||
| @rfm.required_version() |
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 don't think we should allow this.
| import reframe.utility.sanity as sn | ||
|
|
||
|
|
||
| @rfm.required_version() |
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.
Remove this from here.
| @rfm.simple_test | ||
| @rfm.required_version('!=2.0') | ||
| class Test5Check(rfm.RegressionTest): | ||
| pass |
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 don't think we need this file at all. We already have the unittests/resources/checks_unlisted/good.py that is being loaded by the check loader. We could simply decorate one of the tests in there with a required_version that is always true and add another test, which should be skipped.
reframe/core/decorators.py
Outdated
|
|
||
|
|
||
| def required_version(*versions): | ||
| """Class decorator for skipping version-uncompatible 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.
Class decorator for specifying the required ReFrame version for the following test.
| :arg versions: The versions that are compatible with the test. | ||
| .. versionadded:: 2.13 | ||
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.
Remove this blank line.
reframe/core/decorators.py
Outdated
| The decorated class must derive from | ||
| :class:`reframe.core.pipeline.RegressionTest`. This decorator is also | ||
| available directly under the :mod:`reframe` module. |
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 don't think you test for that! And I don't think you should in any case...
reframe/core/decorators.py
Outdated
| :class:`reframe.core.pipeline.RegressionTest`. This decorator is also | ||
| available directly under the :mod:`reframe` module. | ||
| :arg versions: The versions that are compatible with the test. |
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.
Please explain a bit the syntax here. Also you should specify the type of the argument. See other part of the documentation on how this is done.
Also: - Complete documentation of the `require_version` decorator. - More robust algorithm for identifying problematic user code.
|
@jenkins-cscs retry daint |
1 similar comment
|
@jenkins-cscs retry daint |
Closes #166