check_test_cases: move "walk" functions into a class

Make the structure more Pythonic: use classes for abstraction and
refinement, rather than higher-order functions.

Convert walk(function, state, data) into instance.walk(data) where
instance has a method that implements function and state is a field of
instance.

No behavior change.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
diff --git a/tests/scripts/check_test_cases.py b/tests/scripts/check_test_cases.py
index f25b602..04ade63 100755
--- a/tests/scripts/check_test_cases.py
+++ b/tests/scripts/check_test_cases.py
@@ -76,59 +76,98 @@
                         len(description))
     seen[description] = line_number
 
-def walk_test_suite(function, results, descriptions, data_file_name):
-    """Iterate over the test cases in the given unit test data file.
+class TestDescriptionExplorer:
+    """An iterator over test cases with descriptions.
 
-Call function(results, descriptions, data_file_name, line_number, description)
-on each description.
+The test cases that have descriptions are:
+* Individual unit tests (entries in a .data file) in test suites.
+* Individual test cases in ssl-opt.sh.
+
+This is an abstract class. To use it, derive a class that implements
+the process_test_case method, and call walk_all().
 """
-    in_paragraph = False
-    with open(data_file_name, 'rb') as data_file:
-        for line_number, line in enumerate(data_file, 1):
-            line = line.rstrip(b'\r\n')
-            if not line:
-                in_paragraph = False
-                continue
-            if line.startswith(b'#'):
-                continue
-            if not in_paragraph:
-                # This is a test case description line.
-                function(results, descriptions,
-                         data_file_name, line_number, line)
-            in_paragraph = True
 
-def walk_ssl_opt_sh(function, results, descriptions, file_name):
-    """Iterate over the test cases in ssl-opt.sh or a file with a similar format.
+    def process_test_case(self, per_file_state,
+                          file_name, line_number, description):
+        """Process a test case.
 
-Call function(results, descriptions, file_name, line_number, description)
-on each description.
+per_file_state: a new object returned by per_file_state() for each file.
+file_name: a relative path to the file containing the test case.
+line_number: the line number in the given file.
+description: the test case description as a byte string.
 """
-    with open(file_name, 'rb') as file_contents:
-        for line_number, line in enumerate(file_contents, 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(br'\s*run_test\s+"((?:[^\\"]|\\.)*)"', line)
-            if not m:
-                continue
-            description = m.group(1)
-            function(results, descriptions,
-                     file_name, line_number, description)
+        raise NotImplementedError
 
-def walk_all(function, results):
-    """Iterate over all named test cases.
+    def per_file_state(self):
+        """Return a new per-file state object.
 
-Call function(results, {}, file_name, line_number, description)
-on each description.
+The default per-file state object is None. Child classes that require per-file
+state may override this method.
 """
-    test_directories = collect_test_directories()
-    for directory in test_directories:
-        for data_file_name in glob.glob(os.path.join(directory, 'suites',
-                                                     '*.data')):
-            walk_test_suite(function, results, {}, data_file_name)
-        ssl_opt_sh = os.path.join(directory, 'ssl-opt.sh')
-        if os.path.exists(ssl_opt_sh):
-            walk_ssl_opt_sh(function, results, {}, ssl_opt_sh)
+        #pylint: disable=no-self-use
+        return None
+
+    def walk_test_suite(self, data_file_name):
+        """Iterate over the test cases in the given unit test data file."""
+        in_paragraph = False
+        descriptions = self.per_file_state() # pylint: disable=assignment-from-none
+        with open(data_file_name, 'rb') as data_file:
+            for line_number, line in enumerate(data_file, 1):
+                line = line.rstrip(b'\r\n')
+                if not line:
+                    in_paragraph = False
+                    continue
+                if line.startswith(b'#'):
+                    continue
+                if not in_paragraph:
+                    # This is a test case description line.
+                    self.process_test_case(descriptions,
+                                           data_file_name, line_number, line)
+                in_paragraph = True
+
+    def walk_ssl_opt_sh(self, file_name):
+        """Iterate over the test cases in ssl-opt.sh or a file with a similar format."""
+        descriptions = self.per_file_state() # pylint: disable=assignment-from-none
+        with open(file_name, 'rb') as file_contents:
+            for line_number, line in enumerate(file_contents, 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(br'\s*run_test\s+"((?:[^\\"]|\\.)*)"', line)
+                if not m:
+                    continue
+                description = m.group(1)
+                self.process_test_case(descriptions,
+                                       file_name, line_number, description)
+
+    def walk_all(self):
+        """Iterate over all named test cases."""
+        test_directories = collect_test_directories()
+        for directory in test_directories:
+            for data_file_name in glob.glob(os.path.join(directory, 'suites',
+                                                         '*.data')):
+                self.walk_test_suite(data_file_name)
+            ssl_opt_sh = os.path.join(directory, 'ssl-opt.sh')
+            if os.path.exists(ssl_opt_sh):
+                self.walk_ssl_opt_sh(ssl_opt_sh)
+
+class DescriptionChecker(TestDescriptionExplorer):
+    """Check all test case descriptions.
+
+* Check that each description is valid (length, allowed character set, etc.).
+* Check that there is no duplicated description inside of one test suite.
+"""
+
+    def __init__(self, results):
+        self.results = results
+
+    def per_file_state(self):
+        return {}
+
+    def process_test_case(self, per_file_state,
+                          file_name, line_number, description):
+        check_description(self.results, per_file_state,
+                          file_name, line_number, description)
 
 def main():
     parser = argparse.ArgumentParser(description=__doc__)
@@ -140,7 +179,8 @@
                         help='Show warnings (default: on; undoes --quiet)')
     options = parser.parse_args()
     results = Results(options)
-    walk_all(check_description, results)
+    checker = DescriptionChecker(results)
+    checker.walk_all()
     if (results.warnings or results.errors) and not options.quiet:
         sys.stderr.write('{}: {} errors, {} warnings\n'
                          .format(sys.argv[0], results.errors, results.warnings))