Skip to content

Commit 387b9ec

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1404 from vkarak/feat/dont-restage
[feat] Add option to disable test stage directory cleanup
2 parents 973d39e + 3861980 commit 387b9ec

File tree

9 files changed

+153
-42
lines changed

9 files changed

+153
-42
lines changed

docs/config_reference.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,16 @@ General Configuration
10401040

10411041

10421042

1043+
.. js:attribute:: .general[].clean_stagedir
1044+
1045+
:required: No
1046+
:default: ``true``
1047+
1048+
Clean stage directory of tests before populating it.
1049+
1050+
.. versionadded:: 3.1
1051+
1052+
10431053
.. js:attribute:: .general[].colorize
10441054

10451055
:required: No

docs/manpage.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ Options controlling ReFrame output
219219

220220
This option can also be set using the :envvar:`RFM_KEEP_STAGE_FILES` environment variable or the :js:attr:`keep_stage_files` general configuration parameter.
221221

222+
.. option:: --dont-restage
223+
224+
Do not restage a test if its stage directory exists.
225+
Normally, if the stage directory of a test exists, ReFrame will remove it and recreate it.
226+
This option disables this behavior.
227+
228+
.. versionadded:: 3.1
229+
222230
.. option:: --save-log-files
223231

224232
Save ReFrame log files in the output directory before exiting.
@@ -585,6 +593,21 @@ Here is an alphabetical list of the environment variables recognized by ReFrame:
585593
================================== ==================
586594

587595

596+
.. envvar:: RFM_CLEAN_STAGEDIR
597+
598+
Clean stage directory of tests before populating it.
599+
600+
.. versionadded:: 3.1
601+
602+
.. table::
603+
:align: left
604+
605+
================================== ==================
606+
Associated command line option :option:`--dont-restage`
607+
Associated configuration parameter :js:attr:`clean_stagedir` general configuration parameter
608+
================================== ==================
609+
610+
588611
.. envvar:: RFM_COLORIZE
589612

590613
Enable output coloring.

reframe/core/pipeline.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,9 +1072,11 @@ def _copy_to_stagedir(self, path):
10721072
(path, self._stagedir))
10731073
self.logger.debug('symlinking files: %s' % self.readonly_files)
10741074
try:
1075-
os_ext.copytree_virtual(path, self._stagedir, self.readonly_files)
1075+
os_ext.copytree_virtual(
1076+
path, self._stagedir, self.readonly_files, dirs_exist_ok=True
1077+
)
10761078
except (OSError, ValueError, TypeError) as e:
1077-
raise PipelineError('virtual copying of files failed') from e
1079+
raise PipelineError('copying of files failed') from e
10781080

10791081
def _clone_to_stagedir(self, url):
10801082
self.logger.debug('cloning URL %s to stage directory (%s)' %

reframe/core/runtime.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def _makedir(self, *dirs, wipeout=False):
4242
return ret
4343

4444
def _format_dirs(self, *dirs):
45+
if not self.get_option('general/0/clean_stagedir'):
46+
# If stagedir is to be reused, no new stage directories will be
47+
# used for retries
48+
return dirs
49+
4550
try:
4651
last = dirs[-1]
4752
except IndexError:
@@ -134,13 +139,14 @@ def stage_prefix(self):
134139

135140
return os.path.abspath(ret)
136141

137-
def make_stagedir(self, *dirs, wipeout=True):
142+
def make_stagedir(self, *dirs):
143+
wipeout = self.get_option('general/0/clean_stagedir')
138144
return self._makedir(self.stage_prefix,
139145
*self._format_dirs(*dirs), wipeout=wipeout)
140146

141-
def make_outputdir(self, *dirs, wipeout=True):
147+
def make_outputdir(self, *dirs):
142148
return self._makedir(self.output_prefix,
143-
*self._format_dirs(*dirs), wipeout=wipeout)
149+
*self._format_dirs(*dirs), wipeout=True)
144150

145151
@property
146152
def modules_system(self):

reframe/frontend/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ def main():
126126
help='Keep stage directories even for successful checks',
127127
envvar='RFM_KEEP_STAGE_FILES', configvar='general/keep_stage_files'
128128
)
129+
output_options.add_argument(
130+
'--dont-restage', action='store_false', dest='clean_stagedir',
131+
help='Reuse the test stage directory',
132+
envvar='RFM_CLEAN_STAGEDIR', configvar='general/clean_stagedir'
133+
)
129134
output_options.add_argument(
130135
'--save-log-files', action='store_true', default=False,
131136
help='Save ReFrame log files to the output directory',

reframe/schemas/config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
"items": {"type": "string"}
351351
},
352352
"check_search_recursive": {"type": "boolean"},
353+
"clean_stagedir": {"type": "boolean"},
353354
"colorize": {"type": "boolean"},
354355
"ignore_check_conflicts": {"type": "boolean"},
355356
"keep_stage_files": {"type": "boolean"},
@@ -394,6 +395,7 @@
394395
"environments/target_systems": ["*"],
395396
"general/check_search_path": ["${RFM_INSTALL_PREFIX}/checks/"],
396397
"general/check_search_recursive": false,
398+
"general/clean_stagedir": true,
397399
"general/colorize": true,
398400
"general/ignore_check_conflicts": false,
399401
"general/keep_stage_files": false,

reframe/utility/os_ext.py

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,45 @@ def osgroup():
9595

9696

9797
def copytree(src, dst, symlinks=False, ignore=None, copy_function=shutil.copy2,
98-
ignore_dangling_symlinks=False):
99-
'''Same as shutil.copytree() but valid also if 'dst' exists.
100-
101-
In this case it will first remove it and then call the standard
102-
shutil.copytree().'''
98+
ignore_dangling_symlinks=False, dirs_exist_ok=False):
99+
'''Compatibility version of :py:func:`shutil.copytree()` for Python <= 3.8
100+
'''
103101
if src == os.path.commonpath([src, dst]):
104102
raise ValueError("cannot copy recursively the parent directory "
105103
"`%s' into one of its descendants `%s'" % (src, dst))
106104

107-
if os.path.exists(dst):
108-
shutil.rmtree(dst)
105+
if sys.version_info[1] >= 8:
106+
return shutil.copytree(src, dst, symlinks, ignore, copy_function,
107+
ignore_dangling_symlinks, dirs_exist_ok)
108+
109+
if not dirs_exist_ok:
110+
return shutil.copytree(src, dst, symlinks, ignore, copy_function,
111+
ignore_dangling_symlinks)
112+
113+
# dirs_exist_ok=True and Python < 3.8
114+
if not os.path.exists(dst):
115+
return shutil.copytree(src, dst, symlinks, ignore, copy_function,
116+
ignore_dangling_symlinks)
117+
118+
# dst exists; manually descend into the subdirectories
119+
_, subdirs, files = list(os.walk(src))[0]
120+
ignore_paths = ignore(src, os.listdir(src)) if ignore else {}
121+
for f in files:
122+
if f not in ignore_paths:
123+
copy_function(os.path.join(src, f), os.path.join(dst, f))
109124

110-
shutil.copytree(src, dst, symlinks, ignore, copy_function,
111-
ignore_dangling_symlinks)
125+
for d in subdirs:
126+
if d not in ignore_paths:
127+
copytree(os.path.join(src, d), os.path.join(dst, d),
128+
symlinks, ignore, copy_function,
129+
ignore_dangling_symlinks, dirs_exist_ok)
130+
131+
return dst
112132

113133

114134
def copytree_virtual(src, dst, file_links=[],
115135
symlinks=False, copy_function=shutil.copy2,
116-
ignore_dangling_symlinks=False):
136+
ignore_dangling_symlinks=False, dirs_exist_ok=False):
117137
'''Copy `dst` to `src`, but create symlinks for the files in `file_links`.
118138
119139
If `file_links` is empty, this is equivalent to `copytree()`. The rest of
@@ -134,35 +154,42 @@ def copytree_virtual(src, dst, file_links=[],
134154
link_targets = set()
135155
for f in file_links:
136156
if os.path.isabs(f):
137-
raise ValueError("copytree_virtual() failed: `%s': "
138-
"absolute paths not allowed in file_links" % f)
157+
raise ValueError(f'copytree_virtual() failed: {f!r}: '
158+
f'absolute paths not allowed in file_links')
139159

140160
target = os.path.join(src, f)
141161
if not os.path.exists(target):
142-
raise ValueError("copytree_virtual() failed: `%s' "
143-
"does not exist" % target)
162+
raise ValueError(f'copytree_virtual() failed: {target!r} '
163+
f'does not exist')
144164

145165
if os.path.commonpath([src, target]) != src:
146-
raise ValueError("copytree_virtual() failed: "
147-
"`%s' not under `%s'" % (target, src))
166+
raise ValueError(f'copytree_virtual() failed: '
167+
f'{target!r} not under {src!r}')
148168

149169
link_targets.add(os.path.abspath(target))
150170

171+
if '.' in file_links or '..' in file_links:
172+
raise ValueError(f"'.' or '..' are not allowed in file_links")
173+
151174
if not file_links:
152175
ignore = None
153176
else:
154177
def ignore(dir, contents):
155-
return [c for c in contents
156-
if os.path.join(dir, c) in link_targets]
178+
return {c for c in contents
179+
if os.path.join(dir, c) in link_targets}
157180

158181
# Copy to dst ignoring the file_links
159182
copytree(src, dst, symlinks, ignore,
160-
copy_function, ignore_dangling_symlinks)
183+
copy_function, ignore_dangling_symlinks, dirs_exist_ok)
161184

162185
# Now create the symlinks
163186
for f in link_targets:
164187
link_name = f.replace(src, dst)
165-
os.symlink(f, link_name)
188+
try:
189+
os.symlink(f, link_name)
190+
except FileExistsError:
191+
if not dirs_exist_ok:
192+
raise
166193

167194

168195
def rmtree(*args, max_retries=3, **kwargs):

unittests/test_cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,31 @@ def test_check_sanity_failure(run_reframe, tmp_path):
221221
)
222222

223223

224+
def test_dont_restage(run_reframe, tmp_path):
225+
run_reframe(
226+
checkpath=['unittests/resources/checks/frontend_checks.py'],
227+
more_options=['-t', 'SanityFailureCheck']
228+
)
229+
230+
# Place a random file in the test's stage directory and rerun with
231+
# `--dont-restage` and `--max-retries`
232+
stagedir = (tmp_path / 'stage' / 'generic' / 'default' /
233+
'builtin-gcc' / 'SanityFailureCheck')
234+
(stagedir / 'foobar').touch()
235+
returncode, stdout, stderr = run_reframe(
236+
checkpath=['unittests/resources/checks/frontend_checks.py'],
237+
more_options=['-t', 'SanityFailureCheck',
238+
'--dont-restage', '--max-retries=1']
239+
)
240+
assert os.path.exists(stagedir / 'foobar')
241+
assert not os.path.exists(f'{stagedir}_retry1')
242+
243+
# And some standard assertions
244+
assert 'Traceback' not in stdout
245+
assert 'Traceback' not in stderr
246+
assert returncode != 0
247+
248+
224249
def test_checkpath_symlink(run_reframe, tmp_path):
225250
# FIXME: This should move to test_loader.py
226251
checks_symlink = tmp_path / 'checks_symlink'

unittests/test_utility.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,7 @@ def test_command_async(self):
6161
def test_copytree(self):
6262
dir_src = tempfile.mkdtemp()
6363
dir_dst = tempfile.mkdtemp()
64-
65-
with pytest.raises(OSError):
66-
shutil.copytree(dir_src, dir_dst)
67-
68-
try:
69-
os_ext.copytree(dir_src, dir_dst)
70-
except Exception as e:
71-
pytest.fail('custom copytree failed: %s' % e)
72-
64+
os_ext.copytree(dir_src, dir_dst, dirs_exist_ok=True)
7365
shutil.rmtree(dir_src)
7466
shutil.rmtree(dir_dst)
7567

@@ -284,6 +276,9 @@ def setUp(self):
284276
open(os.path.join(self.prefix, 'bar.txt'), 'w').close()
285277
open(os.path.join(self.prefix, 'foo.txt'), 'w').close()
286278

279+
# Create also a subdirectory in target, so as to check the recursion
280+
os.makedirs(os.path.join(self.target, 'foo'), exist_ok=True)
281+
287282
def verify_target_directory(self, file_links=[]):
288283
'''Verify the directory structure'''
289284
assert os.path.exists(os.path.join(self.target, 'bar', 'bar.txt'))
@@ -301,38 +296,54 @@ def verify_target_directory(self, file_links=[]):
301296
assert target_name == os.readlink(link_name)
302297

303298
def test_virtual_copy_nolinks(self):
304-
os_ext.copytree_virtual(self.prefix, self.target)
299+
os_ext.copytree_virtual(self.prefix, self.target, dirs_exist_ok=True)
305300
self.verify_target_directory()
306301

302+
def test_virtual_copy_nolinks_dirs_exist(self):
303+
with pytest.raises(FileExistsError):
304+
os_ext.copytree_virtual(self.prefix, self.target)
305+
307306
def test_virtual_copy_valid_links(self):
308307
file_links = ['bar/', 'foo/bar.txt', 'foo.txt']
309-
os_ext.copytree_virtual(self.prefix, self.target, file_links)
308+
os_ext.copytree_virtual(self.prefix, self.target,
309+
file_links, dirs_exist_ok=True)
310310
self.verify_target_directory(file_links)
311311

312312
def test_virtual_copy_inexistent_links(self):
313313
file_links = ['foobar/', 'foo/bar.txt', 'foo.txt']
314314
with pytest.raises(ValueError):
315-
os_ext.copytree_virtual(self.prefix, self.target, file_links)
315+
os_ext.copytree_virtual(self.prefix, self.target,
316+
file_links, dirs_exist_ok=True)
316317

317318
def test_virtual_copy_absolute_paths(self):
318319
file_links = [os.path.join(self.prefix, 'bar'),
319320
'foo/bar.txt', 'foo.txt']
320321
with pytest.raises(ValueError):
321-
os_ext.copytree_virtual(self.prefix, self.target, file_links)
322+
os_ext.copytree_virtual(self.prefix, self.target,
323+
file_links, dirs_exist_ok=True)
322324

323325
def test_virtual_copy_irrelevenant_paths(self):
324326
file_links = ['/bin', 'foo/bar.txt', 'foo.txt']
325327
with pytest.raises(ValueError):
326-
os_ext.copytree_virtual(self.prefix, self.target, file_links)
328+
os_ext.copytree_virtual(self.prefix, self.target,
329+
file_links, dirs_exist_ok=True)
327330

328331
file_links = [os.path.dirname(self.prefix), 'foo/bar.txt', 'foo.txt']
329332
with pytest.raises(ValueError):
330-
os_ext.copytree_virtual(self.prefix, self.target, file_links)
333+
os_ext.copytree_virtual(self.prefix, self.target,
334+
file_links, dirs_exist_ok=True)
331335

332336
def test_virtual_copy_linkself(self):
333337
file_links = ['.']
334-
with pytest.raises(OSError):
335-
os_ext.copytree_virtual(self.prefix, self.target, file_links)
338+
with pytest.raises(ValueError):
339+
os_ext.copytree_virtual(self.prefix, self.target,
340+
file_links, dirs_exist_ok=True)
341+
342+
def test_virtual_copy_linkparent(self):
343+
file_links = ['..']
344+
with pytest.raises(ValueError):
345+
os_ext.copytree_virtual(self.prefix, self.target,
346+
file_links, dirs_exist_ok=True)
336347

337348
def tearDown(self):
338349
shutil.rmtree(self.prefix)

0 commit comments

Comments
 (0)