Skip to content

Commit e0bb957

Browse files
authored
Merge pull request #3605 from migueldiascosta/from_pr_multiple_prs
add support for multiple PRs in --from-pr
2 parents eb532d0 + d3d8e4c commit e0bb957

File tree

8 files changed

+160
-29
lines changed

8 files changed

+160
-29
lines changed

easybuild/framework/easyconfig/tools.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def alt_easyconfig_paths(tmpdir, tweaked_ecs=False, from_pr=False):
321321
# path where files touched in PR will be downloaded to
322322
pr_path = None
323323
if from_pr:
324-
pr_path = os.path.join(tmpdir, "files_pr%s" % from_pr)
324+
pr_path = os.path.join(tmpdir, "files_pr%s" % '_'.join(str(pr) for pr in from_pr))
325325

326326
return tweaked_ecs_paths, pr_path
327327

@@ -332,14 +332,20 @@ def det_easyconfig_paths(orig_paths):
332332
:param orig_paths: list of original easyconfig paths
333333
:return: list of paths to easyconfig files
334334
"""
335-
from_pr = build_option('from_pr')
335+
try:
336+
from_pr_list = [int(pr_nr) for pr_nr in build_option('from_pr')]
337+
except ValueError:
338+
raise EasyBuildError("Argument to --from-pr must be a comma separated list of PR #s.")
339+
336340
robot_path = build_option('robot_path')
337341

338342
# list of specified easyconfig files
339343
ec_files = orig_paths[:]
340344

341-
if from_pr is not None:
342-
pr_files = fetch_easyconfigs_from_pr(from_pr)
345+
if from_pr_list is not None:
346+
pr_files = []
347+
for pr in from_pr_list:
348+
pr_files.extend(fetch_easyconfigs_from_pr(pr))
343349

344350
if ec_files:
345351
# replace paths for specified easyconfigs that are touched in PR
@@ -725,6 +731,9 @@ def avail_easyblocks():
725731
def det_copy_ec_specs(orig_paths, from_pr):
726732
"""Determine list of paths + target directory for --copy-ec."""
727733

734+
if from_pr is not None and not isinstance(from_pr, list):
735+
from_pr = [from_pr]
736+
728737
target_path, paths = None, []
729738

730739
# if only one argument is specified, use current directory as target directory
@@ -746,8 +755,10 @@ def det_copy_ec_specs(orig_paths, from_pr):
746755
# to avoid potential trouble with already existing files in the working tmpdir
747756
# (note: we use a fixed subdirectory in the working tmpdir here rather than a unique random subdirectory,
748757
# to ensure that the caching for fetch_files_from_pr works across calls for the same PR)
749-
tmpdir = os.path.join(tempfile.gettempdir(), 'fetch_files_from_pr_%s' % from_pr)
750-
pr_paths = fetch_files_from_pr(pr=from_pr, path=tmpdir)
758+
tmpdir = os.path.join(tempfile.gettempdir(), 'fetch_files_from_pr_%s' % '_'.join(str(pr) for pr in from_pr))
759+
pr_paths = []
760+
for pr in from_pr:
761+
pr_paths.extend(fetch_files_from_pr(pr=pr, path=tmpdir))
751762

752763
# assume that files need to be copied to current working directory for now
753764
target_path = os.getcwd()

easybuild/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
207207
options, orig_paths = eb_go.options, eb_go.args
208208

209209
global _log
210-
(build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate, tweaked_ecs_paths) = cfg_settings
210+
(build_specs, _log, logfile, robot_path, search_query, eb_tmpdir, try_to_generate,
211+
from_pr_list, tweaked_ecs_paths) = cfg_settings
211212

212213
# load hook implementations (if any)
213214
hooks = load_hooks(options.hooks)
@@ -318,7 +319,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
318319

319320
if options.copy_ec:
320321
# figure out list of files to copy + target location (taking into account --from-pr)
321-
orig_paths, target_path = det_copy_ec_specs(orig_paths, options.from_pr)
322+
orig_paths, target_path = det_copy_ec_specs(orig_paths, from_pr_list)
322323

323324
categorized_paths = categorize_files_by_type(orig_paths)
324325

easybuild/tools/filetools.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,19 @@ def symlink(source_path, symlink_path, use_abspath_source=True):
298298
if use_abspath_source:
299299
source_path = os.path.abspath(source_path)
300300

301-
try:
302-
os.symlink(source_path, symlink_path)
303-
_log.info("Symlinked %s to %s", source_path, symlink_path)
304-
except OSError as err:
305-
raise EasyBuildError("Symlinking %s to %s failed: %s", source_path, symlink_path, err)
301+
if os.path.exists(symlink_path):
302+
abs_source_path = os.path.abspath(source_path)
303+
symlink_target_path = os.path.abspath(os.readlink(symlink_path))
304+
if abs_source_path != symlink_target_path:
305+
raise EasyBuildError("Trying to symlink %s to %s, but the symlink already exists and points to %s.",
306+
source_path, symlink_path, symlink_target_path)
307+
_log.info("Skipping symlinking %s to %s, link already exists", source_path, symlink_path)
308+
else:
309+
try:
310+
os.symlink(source_path, symlink_path)
311+
_log.info("Symlinked %s to %s", source_path, symlink_path)
312+
except OSError as err:
313+
raise EasyBuildError("Symlinking %s to %s failed: %s", source_path, symlink_path, err)
306314

307315

308316
def remove_file(path):

easybuild/tools/options.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ def github_options(self):
655655
'check-style': ("Run a style check on the given easyconfigs", None, 'store_true', False),
656656
'cleanup-easyconfigs': ("Clean up easyconfig files for pull request", None, 'store_true', True),
657657
'dump-test-report': ("Dump test report to specified path", None, 'store_or_None', 'test_report.md'),
658-
'from-pr': ("Obtain easyconfigs from specified PR", int, 'store', None, {'metavar': 'PR#'}),
658+
'from-pr': ("Obtain easyconfigs from specified PR", 'strlist', 'store', [], {'metavar': 'PR#'}),
659659
'git-working-dirs-path': ("Path to Git working directories for EasyBuild repositories", str, 'store', None),
660660
'github-user': ("GitHub username", str, 'store', None),
661661
'github-org': ("GitHub organization", str, 'store', None),
@@ -1459,10 +1459,16 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False):
14591459
# software name/version, toolchain name/version, extra patches, ...
14601460
(try_to_generate, build_specs) = process_software_build_specs(options)
14611461

1462+
# map --from-pr strlist to list of ints
1463+
try:
1464+
from_pr_list = [int(pr_nr) for pr_nr in eb_go.options.from_pr]
1465+
except ValueError:
1466+
raise EasyBuildError("Argument to --from-pr must be a comma separated list of PR #s.")
1467+
14621468
# determine robot path
14631469
# --try-X, --dep-graph, --search use robot path for searching, so enable it with path of installed easyconfigs
14641470
tweaked_ecs = try_to_generate and build_specs
1465-
tweaked_ecs_paths, pr_path = alt_easyconfig_paths(tmpdir, tweaked_ecs=tweaked_ecs, from_pr=options.from_pr)
1471+
tweaked_ecs_paths, pr_path = alt_easyconfig_paths(tmpdir, tweaked_ecs=tweaked_ecs, from_pr=from_pr_list)
14661472
auto_robot = try_to_generate or options.check_conflicts or options.dep_graph or search_query
14671473
robot_path = det_robot_path(options.robot_paths, tweaked_ecs_paths, pr_path, auto_robot=auto_robot)
14681474
log.debug("Full robot path: %s" % robot_path)
@@ -1491,7 +1497,7 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False):
14911497
# done here instead of in _postprocess_include because github integration requires build_options to be initialized
14921498
if eb_go.options.include_easyblocks_from_pr:
14931499
try:
1494-
easyblock_prs = map(int, eb_go.options.include_easyblocks_from_pr)
1500+
easyblock_prs = [int(pr_nr) for pr_nr in eb_go.options.include_easyblocks_from_pr]
14951501
except ValueError:
14961502
raise EasyBuildError("Argument to --include-easyblocks-from-pr must be a comma separated list of PR #s.")
14971503

@@ -1534,7 +1540,8 @@ def set_up_configuration(args=None, logfile=None, testing=False, silent=False):
15341540
sys.path.remove(fake_vsc_path)
15351541
sys.path.insert(0, new_fake_vsc_path)
15361542

1537-
return eb_go, (build_specs, log, logfile, robot_path, search_query, tmpdir, try_to_generate, tweaked_ecs_paths)
1543+
return eb_go, (build_specs, log, logfile, robot_path, search_query, tmpdir, try_to_generate,
1544+
from_pr_list, tweaked_ecs_paths)
15381545

15391546

15401547
def process_software_build_specs(options):

easybuild/tools/testing.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def session_state():
138138
}
139139

140140

141-
def create_test_report(msg, ecs_with_res, init_session_state, pr_nr=None, gist_log=False, easyblock_pr_nrs=None):
141+
def create_test_report(msg, ecs_with_res, init_session_state, pr_nrs=None, gist_log=False, easyblock_pr_nrs=None):
142142
"""Create test report for easyconfigs PR, in Markdown format."""
143143

144144
github_user = build_option('github_user')
@@ -149,10 +149,11 @@ def create_test_report(msg, ecs_with_res, init_session_state, pr_nr=None, gist_l
149149

150150
# create a gist with a full test report
151151
test_report = []
152-
if pr_nr is not None:
152+
if pr_nrs is not None:
153153
repo = pr_target_repo or GITHUB_EASYCONFIGS_REPO
154+
pr_urls = ["https://github.com/%s/%s/pull/%s" % (pr_target_account, repo, pr_nr) for pr_nr in pr_nrs]
154155
test_report.extend([
155-
"Test report for https://github.com/%s/%s/pull/%s" % (pr_target_account, repo, pr_nr),
156+
"Test report for %s" % ', '.join(pr_urls),
156157
"",
157158
])
158159
if easyblock_pr_nrs:
@@ -190,10 +191,13 @@ def create_test_report(msg, ecs_with_res, init_session_state, pr_nr=None, gist_l
190191
logtxt = read_file(ec_res['log_file'])
191192
partial_log_txt = '\n'.join(logtxt.split('\n')[-500:])
192193
descr = "(partial) EasyBuild log for failed build of %s" % ec['spec']
193-
if pr_nr is not None:
194-
descr += " (PR #%s)" % pr_nr
194+
195+
if pr_nrs is not None:
196+
descr += " (PR #%s)" % ', #'.join(pr_nrs)
197+
195198
if easyblock_pr_nrs:
196199
descr += "".join(" (easyblock PR #%s)" % nr for nr in easyblock_pr_nrs)
200+
197201
fn = '%s_partial.log' % os.path.basename(ec['spec'])[:-3]
198202
gist_url = create_gist(partial_log_txt, fn, descr=descr, github_user=github_user)
199203
test_log = "(partial log available at %s)" % gist_url
@@ -291,7 +295,7 @@ def post_pr_test_report(pr_nr, repo_type, test_report, msg, init_session_state,
291295

292296
if build_option('include_easyblocks_from_pr'):
293297
if repo_type == GITHUB_EASYCONFIGS_REPO:
294-
easyblocks_pr_nrs = map(int, build_option('include_easyblocks_from_pr'))
298+
easyblocks_pr_nrs = [int(pr_nr) for pr_nr in build_option('include_easyblocks_from_pr')]
295299
comment_lines.append("Using easyblocks from PR(s) %s" %
296300
", ".join(["https://github.com/%s/%s/pull/%s" %
297301
(pr_target_account, GITHUB_EASYBLOCKS_REPO, easyblocks_pr_nr)
@@ -327,22 +331,35 @@ def overall_test_report(ecs_with_res, orig_cnt, success, msg, init_session_state
327331
:param init_session_state: initial session state info to include in test report
328332
"""
329333
dump_path = build_option('dump_test_report')
330-
pr_nr = build_option('from_pr')
331-
easyblock_pr_nrs = build_option('include_easyblocks_from_pr')
334+
335+
try:
336+
pr_nrs = [int(pr_nr) for pr_nr in build_option('from_pr')]
337+
except ValueError:
338+
raise EasyBuildError("Argument to --from-pr must be a comma separated list of PR #s.")
339+
340+
try:
341+
easyblock_pr_nrs = [int(pr_nr) for pr_nr in build_option('include_easyblocks_from_pr')]
342+
except ValueError:
343+
raise EasyBuildError("Argument to --include-easyblocks-from-pr must be a comma separated list of PR #s.")
344+
332345
upload = build_option('upload_test_report')
333346

334347
if upload:
335348
msg = msg + " (%d easyconfigs in total)" % orig_cnt
336-
test_report = create_test_report(msg, ecs_with_res, init_session_state, pr_nr=pr_nr, gist_log=True,
349+
350+
test_report = create_test_report(msg, ecs_with_res, init_session_state, pr_nrs=pr_nrs, gist_log=True,
337351
easyblock_pr_nrs=easyblock_pr_nrs)
338-
if pr_nr:
352+
if pr_nrs:
339353
# upload test report to gist and issue a comment in the PR to notify
340-
txt = post_pr_test_report(pr_nr, GITHUB_EASYCONFIGS_REPO, test_report, msg, init_session_state, success)
354+
for pr_nr in pr_nrs:
355+
txt = post_pr_test_report(pr_nr, GITHUB_EASYCONFIGS_REPO, test_report, msg, init_session_state,
356+
success)
341357
elif easyblock_pr_nrs:
342358
# upload test report to gist and issue a comment in the easyblocks PR to notify
343-
for easyblock_pr_nr in map(int, easyblock_pr_nrs):
359+
for easyblock_pr_nr in easyblock_pr_nrs:
344360
txt = post_pr_test_report(easyblock_pr_nr, GITHUB_EASYBLOCKS_REPO, test_report, msg,
345361
init_session_state, success)
362+
346363
else:
347364
# only upload test report as a gist
348365
gist_url = upload_test_report_as_gist(test_report['full'])

test/framework/filetools.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,17 @@ def test_symlink_resolve_path(self):
574574

575575
self.assertTrue(os.path.samefile(os.path.join(self.test_prefix, 'test', 'test.txt'), link))
576576

577+
# test symlink when it already exists and points to the same path
578+
ft.symlink(test_file, link)
579+
580+
# test symlink when it already exists but points to a different path
581+
test_file2 = os.path.join(link_dir, 'test2.txt')
582+
ft.write_file(test_file, "test123")
583+
self.assertErrorRegex(EasyBuildError,
584+
"Trying to symlink %s to %s, but the symlink already exists and points to %s." %
585+
(test_file2, link, test_file),
586+
ft.symlink, test_file2, link)
587+
577588
# test resolve_path
578589
self.assertEqual(test_dir, ft.resolve_path(link_dir))
579590
self.assertEqual(os.path.join(os.path.realpath(self.test_prefix), 'test', 'test.txt'), ft.resolve_path(link))

test/framework/options.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ def test_list_easyblocks(self):
825825
r'\| \| \|-- EB_toytoy',
826826
r'\| \|-- Toy_Extension',
827827
r'\|-- ModuleRC',
828+
r'\|-- PythonBundle',
828829
r'\|-- Toolchain',
829830
r'Extension',
830831
r'\|-- ExtensionEasyBlock',
@@ -1733,6 +1734,41 @@ def test_from_pr(self):
17331734
print("Ignoring URLError '%s' in test_from_pr" % err)
17341735
shutil.rmtree(tmpdir)
17351736

1737+
# test with multiple prs
1738+
tmpdir = tempfile.mkdtemp()
1739+
args = [
1740+
# PRs for ReFrame 3.4.1 and 3.5.0
1741+
'--from-pr=12150,12366',
1742+
'--dry-run',
1743+
# an argument must be specified to --robot, since easybuild-easyconfigs may not be installed
1744+
'--robot=%s' % os.path.join(os.path.dirname(__file__), 'easyconfigs'),
1745+
'--unittest-file=%s' % self.logfile,
1746+
'--github-user=%s' % GITHUB_TEST_ACCOUNT, # a GitHub token should be available for this user
1747+
'--tmpdir=%s' % tmpdir,
1748+
]
1749+
try:
1750+
outtxt = self.eb_main(args, logfile=dummylogfn, raise_error=True)
1751+
modules = [
1752+
(tmpdir, 'ReFrame/3.4.1'),
1753+
(tmpdir, 'ReFrame/3.5.0'),
1754+
]
1755+
for path_prefix, module in modules:
1756+
ec_fn = "%s.eb" % '-'.join(module.split('/'))
1757+
path = '.*%s' % os.path.dirname(path_prefix)
1758+
regex = re.compile(r"^ \* \[.\] %s.*%s \(module: %s\)$" % (path, ec_fn, module), re.M)
1759+
self.assertTrue(regex.search(outtxt), "Found pattern %s in %s" % (regex.pattern, outtxt))
1760+
1761+
# make sure that *only* these modules are listed, no others
1762+
regex = re.compile(r"^ \* \[.\] .*/(?P<filepath>.*) \(module: (?P<module>.*)\)$", re.M)
1763+
self.assertTrue(sorted(regex.findall(outtxt)), sorted(modules))
1764+
1765+
pr_tmpdir = os.path.join(tmpdir, r'eb-\S{6,8}', 'files_pr12150_12366')
1766+
regex = re.compile("Appended list of robot search paths with %s:" % pr_tmpdir, re.M)
1767+
self.assertTrue(regex.search(outtxt), "Found pattern %s in %s" % (regex.pattern, outtxt))
1768+
except URLError as err:
1769+
print("Ignoring URLError '%s' in test_from_pr" % err)
1770+
shutil.rmtree(tmpdir)
1771+
17361772
def test_from_pr_token_log(self):
17371773
"""Check that --from-pr doesn't leak GitHub token in log."""
17381774
if self.github_token is None:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
##
2+
# Copyright 2009-2020 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+
Dummy easyblock for Makecp.
27+
28+
@author: Miguel Dias Costa (National University of Singapore)
29+
"""
30+
from easybuild.framework.easyblock import EasyBlock
31+
32+
33+
class PythonBundle(EasyBlock):
34+
"""Dummy support for bundle of modules."""
35+
36+
@staticmethod
37+
def extra_options(extra_vars=None):
38+
if extra_vars is None:
39+
extra_vars = {}
40+
return EasyBlock.extra_options(extra_vars)

0 commit comments

Comments
 (0)