1*62c56f98SSadaf Ebrahimi#!/usr/bin/env python3 2*62c56f98SSadaf Ebrahimi 3*62c56f98SSadaf Ebrahimi"""Sanity checks for test data. 4*62c56f98SSadaf Ebrahimi 5*62c56f98SSadaf EbrahimiThis program contains a class for traversing test cases that can be used 6*62c56f98SSadaf Ebrahimiindependently of the checks. 7*62c56f98SSadaf Ebrahimi""" 8*62c56f98SSadaf Ebrahimi 9*62c56f98SSadaf Ebrahimi# Copyright The Mbed TLS Contributors 10*62c56f98SSadaf Ebrahimi# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 11*62c56f98SSadaf Ebrahimi 12*62c56f98SSadaf Ebrahimiimport argparse 13*62c56f98SSadaf Ebrahimiimport glob 14*62c56f98SSadaf Ebrahimiimport os 15*62c56f98SSadaf Ebrahimiimport re 16*62c56f98SSadaf Ebrahimiimport subprocess 17*62c56f98SSadaf Ebrahimiimport sys 18*62c56f98SSadaf Ebrahimi 19*62c56f98SSadaf Ebrahimiclass Results: 20*62c56f98SSadaf Ebrahimi """Store file and line information about errors or warnings in test suites.""" 21*62c56f98SSadaf Ebrahimi 22*62c56f98SSadaf Ebrahimi def __init__(self, options): 23*62c56f98SSadaf Ebrahimi self.errors = 0 24*62c56f98SSadaf Ebrahimi self.warnings = 0 25*62c56f98SSadaf Ebrahimi self.ignore_warnings = options.quiet 26*62c56f98SSadaf Ebrahimi 27*62c56f98SSadaf Ebrahimi def error(self, file_name, line_number, fmt, *args): 28*62c56f98SSadaf Ebrahimi sys.stderr.write(('{}:{}:ERROR:' + fmt + '\n'). 29*62c56f98SSadaf Ebrahimi format(file_name, line_number, *args)) 30*62c56f98SSadaf Ebrahimi self.errors += 1 31*62c56f98SSadaf Ebrahimi 32*62c56f98SSadaf Ebrahimi def warning(self, file_name, line_number, fmt, *args): 33*62c56f98SSadaf Ebrahimi if not self.ignore_warnings: 34*62c56f98SSadaf Ebrahimi sys.stderr.write(('{}:{}:Warning:' + fmt + '\n') 35*62c56f98SSadaf Ebrahimi .format(file_name, line_number, *args)) 36*62c56f98SSadaf Ebrahimi self.warnings += 1 37*62c56f98SSadaf Ebrahimi 38*62c56f98SSadaf Ebrahimiclass TestDescriptionExplorer: 39*62c56f98SSadaf Ebrahimi """An iterator over test cases with descriptions. 40*62c56f98SSadaf Ebrahimi 41*62c56f98SSadaf EbrahimiThe test cases that have descriptions are: 42*62c56f98SSadaf Ebrahimi* Individual unit tests (entries in a .data file) in test suites. 43*62c56f98SSadaf Ebrahimi* Individual test cases in ssl-opt.sh. 44*62c56f98SSadaf Ebrahimi 45*62c56f98SSadaf EbrahimiThis is an abstract class. To use it, derive a class that implements 46*62c56f98SSadaf Ebrahimithe process_test_case method, and call walk_all(). 47*62c56f98SSadaf Ebrahimi""" 48*62c56f98SSadaf Ebrahimi 49*62c56f98SSadaf Ebrahimi def process_test_case(self, per_file_state, 50*62c56f98SSadaf Ebrahimi file_name, line_number, description): 51*62c56f98SSadaf Ebrahimi """Process a test case. 52*62c56f98SSadaf Ebrahimi 53*62c56f98SSadaf Ebrahimiper_file_state: an object created by new_per_file_state() at the beginning 54*62c56f98SSadaf Ebrahimi of each file. 55*62c56f98SSadaf Ebrahimifile_name: a relative path to the file containing the test case. 56*62c56f98SSadaf Ebrahimiline_number: the line number in the given file. 57*62c56f98SSadaf Ebrahimidescription: the test case description as a byte string. 58*62c56f98SSadaf Ebrahimi""" 59*62c56f98SSadaf Ebrahimi raise NotImplementedError 60*62c56f98SSadaf Ebrahimi 61*62c56f98SSadaf Ebrahimi def new_per_file_state(self): 62*62c56f98SSadaf Ebrahimi """Return a new per-file state object. 63*62c56f98SSadaf Ebrahimi 64*62c56f98SSadaf EbrahimiThe default per-file state object is None. Child classes that require per-file 65*62c56f98SSadaf Ebrahimistate may override this method. 66*62c56f98SSadaf Ebrahimi""" 67*62c56f98SSadaf Ebrahimi #pylint: disable=no-self-use 68*62c56f98SSadaf Ebrahimi return None 69*62c56f98SSadaf Ebrahimi 70*62c56f98SSadaf Ebrahimi def walk_test_suite(self, data_file_name): 71*62c56f98SSadaf Ebrahimi """Iterate over the test cases in the given unit test data file.""" 72*62c56f98SSadaf Ebrahimi in_paragraph = False 73*62c56f98SSadaf Ebrahimi descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none 74*62c56f98SSadaf Ebrahimi with open(data_file_name, 'rb') as data_file: 75*62c56f98SSadaf Ebrahimi for line_number, line in enumerate(data_file, 1): 76*62c56f98SSadaf Ebrahimi line = line.rstrip(b'\r\n') 77*62c56f98SSadaf Ebrahimi if not line: 78*62c56f98SSadaf Ebrahimi in_paragraph = False 79*62c56f98SSadaf Ebrahimi continue 80*62c56f98SSadaf Ebrahimi if line.startswith(b'#'): 81*62c56f98SSadaf Ebrahimi continue 82*62c56f98SSadaf Ebrahimi if not in_paragraph: 83*62c56f98SSadaf Ebrahimi # This is a test case description line. 84*62c56f98SSadaf Ebrahimi self.process_test_case(descriptions, 85*62c56f98SSadaf Ebrahimi data_file_name, line_number, line) 86*62c56f98SSadaf Ebrahimi in_paragraph = True 87*62c56f98SSadaf Ebrahimi 88*62c56f98SSadaf Ebrahimi def walk_ssl_opt_sh(self, file_name): 89*62c56f98SSadaf Ebrahimi """Iterate over the test cases in ssl-opt.sh or a file with a similar format.""" 90*62c56f98SSadaf Ebrahimi descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none 91*62c56f98SSadaf Ebrahimi with open(file_name, 'rb') as file_contents: 92*62c56f98SSadaf Ebrahimi for line_number, line in enumerate(file_contents, 1): 93*62c56f98SSadaf Ebrahimi # Assume that all run_test calls have the same simple form 94*62c56f98SSadaf Ebrahimi # with the test description entirely on the same line as the 95*62c56f98SSadaf Ebrahimi # function name. 96*62c56f98SSadaf Ebrahimi m = re.match(br'\s*run_test\s+"((?:[^\\"]|\\.)*)"', line) 97*62c56f98SSadaf Ebrahimi if not m: 98*62c56f98SSadaf Ebrahimi continue 99*62c56f98SSadaf Ebrahimi description = m.group(1) 100*62c56f98SSadaf Ebrahimi self.process_test_case(descriptions, 101*62c56f98SSadaf Ebrahimi file_name, line_number, description) 102*62c56f98SSadaf Ebrahimi 103*62c56f98SSadaf Ebrahimi def walk_compat_sh(self, file_name): 104*62c56f98SSadaf Ebrahimi """Iterate over the test cases compat.sh with a similar format.""" 105*62c56f98SSadaf Ebrahimi descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none 106*62c56f98SSadaf Ebrahimi compat_cmd = ['sh', file_name, '--list-test-case'] 107*62c56f98SSadaf Ebrahimi compat_output = subprocess.check_output(compat_cmd) 108*62c56f98SSadaf Ebrahimi # Assume compat.sh is responsible for printing identical format of 109*62c56f98SSadaf Ebrahimi # test case description between --list-test-case and its OUTCOME.CSV 110*62c56f98SSadaf Ebrahimi description = compat_output.strip().split(b'\n') 111*62c56f98SSadaf Ebrahimi # idx indicates the number of test case since there is no line number 112*62c56f98SSadaf Ebrahimi # in `compat.sh` for each test case. 113*62c56f98SSadaf Ebrahimi for idx, descrip in enumerate(description): 114*62c56f98SSadaf Ebrahimi self.process_test_case(descriptions, file_name, idx, descrip) 115*62c56f98SSadaf Ebrahimi 116*62c56f98SSadaf Ebrahimi @staticmethod 117*62c56f98SSadaf Ebrahimi def collect_test_directories(): 118*62c56f98SSadaf Ebrahimi """Get the relative path for the TLS and Crypto test directories.""" 119*62c56f98SSadaf Ebrahimi if os.path.isdir('tests'): 120*62c56f98SSadaf Ebrahimi tests_dir = 'tests' 121*62c56f98SSadaf Ebrahimi elif os.path.isdir('suites'): 122*62c56f98SSadaf Ebrahimi tests_dir = '.' 123*62c56f98SSadaf Ebrahimi elif os.path.isdir('../suites'): 124*62c56f98SSadaf Ebrahimi tests_dir = '..' 125*62c56f98SSadaf Ebrahimi directories = [tests_dir] 126*62c56f98SSadaf Ebrahimi return directories 127*62c56f98SSadaf Ebrahimi 128*62c56f98SSadaf Ebrahimi def walk_all(self): 129*62c56f98SSadaf Ebrahimi """Iterate over all named test cases.""" 130*62c56f98SSadaf Ebrahimi test_directories = self.collect_test_directories() 131*62c56f98SSadaf Ebrahimi for directory in test_directories: 132*62c56f98SSadaf Ebrahimi for data_file_name in glob.glob(os.path.join(directory, 'suites', 133*62c56f98SSadaf Ebrahimi '*.data')): 134*62c56f98SSadaf Ebrahimi self.walk_test_suite(data_file_name) 135*62c56f98SSadaf Ebrahimi ssl_opt_sh = os.path.join(directory, 'ssl-opt.sh') 136*62c56f98SSadaf Ebrahimi if os.path.exists(ssl_opt_sh): 137*62c56f98SSadaf Ebrahimi self.walk_ssl_opt_sh(ssl_opt_sh) 138*62c56f98SSadaf Ebrahimi for ssl_opt_file_name in glob.glob(os.path.join(directory, 'opt-testcases', 139*62c56f98SSadaf Ebrahimi '*.sh')): 140*62c56f98SSadaf Ebrahimi self.walk_ssl_opt_sh(ssl_opt_file_name) 141*62c56f98SSadaf Ebrahimi compat_sh = os.path.join(directory, 'compat.sh') 142*62c56f98SSadaf Ebrahimi if os.path.exists(compat_sh): 143*62c56f98SSadaf Ebrahimi self.walk_compat_sh(compat_sh) 144*62c56f98SSadaf Ebrahimi 145*62c56f98SSadaf Ebrahimiclass TestDescriptions(TestDescriptionExplorer): 146*62c56f98SSadaf Ebrahimi """Collect the available test cases.""" 147*62c56f98SSadaf Ebrahimi 148*62c56f98SSadaf Ebrahimi def __init__(self): 149*62c56f98SSadaf Ebrahimi super().__init__() 150*62c56f98SSadaf Ebrahimi self.descriptions = set() 151*62c56f98SSadaf Ebrahimi 152*62c56f98SSadaf Ebrahimi def process_test_case(self, _per_file_state, 153*62c56f98SSadaf Ebrahimi file_name, _line_number, description): 154*62c56f98SSadaf Ebrahimi """Record an available test case.""" 155*62c56f98SSadaf Ebrahimi base_name = re.sub(r'\.[^.]*$', '', re.sub(r'.*/', '', file_name)) 156*62c56f98SSadaf Ebrahimi key = ';'.join([base_name, description.decode('utf-8')]) 157*62c56f98SSadaf Ebrahimi self.descriptions.add(key) 158*62c56f98SSadaf Ebrahimi 159*62c56f98SSadaf Ebrahimidef collect_available_test_cases(): 160*62c56f98SSadaf Ebrahimi """Collect the available test cases.""" 161*62c56f98SSadaf Ebrahimi explorer = TestDescriptions() 162*62c56f98SSadaf Ebrahimi explorer.walk_all() 163*62c56f98SSadaf Ebrahimi return sorted(explorer.descriptions) 164*62c56f98SSadaf Ebrahimi 165*62c56f98SSadaf Ebrahimiclass DescriptionChecker(TestDescriptionExplorer): 166*62c56f98SSadaf Ebrahimi """Check all test case descriptions. 167*62c56f98SSadaf Ebrahimi 168*62c56f98SSadaf Ebrahimi* Check that each description is valid (length, allowed character set, etc.). 169*62c56f98SSadaf Ebrahimi* Check that there is no duplicated description inside of one test suite. 170*62c56f98SSadaf Ebrahimi""" 171*62c56f98SSadaf Ebrahimi 172*62c56f98SSadaf Ebrahimi def __init__(self, results): 173*62c56f98SSadaf Ebrahimi self.results = results 174*62c56f98SSadaf Ebrahimi 175*62c56f98SSadaf Ebrahimi def new_per_file_state(self): 176*62c56f98SSadaf Ebrahimi """Dictionary mapping descriptions to their line number.""" 177*62c56f98SSadaf Ebrahimi return {} 178*62c56f98SSadaf Ebrahimi 179*62c56f98SSadaf Ebrahimi def process_test_case(self, per_file_state, 180*62c56f98SSadaf Ebrahimi file_name, line_number, description): 181*62c56f98SSadaf Ebrahimi """Check test case descriptions for errors.""" 182*62c56f98SSadaf Ebrahimi results = self.results 183*62c56f98SSadaf Ebrahimi seen = per_file_state 184*62c56f98SSadaf Ebrahimi if description in seen: 185*62c56f98SSadaf Ebrahimi results.error(file_name, line_number, 186*62c56f98SSadaf Ebrahimi 'Duplicate description (also line {})', 187*62c56f98SSadaf Ebrahimi seen[description]) 188*62c56f98SSadaf Ebrahimi return 189*62c56f98SSadaf Ebrahimi if re.search(br'[\t;]', description): 190*62c56f98SSadaf Ebrahimi results.error(file_name, line_number, 191*62c56f98SSadaf Ebrahimi 'Forbidden character \'{}\' in description', 192*62c56f98SSadaf Ebrahimi re.search(br'[\t;]', description).group(0).decode('ascii')) 193*62c56f98SSadaf Ebrahimi if re.search(br'[^ -~]', description): 194*62c56f98SSadaf Ebrahimi results.error(file_name, line_number, 195*62c56f98SSadaf Ebrahimi 'Non-ASCII character in description') 196*62c56f98SSadaf Ebrahimi if len(description) > 66: 197*62c56f98SSadaf Ebrahimi results.warning(file_name, line_number, 198*62c56f98SSadaf Ebrahimi 'Test description too long ({} > 66)', 199*62c56f98SSadaf Ebrahimi len(description)) 200*62c56f98SSadaf Ebrahimi seen[description] = line_number 201*62c56f98SSadaf Ebrahimi 202*62c56f98SSadaf Ebrahimidef main(): 203*62c56f98SSadaf Ebrahimi parser = argparse.ArgumentParser(description=__doc__) 204*62c56f98SSadaf Ebrahimi parser.add_argument('--list-all', 205*62c56f98SSadaf Ebrahimi action='store_true', 206*62c56f98SSadaf Ebrahimi help='List all test cases, without doing checks') 207*62c56f98SSadaf Ebrahimi parser.add_argument('--quiet', '-q', 208*62c56f98SSadaf Ebrahimi action='store_true', 209*62c56f98SSadaf Ebrahimi help='Hide warnings') 210*62c56f98SSadaf Ebrahimi parser.add_argument('--verbose', '-v', 211*62c56f98SSadaf Ebrahimi action='store_false', dest='quiet', 212*62c56f98SSadaf Ebrahimi help='Show warnings (default: on; undoes --quiet)') 213*62c56f98SSadaf Ebrahimi options = parser.parse_args() 214*62c56f98SSadaf Ebrahimi if options.list_all: 215*62c56f98SSadaf Ebrahimi descriptions = collect_available_test_cases() 216*62c56f98SSadaf Ebrahimi sys.stdout.write('\n'.join(descriptions + [''])) 217*62c56f98SSadaf Ebrahimi return 218*62c56f98SSadaf Ebrahimi results = Results(options) 219*62c56f98SSadaf Ebrahimi checker = DescriptionChecker(results) 220*62c56f98SSadaf Ebrahimi checker.walk_all() 221*62c56f98SSadaf Ebrahimi if (results.warnings or results.errors) and not options.quiet: 222*62c56f98SSadaf Ebrahimi sys.stderr.write('{}: {} errors, {} warnings\n' 223*62c56f98SSadaf Ebrahimi .format(sys.argv[0], results.errors, results.warnings)) 224*62c56f98SSadaf Ebrahimi sys.exit(1 if results.errors else 0) 225*62c56f98SSadaf Ebrahimi 226*62c56f98SSadaf Ebrahimiif __name__ == '__main__': 227*62c56f98SSadaf Ebrahimi main() 228