Test data generators: add --list-outdated option

As part of a long-term unification effort of generation scripts, add an
option for test generators to list oudated targets without writing to files.
This corresponds to functionality that the new generate_files_helper module
offers.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
diff --git a/scripts/mbedtls_framework/test_case.py b/scripts/mbedtls_framework/test_case.py
index 58e75a1..a4f81ce 100644
--- a/scripts/mbedtls_framework/test_case.py
+++ b/scripts/mbedtls_framework/test_case.py
@@ -127,6 +127,18 @@
         out.write(prefix + self.function + ':' +
                   ':'.join(self.arguments) + '\n')
 
+def write_data_stream(out,
+                      test_cases: Iterable[TestCase],
+                      caller: Optional[str] = None) -> None:
+    """Write the test cases to the specified output stream."""
+    if caller is None:
+        caller = os.path.basename(sys.argv[0])
+    out.write('# Automatically generated by {}. Do not edit!\n'
+              .format(caller))
+    for tc in test_cases:
+        tc.write(out)
+    out.write('\n# End of automatically generated file.\n')
+
 def write_data_file(filename: str,
                     test_cases: Iterable[TestCase],
                     caller: Optional[str] = None) -> None:
@@ -134,15 +146,9 @@
 
     If the file already exists, it is overwritten.
     """
-    if caller is None:
-        caller = os.path.basename(sys.argv[0])
     tempfile = filename + '.new'
     with open(tempfile, 'w') as out:
-        out.write('# Automatically generated by {}. Do not edit!\n'
-                  .format(caller))
-        for tc in test_cases:
-            tc.write(out)
-        out.write('\n# End of automatically generated file.\n')
+        write_data_stream(out, test_cases, caller)
     os.replace(tempfile, filename)
 
 def psa_or_3_6_feature_macro(psa_name: str,
diff --git a/scripts/mbedtls_framework/test_data_generation.py b/scripts/mbedtls_framework/test_data_generation.py
index a84f7dd..959fc15 100644
--- a/scripts/mbedtls_framework/test_data_generation.py
+++ b/scripts/mbedtls_framework/test_data_generation.py
@@ -11,6 +11,7 @@
 #
 
 import argparse
+import io
 import os
 import posixpath
 import re
@@ -139,6 +140,11 @@
 
 class TestGenerator:
     """Generate test cases and write to data files."""
+
+    # Note that targets whose names contain 'test_format' have their content
+    # validated by `abi_check.py`.
+    targets = {} # type: Dict[str, Callable[..., Iterable[test_case.TestCase]]]
+
     def __init__(self, options) -> None:
         self.test_suite_directory = options.directory
         # Update `targets` with an entry for each child class of BaseTarget.
@@ -163,10 +169,6 @@
         filename = self.filename_for(basename)
         test_case.write_data_file(filename, test_cases)
 
-    # Note that targets whose names contain 'test_format' have their content
-    # validated by `abi_check.py`.
-    targets = {} # type: Dict[str, Callable[..., Iterable[test_case.TestCase]]]
-
     def generate_target(self, name: str, *target_args) -> None:
         """Generate cases and write to data file for a target.
 
@@ -176,6 +178,22 @@
         test_cases = self.targets[name](*target_args)
         self.write_test_data_file(name, test_cases)
 
+    def is_up_to_date(self, target) -> bool:
+        """Check if the given target already has the expected content."""
+        filename = self.filename_for(target)
+        if not os.path.exists(filename):
+            return False
+        test_cases = self.targets[target]()
+        out = io.StringIO()
+        test_case.write_data_stream(out, test_cases)
+        out.seek(0)
+        new_content = out.read()
+        out.close()
+        with open(filename) as current_file:
+            old_content = current_file.read()
+        return new_content == old_content
+
+
 def main(args, description: str, generator_class: Type[TestGenerator] = TestGenerator):
     """Command line entry point."""
     parser = argparse.ArgumentParser(description=description)
@@ -183,6 +201,9 @@
                         help='List available targets and exit')
     parser.add_argument('--list-for-cmake', action='store_true',
                         help='Print \';\'-separated list of available targets and exit')
+    parser.add_argument('--list-outdated', action='store_true',
+                        help=('List outdated targets and exit '
+                              '(succeeds even if there are outdated or missing targets)'))
     # If specified explicitly, this option may be a path relative to the
     # current directory when the script is invoked. The default value
     # is relative to the mbedtls root, which we don't know yet. So we
@@ -221,4 +242,8 @@
     else:
         options.targets = sorted(generator.targets)
     for target in options.targets:
-        generator.generate_target(target)
+        if options.list_outdated:
+            if not generator.is_up_to_date(target):
+                print(generator.filename_for(target))
+        else:
+            generator.generate_target(target)