Skip to content

Conversation

@rsarm
Copy link
Contributor

@rsarm rsarm commented May 23, 2018

Closes #166

@rsarm rsarm added this to the ReFrame sprint 2018w20 milestone May 23, 2018
@rsarm rsarm self-assigned this May 23, 2018
@vkarak vkarak self-requested a review May 28, 2018 09:35
@vkarak vkarak changed the title Version compatibility Support for version compatibility checks May 28, 2018
Copy link
Contributor

@vkarak vkarak left a 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.

self._operations = {
">": lambda x, y: x > y,
">=": lambda x, y: x >= y,
"<": lambda x, y: x < y,
Copy link
Contributor

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.

def validate(self, version_ref):
try:
res = self._validate_interval(version_ref)
except ValueError:
Copy link
Contributor

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.

Copy link
Contributor

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)

return Version(str_version), operator

def _validate_interval(self, version_ref):
min_version_str, max_version_str = self.condition.split(',')
Copy link
Contributor

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.

Copy link
Contributor

@vkarak vkarak May 28, 2018

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(',')

return base + '-dev%s' % self._dev_number


class VersionValidator:
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

except ValueError:
raise ValueError('invalid interval: "%s", '
'expecting "min_version, max_version"'
% self.condition.strip()) from None
Copy link
Contributor

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.

% self.condition.strip()) from None

return ((Version(version_ref) > min_version) and
(Version(version_ref) < max_version))
Copy link
Contributor

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))
Copy link
Contributor

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).

(Version(version_ref) < max_version))

def _validate_relation(self, version_ref):
version, op = self._parse_condition(self.condition)
Copy link
Contributor

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.

@vkarak vkarak changed the title Support for version compatibility checks Support for version compatibility check May 28, 2018
@rsarm
Copy link
Contributor Author

rsarm commented Jun 7, 2018

@jenkins-cscs retry daint dom

min_version_str, max_version_str = condition.split('..')
except ValueError:
raise ValueError("invalid format of version interval: %s"
% condition) from None
Copy link
Contributor

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.

self.max_version = Version(max_version_str)
else:
raise ValueError("missing bound on version interval %s"
% condition)
Copy link
Contributor

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.


def validate(self, version):
return ((Version(version) >= self.min_version) and
(Version(version) <= self.max_version))
Copy link
Contributor

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)

Copy link
Contributor

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?

raise ValueError('invalid condition: %s'
% condition.strip()) from None

self.ref_version = Version(str_version)
Copy link
Contributor

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 = op

Copy link
Contributor

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?

``True`` if a given version string satisfies the relation.
"""
def __init__(self, condition):
self._operations = {
Copy link
Contributor

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

@rsarm
Copy link
Contributor Author

rsarm commented Jun 8, 2018

@jenkins-cscs retry all

@rsarm
Copy link
Contributor Author

rsarm commented Jun 8, 2018

@vkarak I wasn't very sure about which message to print when skipping a test and where to put it.

@codecov-io
Copy link

codecov-io commented Jun 8, 2018

Codecov Report

Merging #287 into master will increase coverage by 0.08%.
The diff coverage is 98.07%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
...sts/resources/checks_unlisted/required_versions.py 100% <100%> (ø)
reframe/core/decorators.py 98.21% <100%> (+0.91%) ⬆️
unittests/test_versioning.py 100% <100%> (ø) ⬆️
reframe/utility/versioning.py 89.02% <95.12%> (+5.69%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fd18a5e...46e86af. Read the comment docs.

import collections
import inspect

from reframe import VERSION
Copy link
Contributor

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.

from reframe import VERSION
from reframe.core.exceptions import ReframeSyntaxError
from reframe.core.pipeline import RegressionTest
from reframe.frontend.printer import PrettyPrinter
Copy link
Contributor

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:

  1. You break a key design concept of the whole framework. The core should never use the frontend.
  2. The PrettyPrinter is 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".
  3. The core uses the low-level logger to log stuff.

if not any(c.validate(VERSION) for c in conditions):
printer.status('SKIP',
'skipping uncompatible class %s' % cls.__name__,
just='center')
Copy link
Contributor

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__)

'skipping uncompatible class %s' % cls.__name__,
just='center')
try:
mod.__rfm_skip_tests |= {cls}
Copy link
Contributor

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.



def required_version(*compat_versions):
printer = PrettyPrinter()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statement before the docstring??


def required_version(*compat_versions):
printer = PrettyPrinter()
"""Class decorator for skipping version-uncompatible tests."""
Copy link
Contributor

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.

try:
mod.__rfm_skip_tests |= {cls}
except AttributeError:
mod.__rfm_skip_tests = {cls}
Copy link
Contributor

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)

...


self._ref_version = Version(cond_match.group(2))
op = cond_match.group(1)
if op == '':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not op:


def validate(self, version):
return self._op_actions[self._operator](Version(version),
self._ref_version)
Copy link
Contributor

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')
Copy link
Contributor

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()
Copy link
Contributor

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()
Copy link
Contributor

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
Copy link
Contributor

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.



def required_version(*versions):
"""Class decorator for skipping version-uncompatible tests.
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this blank line.

The decorated class must derive from
:class:`reframe.core.pipeline.RegressionTest`. This decorator is also
available directly under the :mod:`reframe` module.
Copy link
Contributor

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...

: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.
Copy link
Contributor

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.

@vkarak vkarak self-assigned this Jun 19, 2018
Vasileios Karakasis added 2 commits June 19, 2018 14:41
Also:
- Complete documentation of the `require_version` decorator.
- More robust algorithm for identifying problematic user code.
@vkarak
Copy link
Contributor

vkarak commented Jun 19, 2018

@jenkins-cscs retry daint

1 similar comment
@vkarak
Copy link
Contributor

vkarak commented Jun 19, 2018

@jenkins-cscs retry daint

@vkarak vkarak merged commit 527907f into master Jun 19, 2018
@vkarak vkarak deleted the enhancement/version-compatibility branch June 19, 2018 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants