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