xref: /illumos-gate/usr/src/tools/scripts/check_test_runfiles.py (revision 5d4c552b8501980de62d4e33e1c534f14069a80e)
1e0416ec8SGordon Ross#!@TOOLS_PYTHON@ -Es
2e0416ec8SGordon Ross#
3e0416ec8SGordon Ross# This file and its contents are supplied under the terms of the
4e0416ec8SGordon Ross# Common Development and Distribution License ("CDDL"), version 1.0.
5e0416ec8SGordon Ross# You may only use this file in accordance with the terms of version
6e0416ec8SGordon Ross# 1.0 of the CDDL.
7e0416ec8SGordon Ross#
8e0416ec8SGordon Ross# A full copy of the text of the CDDL should have accompanied this
9e0416ec8SGordon Ross# source.  A copy of the CDDL is also available via the Internet at
10e0416ec8SGordon Ross# http://www.illumos.org/license/CDDL.
11e0416ec8SGordon Ross#
12e0416ec8SGordon Ross
13e0416ec8SGordon Ross#
14e0416ec8SGordon Ross# Copyright 2026 Gordon W. Ross
15e0416ec8SGordon Ross#
16e0416ec8SGordon Ross
17e0416ec8SGordon Ross#
18e0416ec8SGordon Ross# Check test runfiles against installed test artifacts in proto.
19e0416ec8SGordon Ross#
20e0416ec8SGordon Ross# For each section in a runfile, verify that the test group path,
21e0416ec8SGordon Ross# the tests (if listed), and any pre/post auxiliary scripts exist
22e0416ec8SGordon Ross# in the proto area and are executable.  Auxiliary scripts are
23e0416ec8SGordon Ross# allowed to be outside the test group's own directory.
24e0416ec8SGordon Ross#
25*5d4c552bSGordon Ross# Note that some tests might not run on the target system for which
26*5d4c552bSGordon Ross# this workspace is building.  Some runfiles have test groups that
27*5d4c552bSGordon Ross# specify an architecture like "arch=i86pc" and run only there.
28*5d4c552bSGordon Ross# A runfile test group runs on the target if the "arch=" value is
29*5d4c552bSGordon Ross# absent, or if the "arch" value matches the target architecture.
30*5d4c552bSGordon Ross#
31*5d4c552bSGordon Ross# For the build-time checks this runs, assume the build has all
32*5d4c552bSGordon Ross# architectures for a given MACH (eg. i386/amd64 builds for all of
33*5d4c552bSGordon Ross# i86pc i86xpv intel) and check for existence of test programs
34*5d4c552bSGordon Ross# when the runfile arch matches any of the architectures that
35*5d4c552bSGordon Ross# should exist in this build.
36e0416ec8SGordon Ross
37e0416ec8SGordon Rossimport argparse
38e0416ec8SGordon Rossimport ast
39e0416ec8SGordon Rossimport configparser
40e0416ec8SGordon Rossimport io
41e0416ec8SGordon Rossimport os
42e0416ec8SGordon Rossimport posixpath
43e0416ec8SGordon Rossimport re
44e0416ec8SGordon Rossimport sys
45e0416ec8SGordon Rossimport tokenize
46e0416ec8SGordon Ross
47e0416ec8SGordon RossSECTION_RE = re.compile(r'^\s*\[(.+?)\]\s*$')
48e0416ec8SGordon Ross
49*5d4c552bSGordon Ross# Maps MACH (build ISA) to the set of architecture names that may appear
50*5d4c552bSGordon Ross# in runfile "arch=" properties for that build.  Derived from the
51*5d4c552bSGordon Ross# *_ARCHITECTURES variables in usr/src/uts/Makefile.
52*5d4c552bSGordon RossMACH_ARCHITECTURES = {
53*5d4c552bSGordon Ross    'i386':  {'i86pc', 'i86xpv', 'intel'},
54*5d4c552bSGordon Ross    'sparc': {'sun4v', 'sun4u', 'sparc'},
55*5d4c552bSGordon Ross}
56*5d4c552bSGordon Ross
57e0416ec8SGordon Ross
58e0416ec8SGordon Rossdef parse_args():
59e0416ec8SGordon Ross    parser = argparse.ArgumentParser(
60e0416ec8SGordon Ross        description='Validate test runfiles against proto/root_i386 installs')
61e0416ec8SGordon Ross    parser.add_argument(
62e0416ec8SGordon Ross        '-R', dest='root', default=os.environ.get('ROOT'),
63e0416ec8SGordon Ross        help='proto root path (for example, .../proto/root_i386); '
64e0416ec8SGordon Ross             'default: $ROOT')
65e0416ec8SGordon Ross    parser.add_argument(
66e0416ec8SGordon Ross        '-T', dest='testroot', required=True,
67e0416ec8SGordon Ross        help='test root relative to proto root (for example, opt/os-tests)')
68e0416ec8SGordon Ross    parser.add_argument(
69*5d4c552bSGordon Ross        '-m', '--mach', default=None,
70*5d4c552bSGordon Ross        help='target CPU type (for example, i386 or sparc); used to determine '
71*5d4c552bSGordon Ross             'which arch-constrained sections to check')
72e0416ec8SGordon Ross    parser.add_argument(
73e0416ec8SGordon Ross        'runfiles', nargs='+',
74e0416ec8SGordon Ross        help='runfile paths to check')
75e0416ec8SGordon Ross
76e0416ec8SGordon Ross    args = parser.parse_args()
77e0416ec8SGordon Ross    if args.root is None:
78e0416ec8SGordon Ross        parser.error('missing proto root: set -R <root> or the ROOT '
79e0416ec8SGordon Ross                     'environment variable')
80e0416ec8SGordon Ross    return args
81e0416ec8SGordon Ross
82e0416ec8SGordon Ross
83e0416ec8SGordon Rossdef find_runfiles(args):
84e0416ec8SGordon Ross    return list(args.runfiles)
85e0416ec8SGordon Ross
86e0416ec8SGordon Ross
87e0416ec8SGordon Rossdef normalize_testroot(testroot):
88e0416ec8SGordon Ross    root = testroot.strip()
89e0416ec8SGordon Ross    if not root:
90e0416ec8SGordon Ross        return None
91e0416ec8SGordon Ross    if root.startswith('/'):
92e0416ec8SGordon Ross        return None
93e0416ec8SGordon Ross    root = posixpath.normpath(root)
94e0416ec8SGordon Ross    if root in ('.', '..') or root.startswith('../'):
95e0416ec8SGordon Ross        return None
96e0416ec8SGordon Ross    return root
97e0416ec8SGordon Ross
98e0416ec8SGordon Ross
99e0416ec8SGordon Rossdef section_path(section, testroot):
100e0416ec8SGordon Ross    raw = section.split(':', 1)[0].strip()
101e0416ec8SGordon Ross    if raw.startswith('/'):
102e0416ec8SGordon Ross        return posixpath.normpath(raw)
103e0416ec8SGordon Ross    return posixpath.normpath(posixpath.join('/', testroot, raw))
104e0416ec8SGordon Ross
105e0416ec8SGordon Ross
106e0416ec8SGordon Rossdef section_raw_path(section):
107e0416ec8SGordon Ross    return section.split(':', 1)[0].strip()
108e0416ec8SGordon Ross
109e0416ec8SGordon Ross
110e0416ec8SGordon Rossdef is_test_file(path):
111e0416ec8SGordon Ross    return (os.path.isfile(path) and not os.path.islink(path) and
112e0416ec8SGordon Ross            os.access(path, os.X_OK))
113e0416ec8SGordon Ross
114e0416ec8SGordon Ross
115e0416ec8SGordon Rossdef has_adjacent_string_literals(expr):
116e0416ec8SGordon Ross    try:
117e0416ec8SGordon Ross        toks = tokenize.generate_tokens(io.StringIO(expr).readline)
118e0416ec8SGordon Ross    except tokenize.TokenError:
119e0416ec8SGordon Ross        return False
120e0416ec8SGordon Ross
121e0416ec8SGordon Ross    prev = None
122e0416ec8SGordon Ross    ignored = set([tokenize.NL, tokenize.NEWLINE, tokenize.INDENT,
123e0416ec8SGordon Ross                   tokenize.DEDENT, tokenize.COMMENT, tokenize.ENDMARKER])
124e0416ec8SGordon Ross    for tok in toks:
125e0416ec8SGordon Ross        if tok.type in ignored:
126e0416ec8SGordon Ross            continue
127e0416ec8SGordon Ross        if tok.type == tokenize.STRING and prev == tokenize.STRING:
128e0416ec8SGordon Ross            return True
129e0416ec8SGordon Ross        prev = tok.type
130e0416ec8SGordon Ross    return False
131e0416ec8SGordon Ross
132e0416ec8SGordon Ross
133e0416ec8SGordon Rossdef section_line_map(content):
134e0416ec8SGordon Ross    lines = {}
135e0416ec8SGordon Ross    for lineno, line in enumerate(content.splitlines(), 1):
136e0416ec8SGordon Ross        match = SECTION_RE.match(line)
137e0416ec8SGordon Ross        if match is None:
138e0416ec8SGordon Ross            continue
139e0416ec8SGordon Ross        section = match.group(1).strip()
140e0416ec8SGordon Ross        if section not in lines:
141e0416ec8SGordon Ross            lines[section] = lineno
142e0416ec8SGordon Ross    return lines
143e0416ec8SGordon Ross
144e0416ec8SGordon Ross
145e0416ec8SGordon Rossdef format_error(runfile, lineno, test_group, detail):
146e0416ec8SGordon Ross    loc = '%s:%s' % (runfile, lineno)
147e0416ec8SGordon Ross    return 'In test group %s, %s: %s' % (test_group, loc, detail)
148e0416ec8SGordon Ross
149e0416ec8SGordon Ross
150e0416ec8SGordon Rossdef emit_error(issues, runfile, lineno, test_group, detail):
151e0416ec8SGordon Ross    issues.append(format_error(runfile, lineno, test_group, detail))
152e0416ec8SGordon Ross
153e0416ec8SGordon Ross
154*5d4c552bSGordon Rossdef check_runfile(runfile, testroot, protoroot, archlist):
155e0416ec8SGordon Ross    issues = []
156e0416ec8SGordon Ross    config = configparser.RawConfigParser()
157e0416ec8SGordon Ross    content = None
158e0416ec8SGordon Ross
159e0416ec8SGordon Ross    try:
160e0416ec8SGordon Ross        with open(runfile, encoding='utf-8') as f:
161e0416ec8SGordon Ross            content = f.read()
162e0416ec8SGordon Ross    except OSError as err:
163e0416ec8SGordon Ross        return ['%s: failed to read runfile: %s' % (runfile, err)]
164e0416ec8SGordon Ross
165e0416ec8SGordon Ross    section_lines = section_line_map(content)
166e0416ec8SGordon Ross
167e0416ec8SGordon Ross    try:
168e0416ec8SGordon Ross        config.read_string(content, source=runfile)
169e0416ec8SGordon Ross    except configparser.Error as err:
170e0416ec8SGordon Ross        return ['%s: parse error: %s' % (os.path.basename(runfile), err)]
171e0416ec8SGordon Ross
172e0416ec8SGordon Ross    for sec in config.sections():
173e0416ec8SGordon Ross        lineno = section_lines.get(sec, '?')
174e0416ec8SGordon Ross        if section_raw_path(sec).upper() == 'DEFAULT':
175e0416ec8SGordon Ross            continue
176e0416ec8SGordon Ross        if config.has_option(sec, 'arch'):
177e0416ec8SGordon Ross            sec_arch = config.get(sec, 'arch').strip()
178*5d4c552bSGordon Ross            if sec_arch not in archlist:
179e0416ec8SGordon Ross                continue
180*5d4c552bSGordon Ross        # else arch not specified so do checks.
181e0416ec8SGordon Ross
182e0416ec8SGordon Ross        secpath = section_path(sec, testroot)
183e0416ec8SGordon Ross        proto_path = os.path.join(protoroot, secpath.lstrip('/'))
184e0416ec8SGordon Ross        has_tests = config.has_option(sec, 'tests')
185e0416ec8SGordon Ross        has_autotests = config.has_option(sec, 'autotests')
186e0416ec8SGordon Ross
187e0416ec8SGordon Ross        if has_tests or has_autotests:
188e0416ec8SGordon Ross            path_exists = os.path.exists(proto_path)
189e0416ec8SGordon Ross            path_is_dir = os.path.isdir(proto_path)
190e0416ec8SGordon Ross            path_is_file = os.path.isfile(proto_path)
191e0416ec8SGordon Ross
192e0416ec8SGordon Ross            if not path_exists:
193e0416ec8SGordon Ross                emit_error(issues, runfile, lineno, secpath,
194e0416ec8SGordon Ross                           'test group path not found in proto area.')
195e0416ec8SGordon Ross            elif path_is_file:
196e0416ec8SGordon Ross                if has_tests:
197e0416ec8SGordon Ross                    emit_error(
198e0416ec8SGordon Ross                        issues, runfile, lineno, secpath,
199e0416ec8SGordon Ross                        'test group path is a file and should not have a '
200e0416ec8SGordon Ross                        'tests property.')
201e0416ec8SGordon Ross                else:
202e0416ec8SGordon Ross                    emit_error(
203e0416ec8SGordon Ross                        issues, runfile, lineno, secpath,
204e0416ec8SGordon Ross                        'test group path is a file and should not have an '
205e0416ec8SGordon Ross                        'autotests property.')
206e0416ec8SGordon Ross
207e0416ec8SGordon Ross            if has_tests:
208e0416ec8SGordon Ross                tests_raw = config.get(sec, 'tests')
209e0416ec8SGordon Ross                if has_adjacent_string_literals(tests_raw):
210e0416ec8SGordon Ross                    emit_error(
211e0416ec8SGordon Ross                        issues, runfile, lineno, secpath,
212e0416ec8SGordon Ross                        'tests list contains adjacent string literals '
213e0416ec8SGordon Ross                        '(possible missing comma).')
214e0416ec8SGordon Ross
215e0416ec8SGordon Ross                try:
216e0416ec8SGordon Ross                    tests = ast.literal_eval(tests_raw)
217e0416ec8SGordon Ross                except (SyntaxError, ValueError) as err:
218e0416ec8SGordon Ross                    emit_error(
219e0416ec8SGordon Ross                        issues, runfile, lineno, secpath,
220e0416ec8SGordon Ross                        'tests is not a valid Python list: %s' % err)
221e0416ec8SGordon Ross                    continue
222e0416ec8SGordon Ross
223e0416ec8SGordon Ross                if not isinstance(tests, list):
224e0416ec8SGordon Ross                    emit_error(issues, runfile, lineno, secpath,
225e0416ec8SGordon Ross                               'tests must evaluate to a list.')
226e0416ec8SGordon Ross                    continue
227e0416ec8SGordon Ross
228e0416ec8SGordon Ross                bad = [repr(x) for x in tests if not isinstance(x, str)]
229e0416ec8SGordon Ross                if bad:
230e0416ec8SGordon Ross                    emit_error(issues, runfile, lineno, secpath,
231e0416ec8SGordon Ross                               'tests must contain only strings: %s' %
232e0416ec8SGordon Ross                               ', '.join(bad))
233e0416ec8SGordon Ross                    continue
234e0416ec8SGordon Ross
235e0416ec8SGordon Ross                if not path_is_dir:
236e0416ec8SGordon Ross                    continue
237e0416ec8SGordon Ross
238e0416ec8SGordon Ross                for test in tests:
239e0416ec8SGordon Ross                    tpath = os.path.join(proto_path, test)
240e0416ec8SGordon Ross                    if not is_test_file(tpath):
241e0416ec8SGordon Ross                        emit_error(issues, runfile, lineno, secpath,
242e0416ec8SGordon Ross                                   'test %s not found in proto area.' % test)
243e0416ec8SGordon Ross        else:
244e0416ec8SGordon Ross            if not is_test_file(proto_path):
245e0416ec8SGordon Ross                emit_error(issues, runfile, lineno, secpath,
246e0416ec8SGordon Ross                           'single test path not found in proto area.')
247e0416ec8SGordon Ross
248e0416ec8SGordon Ross        # Check pre/post auxiliary scripts. They are allowed to live in any
249e0416ec8SGordon Ross        # directory, so we only verify they exist in the proto area.
250e0416ec8SGordon Ross        aux_dir = secpath if (has_tests or has_autotests) \
251e0416ec8SGordon Ross            else posixpath.dirname(secpath)
252e0416ec8SGordon Ross        for prop in ('pre', 'post'):
253e0416ec8SGordon Ross            if not config.has_option(sec, prop):
254e0416ec8SGordon Ross                continue
255e0416ec8SGordon Ross            val = config.get(sec, prop).strip()
256e0416ec8SGordon Ross            if not val:
257e0416ec8SGordon Ross                continue
258e0416ec8SGordon Ross            if posixpath.isabs(val):
259e0416ec8SGordon Ross                apath = os.path.join(protoroot, val.lstrip('/'))
260e0416ec8SGordon Ross            else:
261e0416ec8SGordon Ross                apath = os.path.join(protoroot, aux_dir.lstrip('/'), val)
262e0416ec8SGordon Ross            if not is_test_file(apath):
263e0416ec8SGordon Ross                emit_error(issues, runfile, lineno, secpath,
264e0416ec8SGordon Ross                           '%s script not found in proto area: %s' %
265e0416ec8SGordon Ross                           (prop, val))
266e0416ec8SGordon Ross
267e0416ec8SGordon Ross    return issues
268e0416ec8SGordon Ross
269e0416ec8SGordon Ross
270e0416ec8SGordon Rossdef main():
271e0416ec8SGordon Ross    args = parse_args()
272e0416ec8SGordon Ross    protoroot = os.path.abspath(os.path.expanduser(args.root))
273e0416ec8SGordon Ross    testroot = normalize_testroot(args.testroot)
274e0416ec8SGordon Ross
275e0416ec8SGordon Ross    if not os.path.isdir(protoroot):
276e0416ec8SGordon Ross        sys.stderr.write('error: proto root not found: %s\n' % protoroot)
277e0416ec8SGordon Ross        return 2
278e0416ec8SGordon Ross
279e0416ec8SGordon Ross    if not os.path.isdir(os.path.join(protoroot, 'opt')):
280e0416ec8SGordon Ross        sys.stderr.write('error: invalid proto root (missing opt/): %s\n' %
281e0416ec8SGordon Ross                         protoroot)
282e0416ec8SGordon Ross        return 2
283e0416ec8SGordon Ross
284e0416ec8SGordon Ross    if testroot is None:
285e0416ec8SGordon Ross        sys.stderr.write('error: invalid -T value (must be a relative path '
286e0416ec8SGordon Ross                         'under proto root): %s\n' % args.testroot)
287e0416ec8SGordon Ross        return 2
288e0416ec8SGordon Ross
289e0416ec8SGordon Ross    testroot_path = os.path.join(protoroot, testroot)
290e0416ec8SGordon Ross    if not os.path.isdir(testroot_path):
291e0416ec8SGordon Ross        sys.stderr.write('error: test root not found under proto root: %s\n' %
292e0416ec8SGordon Ross                         testroot_path)
293e0416ec8SGordon Ross        return 2
294e0416ec8SGordon Ross
295e0416ec8SGordon Ross    runfiles = find_runfiles(args)
296*5d4c552bSGordon Ross    archlist = MACH_ARCHITECTURES.get(args.mach, set())
297e0416ec8SGordon Ross    issues = []
298e0416ec8SGordon Ross    for runfile in runfiles:
299*5d4c552bSGordon Ross        issues.extend(check_runfile(runfile, testroot, protoroot, archlist))
300e0416ec8SGordon Ross
301e0416ec8SGordon Ross    for issue in issues:
302e0416ec8SGordon Ross        sys.stderr.write('%s\n' % issue)
303e0416ec8SGordon Ross
304e0416ec8SGordon Ross    return 1 if issues else 0
305e0416ec8SGordon Ross
306e0416ec8SGordon Ross
307e0416ec8SGordon Rossif __name__ == '__main__':
308e0416ec8SGordon Ross    sys.exit(main())
309