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
10 changes: 9 additions & 1 deletion docs/manpage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Test discovery and test loading
This is the very first phase of the frontend.
ReFrame will search for tests in its *check search path* and will load them.
When ReFrame loads a test, it actually *instantiates* it, meaning that it will call its :func:`__init__` method unconditionally whether this test is meant to run on the selected system or not.
This is something that writers of regression tests should bear in mind.
This is something that test developers should bear in mind.

.. option:: -c, --checkpath=PATH

Expand All @@ -45,6 +45,14 @@ This is something that writers of regression tests should bear in mind.

This option can also be set using the :envvar:`RFM_CHECK_SEARCH_RECURSIVE` environment variable or the :attr:`~config.general.check_search_recursive` general configuration parameter.

.. note::
ReFrame will fail to load a test with a relative import unless *any* of the following holds true:

1. The test is located under ReFrame's installation prefix.
2. The parent directory of the test contains an ``__init__.py`` file.

For versions prior to 4.6, relative imports are supported only for case (1).


.. _test-filtering:

Expand Down
9 changes: 8 additions & 1 deletion reframe/frontend/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,15 @@ def load_from_file(self, filename, force=False):
dirname = os.path.dirname(filename)
with osext.change_dir(dirname):
with util.temp_sys_path(dirname):
if os.path.exists(os.path.join(dirname, '__init__.py')):
# If the containing directory is a package,
# import it, too.
parent = util.import_module_from_file(dirname).__name__
else:
parent = None

return self.load_from_module(
util.import_module_from_file(filename, force)
util.import_module_from_file(filename, force, parent)
)
except Exception:
exc_info = sys.exc_info()
Expand Down
12 changes: 11 additions & 1 deletion reframe/utility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _do_import_module_from_file(filename, module_name=None):
return module


def import_module_from_file(filename, force=False):
def import_module_from_file(filename, force=False, parent=None):
'''Import module from file.

If the file location refers to a directory, the contained ``__init__.py``
Expand All @@ -85,7 +85,14 @@ def import_module_from_file(filename, force=False):

:arg filename: The path to the filename of a Python module.
:arg force: Force reload of module in case it is already loaded.
:arg parent: The name of the parent module of the one that will be loaded.
This will essentially prefix the module of the newly loaded module with
``parent`` so that Python would be able to resolve relative imports in
the module file.
:returns: The loaded Python module.

.. versionchanged:: 4.6
The ``parent`` argument is added.
'''

# Expand and sanitize filename
Expand All @@ -103,6 +110,9 @@ def import_module_from_file(filename, force=False):
# with other modules loaded with a standard `import` or with multiple
# test files with the same name that reside in different directories.
module_hash = sha256(filename.encode('utf-8')).hexdigest()[:8]
if parent:
module_name = f'{parent}.{module_name}'

module_name = f'{module_name}@{module_hash}'
return _do_import_module_from_file(filename, module_name)

Expand Down
Empty file.
10 changes: 3 additions & 7 deletions unittests/resources/checks_unlisted/testlib/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@

import reframe as rfm
import reframe.utility.sanity as sn


class dummy_fixture(rfm.RunOnlyRegressionTest, pin_prefix=True):
executable = 'echo'
sanity_patterns = sn.assert_true(1)
from .utility import dummy_fixture


@rfm.simple_test
class simple_echo_check(rfm.RunOnlyRegressionTest):
class simple_echo_check(rfm.RunOnlyRegressionTest, pin_prefix=True):
descr = 'Simple Echo Test'
valid_systems = ['*']
valid_prog_environs = ['builtin']
executable = 'echo'
executable_opts = ['Hello']
message = variable(str, value='World')
message = variable(str, value='World')
dummy = fixture(dummy_fixture, scope='environment')

@run_before('run')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import reframe as rfm
import reframe.utility.sanity as sn


class dummy_fixture(rfm.RunOnlyRegressionTest):
executable = 'echo'
sanity_patterns = sn.assert_true(1)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# SPDX-License-Identifier: BSD-3-Clause

import reframe as rfm

from testlib.simple import simple_echo_check


Expand Down
14 changes: 14 additions & 0 deletions unittests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import shutil

import reframe as rfm
import reframe.utility.osext as osext
from reframe.core.exceptions import ReframeSyntaxError
from reframe.frontend.loader import RegressionCheckLoader

Expand Down Expand Up @@ -140,3 +141,16 @@ def setup(self, partition, environ, **job_opts):
class TestSpecialDerived(TestSpecial):
def setup(self, partition, environ, **job_opts):
super().setup(partition, environ, **job_opts)


def test_relative_import_outside_rfm_prefix(loader, tmp_path):
# If a test file resides under the reframe installation prefix, it will be
# imported as a hierarchical module. If not, we want to make sure that
# reframe will still load its parent modules

osext.copytree(
os.path.abspath('unittests/resources/checks_unlisted/testlib'),
tmp_path / 'testlib', dirs_exist_ok=True
)
tests = loader.load_from_file(str(tmp_path / 'testlib' / 'simple.py'))
assert len(tests) == 2