check_test_cases.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env python3
  2. """Sanity checks for test data.
  3. This program contains a class for traversing test cases that can be used
  4. independently of the checks.
  5. """
  6. # Copyright The Mbed TLS Contributors
  7. # SPDX-License-Identifier: Apache-2.0
  8. #
  9. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  10. # not use this file except in compliance with the License.
  11. # You may obtain a copy of the License at
  12. #
  13. # http://www.apache.org/licenses/LICENSE-2.0
  14. #
  15. # Unless required by applicable law or agreed to in writing, software
  16. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  17. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. # See the License for the specific language governing permissions and
  19. # limitations under the License.
  20. import argparse
  21. import glob
  22. import os
  23. import re
  24. import sys
  25. class Results:
  26. """Store file and line information about errors or warnings in test suites."""
  27. def __init__(self, options):
  28. self.errors = 0
  29. self.warnings = 0
  30. self.ignore_warnings = options.quiet
  31. def error(self, file_name, line_number, fmt, *args):
  32. sys.stderr.write(('{}:{}:ERROR:' + fmt + '\n').
  33. format(file_name, line_number, *args))
  34. self.errors += 1
  35. def warning(self, file_name, line_number, fmt, *args):
  36. if not self.ignore_warnings:
  37. sys.stderr.write(('{}:{}:Warning:' + fmt + '\n')
  38. .format(file_name, line_number, *args))
  39. self.warnings += 1
  40. class TestDescriptionExplorer:
  41. """An iterator over test cases with descriptions.
  42. The test cases that have descriptions are:
  43. * Individual unit tests (entries in a .data file) in test suites.
  44. * Individual test cases in ssl-opt.sh.
  45. This is an abstract class. To use it, derive a class that implements
  46. the process_test_case method, and call walk_all().
  47. """
  48. def process_test_case(self, per_file_state,
  49. file_name, line_number, description):
  50. """Process a test case.
  51. per_file_state: an object created by new_per_file_state() at the beginning
  52. of each file.
  53. file_name: a relative path to the file containing the test case.
  54. line_number: the line number in the given file.
  55. description: the test case description as a byte string.
  56. """
  57. raise NotImplementedError
  58. def new_per_file_state(self):
  59. """Return a new per-file state object.
  60. The default per-file state object is None. Child classes that require per-file
  61. state may override this method.
  62. """
  63. #pylint: disable=no-self-use
  64. return None
  65. def walk_test_suite(self, data_file_name):
  66. """Iterate over the test cases in the given unit test data file."""
  67. in_paragraph = False
  68. descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none
  69. with open(data_file_name, 'rb') as data_file:
  70. for line_number, line in enumerate(data_file, 1):
  71. line = line.rstrip(b'\r\n')
  72. if not line:
  73. in_paragraph = False
  74. continue
  75. if line.startswith(b'#'):
  76. continue
  77. if not in_paragraph:
  78. # This is a test case description line.
  79. self.process_test_case(descriptions,
  80. data_file_name, line_number, line)
  81. in_paragraph = True
  82. def walk_ssl_opt_sh(self, file_name):
  83. """Iterate over the test cases in ssl-opt.sh or a file with a similar format."""
  84. descriptions = self.new_per_file_state() # pylint: disable=assignment-from-none
  85. with open(file_name, 'rb') as file_contents:
  86. for line_number, line in enumerate(file_contents, 1):
  87. # Assume that all run_test calls have the same simple form
  88. # with the test description entirely on the same line as the
  89. # function name.
  90. m = re.match(br'\s*run_test\s+"((?:[^\\"]|\\.)*)"', line)
  91. if not m:
  92. continue
  93. description = m.group(1)
  94. self.process_test_case(descriptions,
  95. file_name, line_number, description)
  96. @staticmethod
  97. def collect_test_directories():
  98. """Get the relative path for the TLS and Crypto test directories."""
  99. if os.path.isdir('tests'):
  100. tests_dir = 'tests'
  101. elif os.path.isdir('suites'):
  102. tests_dir = '.'
  103. elif os.path.isdir('../suites'):
  104. tests_dir = '..'
  105. directories = [tests_dir]
  106. return directories
  107. def walk_all(self):
  108. """Iterate over all named test cases."""
  109. test_directories = self.collect_test_directories()
  110. for directory in test_directories:
  111. for data_file_name in glob.glob(os.path.join(directory, 'suites',
  112. '*.data')):
  113. self.walk_test_suite(data_file_name)
  114. ssl_opt_sh = os.path.join(directory, 'ssl-opt.sh')
  115. if os.path.exists(ssl_opt_sh):
  116. self.walk_ssl_opt_sh(ssl_opt_sh)
  117. class DescriptionChecker(TestDescriptionExplorer):
  118. """Check all test case descriptions.
  119. * Check that each description is valid (length, allowed character set, etc.).
  120. * Check that there is no duplicated description inside of one test suite.
  121. """
  122. def __init__(self, results):
  123. self.results = results
  124. def new_per_file_state(self):
  125. """Dictionary mapping descriptions to their line number."""
  126. return {}
  127. def process_test_case(self, per_file_state,
  128. file_name, line_number, description):
  129. """Check test case descriptions for errors."""
  130. results = self.results
  131. seen = per_file_state
  132. if description in seen:
  133. results.error(file_name, line_number,
  134. 'Duplicate description (also line {})',
  135. seen[description])
  136. return
  137. if re.search(br'[\t;]', description):
  138. results.error(file_name, line_number,
  139. 'Forbidden character \'{}\' in description',
  140. re.search(br'[\t;]', description).group(0).decode('ascii'))
  141. if re.search(br'[^ -~]', description):
  142. results.error(file_name, line_number,
  143. 'Non-ASCII character in description')
  144. if len(description) > 66:
  145. results.warning(file_name, line_number,
  146. 'Test description too long ({} > 66)',
  147. len(description))
  148. seen[description] = line_number
  149. def main():
  150. parser = argparse.ArgumentParser(description=__doc__)
  151. parser.add_argument('--quiet', '-q',
  152. action='store_true',
  153. help='Hide warnings')
  154. parser.add_argument('--verbose', '-v',
  155. action='store_false', dest='quiet',
  156. help='Show warnings (default: on; undoes --quiet)')
  157. options = parser.parse_args()
  158. results = Results(options)
  159. checker = DescriptionChecker(results)
  160. checker.walk_all()
  161. if (results.warnings or results.errors) and not options.quiet:
  162. sys.stderr.write('{}: {} errors, {} warnings\n'
  163. .format(sys.argv[0], results.errors, results.warnings))
  164. sys.exit(1 if results.errors else 0)
  165. if __name__ == '__main__':
  166. main()