blob: c326710b6738f83ceaf702fc7094a58deb76430d [file] [log] [blame]
Gilles Peskine31e31522024-10-03 17:23:53 +02001"""Discover all the test cases (unit tests and SSL tests)."""
2
3# Copyright The Mbed TLS Contributors
4# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
Gilles Peskineeba00972024-10-03 17:35:52 +02005
6import glob
7import os
8import re
9import subprocess
10import sys
11
12import scripts_path # pylint: disable=unused-import
13from mbedtls_framework import build_tree
14
15
16class ScriptOutputError(ValueError):
17 """A kind of ValueError that indicates we found
18 the script doesn't list test cases in an expected
19 pattern.
20 """
21
22 @property
23 def script_name(self):
24 return super().args[0]
25
26 @property
27 def idx(self):
28 return super().args[1]
29
30 @property
31 def line(self):
32 return super().args[2]
33
34class Results:
35 """Store file and line information about errors or warnings in test suites."""
36
37 def __init__(self, options):
38 self.errors = 0
39 self.warnings = 0
40 self.ignore_warnings = options.quiet
41
42 def error(self, file_name, line_number, fmt, *args):
43 sys.stderr.write(('{}:{}:ERROR:' + fmt + '\n').
44 format(file_name, line_number, *args))
45 self.errors += 1
46
47 def warning(self, file_name, line_number, fmt, *args):
48 if not self.ignore_warnings:
49 sys.stderr.write(('{}:{}:Warning:' + fmt + '\n')
50 .format(file_name, line_number, *args))
51 self.warnings += 1
52
53class TestDescriptionExplorer:
54 """An iterator over test cases with descriptions.
55
56The test cases that have descriptions are:
57* Individual unit tests (entries in a .data file) in test suites.
58* Individual test cases in ssl-opt.sh.
59
60This is an abstract class. To use it, derive a class that implements
61the process_test_case method, and call walk_all().
62"""
63
64 def process_test_case(self, per_file_state,
65 file_name, line_number, description):
66 """Process a test case.
67
68per_file_state: an object created by new_per_file_state() at the beginning
69 of each file.
70file_name: a relative path to the file containing the test case.
71line_number: the line number in the given file.
72description: the test case description as a byte string.
73"""
74 raise NotImplementedError
75
76 def new_per_file_state(self):
77 """Return a new per-file state object.
78
79The default per-file state object is None. Child classes that require per-file
80state may override this method.
81"""
82 #pylint: disable=no-self-use
83 return None
84
85 def walk_test_suite(self, data_file_name):
86 """Iterate over the test cases in the given unit test data file."""
87 in_paragraph = False
88 descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none
89 with open(data_file_name, 'rb') as data_file:
90 for line_number, line in enumerate(data_file, 1):
91 line = line.rstrip(b'\r\n')
92 if not line:
93 in_paragraph = False
94 continue
95 if line.startswith(b'#'):
96 continue
97 if not in_paragraph:
98 # This is a test case description line.
99 self.process_test_case(descriptions,
100 data_file_name, line_number, line)
101 in_paragraph = True
102
103 def collect_from_script(self, script_name):
104 """Collect the test cases in a script by calling its listing test cases
105option"""
106 descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none
107 listed = subprocess.check_output(['sh', script_name, '--list-test-cases'])
108 # Assume test file is responsible for printing identical format of
109 # test case description between --list-test-cases and its OUTCOME.CSV
110 #
111 # idx indicates the number of test case since there is no line number
112 # in the script for each test case.
113 for idx, line in enumerate(listed.splitlines()):
114 # We are expecting the script to list the test cases in
115 # `<suite_name>;<description>` pattern.
116 script_outputs = line.split(b';', 1)
117 if len(script_outputs) == 2:
118 suite_name, description = script_outputs
119 else:
120 raise ScriptOutputError(script_name, idx, line.decode("utf-8"))
121
122 self.process_test_case(descriptions,
123 suite_name.decode('utf-8'),
124 idx,
125 description.rstrip())
126
127 @staticmethod
128 def collect_test_directories():
129 """Get the relative path for the TLS and Crypto test directories."""
130 mbedtls_root = build_tree.guess_mbedtls_root()
131 directories = [os.path.join(mbedtls_root, 'tests'),
132 os.path.join(mbedtls_root, 'tf-psa-crypto', 'tests')]
133 directories = [os.path.relpath(p) for p in directories]
134 return directories
135
136 def walk_all(self):
137 """Iterate over all named test cases."""
138 test_directories = self.collect_test_directories()
139 for directory in test_directories:
140 for data_file_name in glob.glob(os.path.join(directory, 'suites',
141 '*.data')):
142 self.walk_test_suite(data_file_name)
143
144 for sh_file in ['ssl-opt.sh', 'compat.sh']:
145 sh_file = os.path.join(directory, sh_file)
146 if os.path.isfile(sh_file):
147 self.collect_from_script(sh_file)
148
149class TestDescriptions(TestDescriptionExplorer):
150 """Collect the available test cases."""
151
152 def __init__(self):
153 super().__init__()
154 self.descriptions = set()
155
156 def process_test_case(self, _per_file_state,
157 file_name, _line_number, description):
158 """Record an available test case."""
159 base_name = re.sub(r'\.[^.]*$', '', re.sub(r'.*/', '', file_name))
160 key = ';'.join([base_name, description.decode('utf-8')])
161 self.descriptions.add(key)
162
163def collect_available_test_cases():
164 """Collect the available test cases."""
165 explorer = TestDescriptions()
166 explorer.walk_all()
167 return sorted(explorer.descriptions)