Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions reframe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import os
import sys


VERSION = '3.5-dev1'
VERSION = '3.5.0-dev.1'
INSTALL_PREFIX = os.path.normpath(
os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
)
Expand Down
22 changes: 15 additions & 7 deletions reframe/core/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import sys
import traceback

import reframe
import reframe.utility.osext as osext
from reframe.core.exceptions import ReframeSyntaxError, user_frame
from reframe.core.logging import getlogger
from reframe.core.pipeline import RegressionTest
Expand Down Expand Up @@ -139,24 +139,32 @@ def required_version(*versions):
If the test is not compatible with the current ReFrame version it will be
skipped.

.. versionadded:: 2.13

:arg versions: A list of ReFrame version specifications that this test is
allowed to run. A version specification string can have one of the
following formats:

1. ``VERSION``: Specifies a single version.
2. ``{OP}VERSION``, where ``{OP}`` can be any of ``>``, ``>=``, ``<``,
``<=``, ``==`` and ``!=``. For example, the version specification
string ``'>=2.15'`` will allow the following test to be loaded only
by ReFrame 2.15 and higher. The ``==VERSION`` specification is the
string ``'>=3.5.0'`` will allow the following test to be loaded only
by ReFrame 3.5.0 and higher. The ``==VERSION`` specification is the
equivalent of ``VERSION``.
3. ``V1..V2``: Specifies a range of versions.

You can specify multiple versions with this decorator, such as
``@required_version('2.13', '>=2.16')``, in which case the test will be
``@required_version('3.5.1', '>=3.5.6')``, in which case the test will be
selected if *any* of the versions is satisfied, even if the versions
specifications are conflicting.

.. versionadded:: 2.13

.. versionchanged:: 3.5.0

Passing ReFrame version numbers that do not comply with the `semantic
versioning <https://semver.org/>`__ specification is deprecated.
Examples of non-compliant version numbers are ``3.5`` and ``3.5-dev0``.
These should be written as ``3.5.0`` and ``3.5.0-dev.0``.

'''
if not versions:
raise ValueError('no versions specified')
Expand All @@ -168,7 +176,7 @@ def _skip_tests(cls):
if not hasattr(mod, '__rfm_skip_tests'):
mod.__rfm_skip_tests = set()

if not any(c.validate(reframe.VERSION) for c in conditions):
if not any(c.validate(osext.reframe_version()) for c in conditions):
getlogger().info('skipping incompatible test defined'
' in class: %s' % cls.__name__)
mod.__rfm_skip_tests.add(cls)
Expand Down
19 changes: 17 additions & 2 deletions reframe/core/warnings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import contextlib
import inspect
import semver
import warnings

import reframe
from reframe.core.exceptions import ReframeFatalError


Expand Down Expand Up @@ -44,12 +46,21 @@ def _format_warning(message, category, filename, lineno, line=None):
warnings.formatwarning = _format_warning


def user_deprecation_warning(message):
_RAISE_DEPRECATION_ALWAYS = False


def user_deprecation_warning(message, from_version='0.0.0'):
'''Raise a deprecation warning at the user stack frame that eventually
calls this function.

As "user stack frame" is considered a stack frame that is outside the
:py:mod:`reframe` base module.

:arg message: the message of the warning
:arg from_version: raise the warning only for ReFrame versions greater than
this one. This is useful if you want to "schedule" a deprecation
warning for the future

'''

# Unroll the stack and issue the warning from the first stack frame that is
Expand All @@ -62,4 +73,8 @@ def user_deprecation_warning(message):

stack_level += 1

warnings.warn(message, ReframeDeprecationWarning, stacklevel=stack_level)
min_version = semver.VersionInfo.parse(from_version)
version = semver.VersionInfo.parse(reframe.VERSION)
if _RAISE_DEPRECATION_ALWAYS or version >= min_version:
warnings.warn(message, ReframeDeprecationWarning,
stacklevel=stack_level)
10 changes: 6 additions & 4 deletions reframe/frontend/runreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import reframe as rfm
import reframe.core.exceptions as errors
import reframe.utility.jsonext as jsonext
from reframe.utility.versioning import Version
import reframe.utility.versioning as versioning


DATA_VERSION = '1.3'
DATA_VERSION = '1.3.0'
_SCHEMA = os.path.join(rfm.INSTALL_PREFIX, 'reframe/schemas/runreport.json')


Expand Down Expand Up @@ -141,8 +141,10 @@ def load_report(filename):
raise errors.ReframeError(f'invalid report {filename!r}') from e

# Check if the report data is compatible
found_ver = Version(report['session_info']['data_version'])
required_ver = Version(DATA_VERSION)
found_ver = versioning.parse(
report['session_info']['data_version']
)
required_ver = versioning.parse(DATA_VERSION)
if found_ver.major != required_ver.major or found_ver < required_ver:
raise errors.ReframeError(
f'incompatible report data versions: '
Expand Down
14 changes: 8 additions & 6 deletions reframe/utility/osext.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import grp
import os
import re
import semver
import shlex
import shutil
import signal
Expand Down Expand Up @@ -481,15 +482,16 @@ def git_repo_hash(commit='HEAD', short=True, wd=None):
def reframe_version():
'''Return ReFrame version.

If ReFrame's installation contains the repository metadata, the
repository's hash will be appended to the actual version.
If ReFrame's installation contains the repository metadata and the current
version is a pre-release version, the repository's hash will be appended
to the actual version.

'''
version = reframe.VERSION
repo_hash = git_repo_hash()
if repo_hash:
return '%s (rev: %s)' % (version, repo_hash)
if repo_hash and semver.VersionInfo.parse(reframe.VERSION).prerelease:
return f'{reframe.VERSION}+{repo_hash}'
else:
return version
return reframe.VERSION


def expandvars(s):
Expand Down
117 changes: 38 additions & 79 deletions reframe/utility/versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,86 +4,45 @@
# SPDX-License-Identifier: BSD-3-Clause

import abc
import functools
import sys
import re
import semver

from reframe.core.warnings import user_deprecation_warning

@functools.total_ordering
class Version:
def __init__(self, version):
if version is None:
raise ValueError('version string may not be None')

base_part, *dev_part = version.split('-dev')
def parse(version_str):
'''Compatibility function to normalize version strings from prior
ReFrame versions

try:
major, minor, *patch_part = base_part.split('.')
except ValueError:
raise ValueError('invalid version string: %s' % version) from None

patch_level = patch_part[0] if patch_part else 0

try:
self._major = int(major)
self._minor = int(minor)
self._patch_level = int(patch_level)
self._dev_number = int(dev_part[0]) if dev_part else None
except ValueError:
raise ValueError('invalid version string: %s' % version) from None

@property
def major(self):
return self._major

@property
def minor(self):
return self._minor

@property
def patch_level(self):
return self._patch_level

@property
def dev_number(self):
return self._dev_number

def _value(self):
return 10000*self._major + 100*self._minor + self._patch_level

def __eq__(self, other):
if not isinstance(other, type(self)):
return NotImplemented

return (self._value() == other._value() and
self._dev_number == other._dev_number)

def __gt__(self, other):
if not isinstance(other, type(self)):
return NotImplemented

if self._value() > other._value():
return self._value() > other._value()

if self._value() < other._value():
return self._value() > other._value()

self_dev_number = (self._dev_number if self._dev_number is not None
else sys.maxsize)
other_dev_number = (other._dev_number if other._dev_number is not None
else sys.maxsize)

return self_dev_number > other_dev_number

def __repr__(self):
return "Version('%s')" % self

def __str__(self):
base = '%s.%s.%s' % (self._major, self._minor, self._patch_level)
if self._dev_number is None:
return base
:returns: a :class:`semver.VersionInfo` object.
'''

return base + '-dev%s' % self._dev_number
compat = False
old_style_stable = re.search(r'^(\d+)\.(\d+)$', version_str)
old_style_dev = re.search(r'(\d+)\.(\d+)((\d+))?-dev(\d+)$', version_str)
if old_style_stable:
compat = True
major = old_style_stable.group(1)
minor = old_style_stable.group(2)
ret = semver.VersionInfo(major, minor, 0)
elif old_style_dev:
compat = True
major = old_style_dev.group(1)
minor = old_style_dev.group(2)
patchlevel = old_style_dev.group(4) or 0
prerelease = old_style_dev.group(5)
ret = semver.VersionInfo(major, minor, patchlevel, f'dev.{prerelease}')
else:
ret = semver.VersionInfo.parse(version_str)

if compat:
user_deprecation_warning(
f"the version string {version_str!r} is deprecated; "
f"please use the conformant '{ret}'",
from_version='3.5.0'
)

return ret


class _ValidatorImpl(abc.ABC):
Expand All @@ -109,14 +68,14 @@ def __init__(self, condition):
condition) from None

if min_version_str and max_version_str:
self._min_version = Version(min_version_str)
self._max_version = Version(max_version_str)
self._min_version = parse(min_version_str)
self._max_version = parse(max_version_str)
else:
raise ValueError("missing bound on version interval %s" %
condition)

def validate(self, version):
version = Version(version)
version = parse(version)
return ((version >= self._min_version) and
(version <= self._max_version))

Expand All @@ -142,7 +101,7 @@ def __init__(self, condition):
if not cond_match:
raise ValueError("invalid condition: '%s'" % condition)

self._ref_version = Version(cond_match.group(2))
self._ref_version = parse(cond_match.group(2))
op = cond_match.group(1)
if not op:
op = '=='
Expand All @@ -154,7 +113,7 @@ def __init__(self, condition):

def validate(self, version):
do_validate = self._op_actions[self._operator]
return do_validate(Version(version), self._ref_version)
return do_validate(parse(version), self._ref_version)


class VersionValidator:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pytest-forked==1.3.0
pytest-parallel==0.1.0
PyYAML==5.3.1
requests==2.25.1
semver==2.13.0
setuptools==50.3.0
wcwidth==0.2.5
#+pygelf%pygelf==0.3.6
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
),
package_data={'reframe': ['schemas/*']},
include_package_data=True,
install_requires=['argcomplete', 'jsonschema', 'PyYAML'],
install_requires=['argcomplete', 'jsonschema', 'PyYAML', 'semver'],
python_requires='>=3.6',
scripts=['bin/reframe'],
classifiers=(
Expand Down
Loading