Skip to content

Commit 1a5ea64

Browse files
committed
WIP - adding tests
1 parent 0edd625 commit 1a5ea64

File tree

6 files changed

+289
-9
lines changed

6 files changed

+289
-9
lines changed

easybuild/tools/docs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from easybuild.tools.toolchain.utilities import search_toolchain
6767
from easybuild.tools.utilities import INDENT_2SPACES, INDENT_4SPACES
6868
from easybuild.tools.utilities import import_available_modules, mk_md_table, mk_rst_table, nub, quote_str
69+
from easybuild.tools.entrypoints import EASYBLOCK_ENTRYPOINT_MARK
6970

7071

7172
_log = fancylogger.getLogger('tools.docs')
@@ -724,6 +725,9 @@ def gen_list_easyblocks(list_easyblocks, format_strings):
724725
def add_class(classes, cls):
725726
"""Add a new class, and all of its subclasses."""
726727
children = cls.__subclasses__()
728+
# Filter out possible sublcasses coming from entrypoints as they will be readded letter
729+
if not build_option('use_entrypoints', default=False):
730+
children = [c for c in children if not hasattr(c, EASYBLOCK_ENTRYPOINT_MARK)]
727731
classes.update({cls.__name__: {
728732
'module': cls.__module__,
729733
'children': sorted([c.__name__ for c in children], key=lambda x: x.lower())

easybuild/tools/entrypoints.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,9 @@ def get_group_entrypoints(group: str):
4747
_log.debug("`get_group_entrypoints` called before BuildOptions initialized, with python < 3.8")
4848
else:
4949
if HAVE_ENTRY_POINTS_CLS:
50-
eps = entry_points(group=group)
51-
res = set(eps)
50+
res = set(entry_points(group=group))
5251
else:
53-
eps = entry_points()
54-
res = set(eps.get(group, []))
52+
res = set(entry_points().get(group, []))
5553

5654
return res
5755

easybuild/tools/toolchain/toolchain.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class Toolchain:
168168
CLASS_CONSTANTS_TO_RESTORE = None
169169
CLASS_CONSTANT_COPIES = {}
170170

171-
# class method
171+
@classmethod
172172
def _is_toolchain_for(cls, name):
173173
"""see if this class can provide support for toolchain named name"""
174174
# TODO report later in the initialization the found version
@@ -181,8 +181,6 @@ def _is_toolchain_for(cls, name):
181181
# is no name is supplied, check whether class can be used as a toolchain
182182
return bool(getattr(cls, 'NAME', None))
183183

184-
_is_toolchain_for = classmethod(_is_toolchain_for)
185-
186184
def __init__(self, name=None, version=None, mns=None, class_constants=None, tcdeps=None, modtool=None,
187185
hidden=False):
188186
"""

easybuild/tools/toolchain/utilities.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import easybuild.tools.toolchain
4343
from easybuild.tools.entrypoints import (
4444
get_toolchain_entrypoints, validate_toolchain_entrypoints,
45-
TOOLCHAIN_ENTRYPOINT_PREPEND
45+
TOOLCHAIN_ENTRYPOINT_PREPEND, TOOLCHAIN_ENTRYPOINT_MARK
4646
)
4747
from easybuild.base import fancylogger
4848
from easybuild.tools.build_log import EasyBuildError
@@ -113,6 +113,10 @@ def search_toolchain(name):
113113
# obtain all subclasses of toolchain
114114
found_tcs = nub(get_subclasses(Toolchain))
115115

116+
# Getting all subclasses will also include toolchains that are registered as entrypoints even if we are not
117+
# using the `--use-entrypoints` option, so we filter them out here and re-add them later if needed.
118+
found_tcs = [x for x in found_tcs if not hasattr(x, TOOLCHAIN_ENTRYPOINT_MARK)]
119+
116120
invalid_eps = validate_toolchain_entrypoints()
117121
if invalid_eps:
118122
_log.warning("Invalid toolchain entrypoints found: %s", ', '.join(invalid_eps))

test/framework/entrypoints.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# #
2+
# Copyright 2013-2025 Ghent University
3+
#
4+
# This file is part of EasyBuild,
5+
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6+
# with support of Ghent University (http://ugent.be/hpc),
7+
# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8+
# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9+
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10+
#
11+
# https://github.com/easybuilders/easybuild
12+
#
13+
# EasyBuild is free software: you can redistribute it and/or modify
14+
# it under the terms of the GNU General Public License as published by
15+
# the Free Software Foundation v2.
16+
#
17+
# EasyBuild is distributed in the hope that it will be useful,
18+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
# GNU General Public License for more details.
21+
#
22+
# You should have received a copy of the GNU General Public License
23+
# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>.
24+
# #
25+
"""
26+
Unit tests for EasyBuild configuration.
27+
28+
@author: Davide Grassano (CECAM - EPFL)
29+
"""
30+
31+
import os
32+
import re
33+
import shutil
34+
import sys
35+
import tempfile
36+
from importlib import reload
37+
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
38+
from unittest import TextTestRunner
39+
40+
import easybuild.tools.options as eboptions
41+
from easybuild.tools.build_log import EasyBuildError
42+
from easybuild.tools.config import ERROR, IGNORE, WARN, BuildOptions, ConfigurationVariables
43+
from easybuild.tools.config import build_option, build_path, get_build_log_path, get_log_filename, get_repositorypath
44+
from easybuild.tools.config import install_path, log_file_format, log_path, source_paths
45+
from easybuild.tools.config import update_build_option, update_build_options
46+
from easybuild.tools.config import DEFAULT_PATH_SUBDIRS, init_build_options
47+
from easybuild.tools.filetools import copy_dir, mkdir, write_file
48+
from easybuild.tools.options import CONFIG_ENV_VAR_PREFIX
49+
from easybuild.tools.docs import list_easyblocks, list_software, list_toolchains
50+
from easybuild.tools.entrypoints import (
51+
get_group_entrypoints, HOOKS_ENTRYPOINT, EASYBLOCK_ENTRYPOINT, TOOLCHAIN_ENTRYPOINT,
52+
HAVE_ENTRY_POINTS
53+
)
54+
from easybuild.framework.easyconfig.easyconfig import get_module_path
55+
from easybuild.framework.easyblock import EasyBlock
56+
57+
58+
if HAVE_ENTRY_POINTS:
59+
from importlib.metadata import DistributionFinder, Distribution
60+
61+
62+
MOCK_HOOK_EP_NAME = "mock_hook"
63+
MOCK_EASYBLOCK_EP_NAME = "mock_easyblock"
64+
MOCK_TOOLCHAIN_EP_NAME = "mock_toolchain"
65+
66+
MOCK_HOOK = "hello_world"
67+
MOCK_EASYBLOCK = "TestEasyBlock"
68+
MOCK_TOOLCHAIN = "MockTc"
69+
70+
71+
MOCK_EP_FILE=f"""
72+
from easybuild.tools.entrypoints import register_entrypoint_hooks
73+
from easybuild.tools.hooks import CONFIGURE_STEP, START
74+
75+
76+
@register_entrypoint_hooks(START)
77+
def {MOCK_HOOK}():
78+
print("Hello, World! ----------------------------------------")
79+
80+
##########################################################################
81+
from easybuild.framework.easyblock import EasyBlock
82+
from easybuild.tools.entrypoints import register_easyblock_entrypoint
83+
84+
@register_easyblock_entrypoint()
85+
class {MOCK_EASYBLOCK}(EasyBlock):
86+
def configure_step(self):
87+
print("{MOCK_EASYBLOCK}: configure_step called.")
88+
89+
def build_step(self):
90+
print("{MOCK_EASYBLOCK}: build_step called.")
91+
92+
def install_step(self):
93+
print("{MOCK_EASYBLOCK}: install_step called.")
94+
95+
def sanity_check_step(self):
96+
print("{MOCK_EASYBLOCK}: sanity_check_step called.")
97+
98+
##########################################################################
99+
from easybuild.tools.entrypoints import register_toolchain_entrypoint
100+
from easybuild.tools.toolchain.compiler import DEFAULT_OPT_LEVEL, Compiler
101+
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME
102+
103+
TC_CONSTANT_MOCK = "Mock"
104+
105+
class MockCompiler(Compiler):
106+
COMPILER_FAMILY = TC_CONSTANT_MOCK
107+
SUBTOOLCHAIN = SYSTEM_TOOLCHAIN_NAME
108+
109+
@register_toolchain_entrypoint()
110+
class {MOCK_TOOLCHAIN}(MockCompiler):
111+
NAME = '{MOCK_TOOLCHAIN}' # Using `...tc` to distinguish toolchain from package
112+
COMPILER_MODULE_NAME = [NAME]
113+
SUBTOOLCHAIN = [SYSTEM_TOOLCHAIN_NAME]
114+
"""
115+
116+
117+
118+
MOCK_EP_META_FILE = f"""
119+
[{HOOKS_ENTRYPOINT}]
120+
{MOCK_HOOK_EP_NAME} = {{module}}:hello_world
121+
122+
[{EASYBLOCK_ENTRYPOINT}]
123+
{MOCK_EASYBLOCK_EP_NAME} = {{module}}:TestEasyBlock
124+
125+
[{TOOLCHAIN_ENTRYPOINT}]
126+
{MOCK_TOOLCHAIN_EP_NAME} = {{module}}:MockTc
127+
"""
128+
129+
130+
class MockDistribution(Distribution):
131+
"""Mock distribution for testing entry points."""
132+
def __init__(self, module):
133+
self.module = module
134+
135+
def read_text(self, filename):
136+
if filename == "entry_points.txt":
137+
return MOCK_EP_META_FILE.format(module=self.module)
138+
139+
if filename == "METADATA":
140+
return "Name: mock_hook\nVersion: 0.1.0\n"
141+
142+
class MockDistributionFinder(DistributionFinder):
143+
"""Mock distribution finder for testing entry points."""
144+
def __init__(self, *args, module, **kwargs):
145+
super().__init__(*args, **kwargs)
146+
self.module = module
147+
148+
def find_distributions(self, context=None):
149+
yield MockDistribution(self.module)
150+
151+
152+
class EasyBuildEntrypointsTest(EnhancedTestCase):
153+
"""Test cases for EasyBuild configuration."""
154+
155+
tmpdir = None
156+
157+
def setUp(self):
158+
"""Set up the test environment."""
159+
reload(eboptions)
160+
super().setUp()
161+
self.tmpdir = tempfile.mkdtemp(prefix='easybuild_test_')
162+
163+
if HAVE_ENTRY_POINTS:
164+
filename_root = "mock"
165+
dirname, dirpath = os.path.split(self.tmpdir)
166+
167+
self.module = '.'.join([dirpath, filename_root])
168+
sys.path.insert(0, dirname)
169+
sys.meta_path.insert(0, MockDistributionFinder(module=self.module))
170+
171+
# Create a mock entry point for testing
172+
mock_hook_file = os.path.join(self.tmpdir, f'{filename_root}.py')
173+
write_file(mock_hook_file, MOCK_EP_FILE)
174+
175+
def tearDown(self):
176+
"""Clean up the test environment."""
177+
if self.tmpdir and os.path.isdir(self.tmpdir):
178+
shutil.rmtree(self.tmpdir)
179+
180+
if HAVE_ENTRY_POINTS:
181+
# Remove the entry point from the working set
182+
torm = []
183+
for idx,cls in enumerate(sys.meta_path):
184+
if isinstance(cls, MockDistributionFinder):
185+
torm.append(idx)
186+
for idx in reversed(torm):
187+
del sys.meta_path[idx]
188+
189+
def test_entrypoints_get_group_too_old_python(self):
190+
"""Test retrieving entrypoints for a specific group with too old Python version."""
191+
if HAVE_ENTRY_POINTS:
192+
self.skipTest("Entry points available in this Python version")
193+
self.assertRaises(EasyBuildError, get_group_entrypoints, HOOKS_ENTRYPOINT)
194+
195+
def test_entrypoints_get_group(self):
196+
"""Test retrieving entrypoints for a specific group."""
197+
if not HAVE_ENTRY_POINTS:
198+
self.skipTest("Entry points not available in this Python version")
199+
200+
expected = {
201+
HOOKS_ENTRYPOINT: MOCK_HOOK_EP_NAME,
202+
EASYBLOCK_ENTRYPOINT: MOCK_EASYBLOCK_EP_NAME,
203+
TOOLCHAIN_ENTRYPOINT: MOCK_TOOLCHAIN_EP_NAME,
204+
}
205+
206+
# init_config()
207+
for group in [HOOKS_ENTRYPOINT, EASYBLOCK_ENTRYPOINT, TOOLCHAIN_ENTRYPOINT]:
208+
epts = get_group_entrypoints(group)
209+
self.assertIsInstance(epts, set, f"Expected set for group {group}")
210+
self.assertEqual(len(epts), 0, f"Expected non-empty set for group {group}")
211+
212+
init_config(build_options={'use_entrypoints': True})
213+
for group in [HOOKS_ENTRYPOINT, EASYBLOCK_ENTRYPOINT, TOOLCHAIN_ENTRYPOINT]:
214+
epts = get_group_entrypoints(group)
215+
self.assertIsInstance(epts, set, f"Expected set for group {group}")
216+
self.assertGreater(len(epts), 0, f"Expected non-empty set for group {group}")
217+
218+
loaded_names = [ep.name for ep in epts]
219+
self.assertIn(expected[group], loaded_names, f"Expected entry point {expected[group]} in group {group}")
220+
221+
def test_entrypoints_list_easyblocks(self):
222+
"""
223+
Tests for list_easyblocks function with entry points enabled.
224+
"""
225+
if not HAVE_ENTRY_POINTS:
226+
self.skipTest("Entry points not available in this Python version")
227+
228+
# init_config()
229+
# print('-------', build_option('use_entrypoints', default='1234'))
230+
txt = list_easyblocks()
231+
self.assertNotIn("TestEasyBlock", txt, "TestEasyBlock should not be listed without entry points enabled")
232+
233+
init_config(build_options={'use_entrypoints': True})
234+
txt = list_easyblocks()
235+
self.assertIn("TestEasyBlock", txt, "TestEasyBlock should be listed with entry points enabled")
236+
237+
def test_entrypoints_list_toolchains(self):
238+
"""
239+
Tests for list_toolchains function with entry points enabled.
240+
"""
241+
if not HAVE_ENTRY_POINTS:
242+
self.skipTest("Entry points not available in this Python version")
243+
244+
# init_config()
245+
txt = list_toolchains()
246+
self.assertNotIn(MOCK_TOOLCHAIN, txt, f"{MOCK_TOOLCHAIN} should not be listed without entry points enabled")
247+
248+
init_config(build_options={'use_entrypoints': True})
249+
250+
txt = list_toolchains()
251+
self.assertIn(MOCK_TOOLCHAIN, txt, f"{MOCK_TOOLCHAIN} should be listed with entry points enabled")
252+
253+
def test_entrypoints_get_module_path(self):
254+
"""
255+
Tests for get_module_path function with entry points enabled.
256+
"""
257+
if not HAVE_ENTRY_POINTS:
258+
self.skipTest("Entry points not available in this Python version")
259+
260+
module_path = get_module_path(MOCK_EASYBLOCK)
261+
self.assertIn('.generic.', module_path, "Module path should contain '.generic.'")
262+
263+
init_config(build_options={'use_entrypoints': True})
264+
# Reload the EasyBlock module to ensure it is recognized
265+
module_path = get_module_path(MOCK_EASYBLOCK)
266+
self.assertEqual(module_path, self.module, "Module path should match the mock module path")
267+
268+
269+
def suite():
270+
return TestLoaderFiltered().loadTestsFromTestCase(EasyBuildEntrypointsTest, sys.argv[1:])
271+
272+
273+
if __name__ == '__main__':
274+
res = TextTestRunner(verbosity=1).run(suite())
275+
sys.exit(len(res.failures))

test/framework/suite.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import test.framework.easyconfigversion as ev
5353
import test.framework.easystack as es
5454
import test.framework.ebconfigobj as ebco
55+
import test.framework.entrypoints as epts
5556
import test.framework.environment as env
5657
import test.framework.docs as d
5758
import test.framework.filetools as f
@@ -119,7 +120,7 @@
119120

120121
# call suite() for each module and then run them all
121122
# note: make sure the options unit tests run first, to avoid running some of them with a readily initialized config
122-
tests = [gen, d, bl, o, r, ef, ev, ebco, ep, e, mg, m, mt, f, run, a, robot, b, v, g, tcv, tc, t, c, s, lic, f_c,
123+
tests = [gen, d, bl, o, r, ef, ev, ebco, ep, epts, e, mg, m, mt, f, run, a, robot, b, v, g, tcv, tc, t, c, s, lic, f_c,
123124
tw, p, i, pkg, env, et, st, h, ct, lib, u, es, ou]
124125

125126
SUITE = unittest.TestSuite([x.suite() for x in tests])

0 commit comments

Comments
 (0)