Skip to content

Commit c070940

Browse files
committed
Initial commit with script to automatically update the lists of disabled tests
in the gfortran test suite.
1 parent 8352ecb commit c070940

File tree

1 file changed

+373
-0
lines changed

1 file changed

+373
-0
lines changed
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Run all the disabled tests in the gfortran test suite and re-enable those that
4+
# pass.
5+
#
6+
# USAGE
7+
#
8+
# update-disabled-tests [options] <path>
9+
#
10+
# ARGUMENTS
11+
#
12+
# <path> Path to the top-level directory of the LLVM test suite.
13+
#
14+
15+
import argparse
16+
import glob
17+
import json
18+
import os
19+
import re
20+
import shutil
21+
import subprocess
22+
import tempfile
23+
24+
def parse_cmdline_args():
25+
ap = argparse.ArgumentParser(
26+
prog = 'update-disabled-tests',
27+
description =
28+
'Run all disabled tests in the gfortran test suite and remove any that '
29+
'pass from the list of disabled tests',
30+
)
31+
ap.add_argument(
32+
'-b',
33+
'--build-dir',
34+
metavar = '<dir>',
35+
help = 'The build directory to use'
36+
)
37+
ap.add_argument(
38+
'-c',
39+
'--clean',
40+
default = False,
41+
action = 'store_true',
42+
help = 'If a build directory has been specified, delete everything in '
43+
'it. If this option is not provided, and the build directory is not '
44+
'empty, an error will be raised.'
45+
)
46+
ap.add_argument(
47+
'-i',
48+
'--inplace',
49+
default = False,
50+
action = 'store_true',
51+
help = 'Rewrite all DisabledFiles.cmake files. The original files will '
52+
'be saved as DisabledFiles.cmake.bak'
53+
)
54+
ap.add_argument(
55+
'-j',
56+
'--parallel',
57+
metavar = 'N',
58+
type = int,
59+
default = 1,
60+
help = 'The number of parallel threads to use when running the tests'
61+
)
62+
ap.add_argument(
63+
'--keep',
64+
default = False,
65+
action = 'store_true',
66+
help = 'Do not delete the build directory after building and running '
67+
'the tests'
68+
)
69+
ap.add_argument(
70+
'-n',
71+
'--dry-run',
72+
default = False,
73+
action = 'store_true',
74+
help = 'Only print the commands that will be executed. Do not make any '
75+
'directories or build/run any tests'
76+
)
77+
ap.add_argument(
78+
'-v',
79+
'--verbose',
80+
default = False,
81+
action = 'store_true',
82+
help = 'Enable verbose mode'
83+
)
84+
ap.add_argument(
85+
'--with-cmake',
86+
metavar = '<path>',
87+
type = str,
88+
default = 'cmake',
89+
dest = 'cmake',
90+
help = 'Path to cmake'
91+
)
92+
ap.add_argument(
93+
'--with-clang',
94+
metavar = '<path>',
95+
type = str,
96+
default = 'clang',
97+
dest = 'clang',
98+
help = 'Path to clang'
99+
)
100+
ap.add_argument(
101+
'--with-flang',
102+
metavar = '<path>',
103+
type = str,
104+
default = 'flang',
105+
dest = 'flang',
106+
help = 'Path to flang'
107+
)
108+
ap.add_argument(
109+
'--with-make',
110+
metavar = '<path>',
111+
type = str,
112+
default = 'make',
113+
dest = 'make',
114+
help = 'Path to make'
115+
)
116+
ap.add_argument(
117+
'--with-lit',
118+
metavar = '<path>',
119+
type = str,
120+
default = 'lit',
121+
dest = 'lit',
122+
help = 'Path to LLVM lit'
123+
)
124+
ap.add_argument(
125+
'source',
126+
metavar = '<source>',
127+
help = 'Path to the top-level source directory of the LLVM test suite',
128+
)
129+
130+
return ap.parse_args()
131+
132+
# Parse a file containing the disabled tests. For now, we only care about
133+
# the unimplemented (tests that are disabled because they trigger a "not yet
134+
# implemented" assertion), and skipped (tests that are skipped because they
135+
# fail without hitting any todo's or other assertions) list. The unsupported
136+
# list is irrelevant and the failing list needs a lot of additional processing
137+
# since the tests in that list fail for a variety of reasons.
138+
# { str : *}, os.path -> [str]
139+
def parse_disabled_file(args, filename):
140+
if args.verbose or args.dry_run:
141+
print('Parsing disabled tests file:', filename)
142+
if args.dry_run:
143+
return []
144+
145+
re_unimplemented = re.compile('^[ ]*file[ ]*[(][ ]*GLOB[ ]+UNIMPLEMENTED.+$')
146+
re_skipped = re.compile('^[ ]*file[ ]*[(][ ]*GLOB[ ]+SKIPPED.+$')
147+
re_comment = re.compile('^[ ]*#.*$')
148+
re_close = re.compile('^[ ]*[)][ ]*$')
149+
150+
d = os.path.dirname(filename)
151+
record = False
152+
tests = []
153+
with open(filename) as f:
154+
for line in f:
155+
if re_comment.match(line):
156+
pass
157+
elif re_unimplemented.match(line):
158+
record = True
159+
elif re_skipped.match(line):
160+
record = True
161+
elif re_close.match(line):
162+
record = False
163+
elif line.endswith(')'):
164+
tests.append(line[:-1].strip())
165+
record = False
166+
elif record:
167+
tests.append(line.strip())
168+
return tests
169+
170+
# Build the cmake command for the given configuration. config must be either
171+
# 'default' or 'all'. build_dir must be build directory for the configuration.
172+
# { str : * }, os.path, str -> str
173+
def get_configure_command(args, build_dir, config):
174+
return f'{args.cmake} ' \
175+
f'-B "{build_dir}" ' \
176+
f'-S "{args.source}" ' \
177+
f'-DCMAKE_C_COMPILER="{args.clang}" ' \
178+
f'-DCMAKE_CXX_COMPILER="{args.clang}++" ' \
179+
f'-DCMAKE_Fortran_COMPILER="{args.flang}" ' \
180+
f'-DTEST_SUITE_FORTRAN=ON ' \
181+
f'-DTEST_SUITE_SUBDIRS=Fortran/gfortran ' \
182+
f'-DTEST_SUITE_FORTRAN_FORCE_ALL_TESTS={"OFF" if config == "default" else "ON"}'
183+
184+
# Build the make command. build_dir must be the build directory for a specific
185+
# configuration. config must be either 'default' or 'all'.
186+
# { str: * }, os.path, str -> str
187+
def get_build_command(args, build_dir, config):
188+
cmd = [args.make, '-C', build_dir, '-j', str(args.parallel)]
189+
# When building all tests, some may fail at build time because the flang may
190+
# crash. We want to keep going in this case.
191+
if config == 'all':
192+
cmd.append('--ignore-errors')
193+
194+
return ' '.join(cmd)
195+
196+
# Build the run command for the given configuration. config must be either
197+
# 'default' or 'all'. build_dir must be the build directory for the
198+
# configuration.
199+
# { str : * }, os.path -> str
200+
def get_run_command(args, build_dir, config):
201+
build_root = os.path.dirname(build_dir)
202+
# Write the report to the top-level build directory.
203+
return f'{args.lit}' \
204+
f' -j {args.parallel} ' \
205+
f' -o "{build_root}/{config}.json" '\
206+
f' {build_dir}/Fortran/gfortran'
207+
208+
# Create a subdirectory within the build directory for the specified
209+
# configuration. config must be either 'default' or 'all'. The subdirectory must
210+
# not already exist.
211+
# { str : * }, os.path, str -> os.path
212+
def setup_build_dir_for_configuration(args, build_dir, config):
213+
if args.verbose or args.dry_run:
214+
print('Making build directory for configuration:', config)
215+
216+
d = os.path.join(build_dir, config)
217+
if not args.dry_run:
218+
if os.path.exists(d):
219+
print(
220+
f'ERROR: Build directory for configuration \'{config}\' exists'
221+
)
222+
exit(1)
223+
if not args.dry_run:
224+
os.mkdir(d)
225+
226+
if args.verbose or args.dry_run:
227+
print('Using build directory:', d)
228+
return d
229+
230+
# Check if the build directory is empty. Empty it if so instructed, raise an
231+
# error and exit otherwise.
232+
# { str : * }, os.path -> None
233+
def prepare_build_dir(args, build_dir):
234+
contents = os.listdir(build_dir)
235+
if len(contents) == 0:
236+
return
237+
238+
if not args.clean:
239+
print('ERROR: Build directory is not empty:', build_dir)
240+
exit(1)
241+
242+
if args.verbose or args.dry_run:
243+
print('Deleting contents of build directory:', build_dir)
244+
245+
if not args.dry_run:
246+
for f in contents:
247+
shutil.rmtree(f)
248+
249+
# Setup the build directory. This will create a temporary build directory if
250+
# a build directory was not explicitly specified. If a build directory was
251+
# explicitly specified, it is expected to exist.
252+
# { str : * } -> os.path
253+
def setup_build_dir(args):
254+
if args.build_dir:
255+
d = os.path.abspath(args.build_dir)
256+
if not os.path.exists(d):
257+
print('ERROR: Build directory does not exist:', d)
258+
exit(1)
259+
prepare_build_dir(args, d)
260+
else:
261+
d = tempfile.mkdtemp()
262+
if not os.path.exists(d):
263+
if args.verbose or args.dry_run:
264+
print('Making temporary build directory:', d)
265+
if not args.dry_run:
266+
os.mkdir(d)
267+
268+
if args.verbose or args.dry_run:
269+
print('Using build directory:', d)
270+
271+
return d
272+
273+
# Teardown the build directory if necessary. If the --keep option was not
274+
# specified and a temporary build directory was created, it will be deleted
275+
# along with all its contents. If an explicit build directory was specified, it
276+
# will not be deleted.
277+
# { str : * } -> None
278+
def teardown_build_dir(args, d):
279+
if not args.build_dir and not args.keep:
280+
if args.verbose or args.dry_run:
281+
print('Removing build directory:', d)
282+
if not args.dry_run:
283+
shutil.rmtree(d)
284+
285+
# Build and run the tests using the given commands. Return the process' return
286+
# code. build_root is the top-level build directory. config must be either
287+
# 'default' or 'all'.
288+
# { str : * }, os.path, str -> None
289+
def build_and_run_tests(args, build_root, config):
290+
build_dir = setup_build_dir_for_configuration(args, build_root, config)
291+
configure = get_configure_command(args, build_dir, config)
292+
build = get_build_command(args, build_dir, config)
293+
run = get_run_command(args, build_dir, config)
294+
295+
if args.verbose or args.dry_run:
296+
print(configure)
297+
if not args.dry_run:
298+
if subprocess.run(configure, shell = True).returncode != 0:
299+
print('ERROR: Error configuring test suite')
300+
exit(1)
301+
302+
if args.verbose or args.dry_run:
303+
print(build)
304+
if not args.dry_run:
305+
if subprocess.run(build, shell = True).returncode != 0:
306+
# When building all the tests, we may encounter errors because flang
307+
# may crash. We want to keep going regardless.
308+
if config != 'all':
309+
print('ERROR: Error building test suite')
310+
exit(1)
311+
312+
if args.verbose or args.dry_run:
313+
print(run)
314+
if not args.dry_run:
315+
ret = subprocess.run(run, cwd = build_dir, shell = True)
316+
# The default configuration is expected to pass. If it doesn't,
317+
# something is wrong. Don't proceed any further. The 'all' configuration
318+
# is expected to fail, so just ignore the return code there.
319+
if ret.returncode != 0 and config != 'all':
320+
print(
321+
'ERROR: Some test(s) in the default configuration failed. '
322+
'This is not expected'
323+
)
324+
exit(1)
325+
326+
# Compare the test results and get the list of tests to be enabled. The result
327+
# will be a list of paths to tests that should be enabled.
328+
# os.path -> [os.path]
329+
def get_tests_to_be_enabled(build_dir):
330+
# TODO: Implement this.
331+
return []
332+
333+
# Update the DisabledFiles.cmake files based on the tests that were disabled
334+
# but should now be enabled.
335+
# { str : * }, [os.path] -> None
336+
def update_disabled_lists(args, enabled_tests):
337+
# TODO: Implement this.
338+
pass
339+
340+
def main():
341+
args = parse_cmdline_args()
342+
343+
# Parse disabled files lists.
344+
check_disabled = {}
345+
pattern = os.path.join(args.source, '**/DisabledFiles.cmake')
346+
for filename in glob.iglob(pattern, recursive=True):
347+
d = os.path.dirname(filename)
348+
check_disabled[d] = []
349+
for test in parse_disabled_file(args, filename):
350+
check_disabled[d].append(os.path.join(d, test))
351+
352+
print(len(check_disabled))
353+
for d, tests in check_disabled.items():
354+
print(d, len(tests))
355+
exit(0)
356+
build_dir = setup_build_dir(args)
357+
358+
build_and_run_tests(args, build_dir, 'default')
359+
build_and_run_tests(args, build_dir, 'all')
360+
361+
enabled = get_tests_to_be_enabled(build_dir)
362+
if args.verbose or args.dry_run:
363+
print(f'Enabling {len(enabled)} tests')
364+
for test in enabled:
365+
print(' ', test)
366+
if not args.dry_run:
367+
update_disabled_lists(args, enabled_tests)
368+
369+
# Cleanup, if necessary.
370+
teardown_build_dir(args, build_dir)
371+
372+
if __name__ == '__main__':
373+
main()

0 commit comments

Comments
 (0)