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