xref: /aosp_15_r20/external/mbedtls/tests/scripts/check_test_cases.py (revision 62c56f9862f102b96d72393aff6076c951fb8148)
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