New test script check-test-cases.py

This script checks test case descriptions in test_suite_*.data and
ssl-opt.sh.

It reports the following issues:
* Error: forbidden character in a test case description.
* Error: Duplicate test description.
* Warning: Test description is too long.
diff --git a/tests/scripts/check-test-cases.py b/tests/scripts/check-test-cases.py
new file mode 100755
index 0000000..688ad25
--- /dev/null
+++ b/tests/scripts/check-test-cases.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+
+"""Sanity checks for test data.
+"""
+
+# Copyright (C) 2019, Arm Limited, All Rights Reserved
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# This file is part of Mbed TLS (https://tls.mbed.org)
+
+import glob
+import os
+import re
+import sys
+
+class Results:
+    def __init__(self):
+        self.errors = 0
+        self.warnings = 0
+
+    def error(self, file_name, line_number, fmt, *args):
+        sys.stderr.write(('{}:{}:ERROR:' + fmt + '\n').
+                         format(file_name, line_number, *args))
+        self.errors += 1
+
+    def warning(self, file_name, line_number, fmt, *args):
+        sys.stderr.write(('{}:{}:Warning:' + fmt + '\n')
+                         .format(file_name, line_number, args))
+        self.warnings += 1
+
+def collect_test_directories():
+    if os.path.isdir('tests'):
+        tests_dir = 'tests'
+    elif os.path.isdir('suites'):
+        tests_dir = '.'
+    elif os.path.isdir('../suites'):
+        tests_dir = '..'
+    directories = [tests_dir]
+    crypto_tests_dir = os.path.normpath(os.path.join(tests_dir,
+                                                     '../crypto/tests'))
+    if os.path.isdir(crypto_tests_dir):
+        directories.append(crypto_tests_dir)
+    return directories
+
+def check_test_suite(results, data_file_name):
+    in_paragraph = False
+    descriptions = {}
+    line_number = 0
+    with open(data_file_name) as data_file:
+        for line in data_file:
+            line_number += 1
+            line = line.rstrip('\r\n')
+            if not line:
+                in_paragraph = False
+                continue
+            if line.startswith('#'):
+                continue
+            if not in_paragraph:
+                # This is a test case description line.
+                if line in descriptions:
+                    results.error(data_file_name, line_number,
+                                  'Duplicate description (also line {}): {}',
+                                  descriptions[line], line)
+                else:
+                    if re.search(r'[\t;]', line):
+                        results.error(data_file_name, line_number,
+                                      'Forbidden character in description')
+                    if len(line) > 66:
+                        results.warning(data_file_name, line_number,
+                                        'Test description will be truncated')
+                    descriptions[line] = line_number
+            in_paragraph = True
+
+def check_ssl_opt_sh(results, file_name):
+    descriptions = {}
+    line_number = 0
+    with open(file_name) as file_contents:
+        for line in file_contents:
+            line_number += 1
+            # Assume that all run_test calls have the same simple form
+            # with the test description entirely on the same line as the
+            # function name.
+            m = re.match(r'\s+run_test\s+"([^"])"', line)
+            if not m:
+                continue
+            description = m.group(1)
+            if description in descriptions:
+                results.error(data_file_name, line_number,
+                              'Duplicate description (also line {}): {}',
+                              descriptions[line], line)
+            else:
+                if re.search(r'[\t;]', line):
+                    results.error(data_file_name, line_number,
+                                  'Forbidden character in description')
+                if len(line) > 66:
+                    results.warning(data_file_name, line_number,
+                                    'Test description will break visual alignment')
+                descriptions[line] = line_number
+
+def main():
+    test_directories = collect_test_directories()
+    results = Results()
+    for directory in test_directories:
+        for data_file_name in glob.glob(os.path.join(directory, 'suites',
+                                                     '*.data')):
+            check_test_suite(results, data_file_name)
+        ssl_opt_sh = os.path.join(directory, 'ssl-opt.sh')
+        if os.path.exists(ssl_opt_sh):
+            check_ssl_opt_sh(results, ssl_opt_sh)
+    if results.warnings or results.errors:
+        sys.stderr.write('{}: {} errors, {} warnings\n'
+                         .format(sys.argv[0], results.errors, results.warnings))
+    sys.exit(1 if results.errors else 0)
+
+if __name__ == '__main__':
+    main()