Add scripts/regenerate_goldens.py for batch golden regeneration (#251)

* Add scripts/regenerate_goldens.py for batch golden regeneration

Mirrors the cpp_golden_test list in compiler/back_end/cpp/BUILD,
invoking embossc on each input .emb file to regenerate the
corresponding golden header. Makes generator changes (which usually
require regenerating dozens of golden files) a one-command operation
instead of a per-file dance.

Also handles the testdata/imported_genfiles.emb genrule transform
locally and cleans up afterward so the produced file doesn't collide
with the bazel genrule on subsequent builds.

* Note regenerate_goldens.py mirror in BUILD
diff --git a/compiler/back_end/cpp/BUILD b/compiler/back_end/cpp/BUILD
index be3300f..23b5078 100644
--- a/compiler/back_end/cpp/BUILD
+++ b/compiler/back_end/cpp/BUILD
@@ -399,7 +399,10 @@
     ],
 )
 
-# New golden test infrastructure
+# New golden test infrastructure.
+#
+# NOTE: When adding or removing cpp_golden_test(...) rules below, also update
+# the GOLDENS list in scripts/regenerate_goldens.py so the two stay in sync.
 py_library(
     name = "one_golden_test_lib",
     srcs = ["one_golden_test.py"],
diff --git a/scripts/regenerate_goldens.py b/scripts/regenerate_goldens.py
new file mode 100644
index 0000000..4a234dc
--- /dev/null
+++ b/scripts/regenerate_goldens.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+"""Regenerates testdata/golden_cpp/*.h files.
+
+Run from the repo root. Mirrors the cpp_golden_test list in
+compiler/back_end/cpp/BUILD.
+"""
+
+import os
+import subprocess
+import sys
+
+REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Golden tests: emb file (under testdata/) -> golden file (under testdata/golden_cpp/).
+# All cpp_golden_test entries from compiler/back_end/cpp/BUILD.
+GOLDENS = [
+    ("anonymous_bits.emb", "anonymous_bits.emb.h"),
+    ("bits.emb", "bits.emb.h"),
+    ("absolute_cpp_namespace.emb", "absolute_cpp_namespace.emb.h"),
+    ("alignments.emb", "alignments.emb.h"),
+    ("auto_array_size.emb", "auto_array_size.emb.h"),
+    ("bcd.emb", "bcd.emb.h"),
+    ("complex_offset.emb", "complex_offset.emb.h"),
+    ("complex_structure.emb", "complex_structure.emb.h"),
+    ("condition.emb", "condition.emb.h"),
+    ("cpp_namespace.emb", "cpp_namespace.emb.h"),
+    ("dynamic_size.emb", "dynamic_size.emb.h"),
+    ("enum_case.emb", "enum_case.emb.h"),
+    ("enum.emb", "enum.emb.h"),
+    ("explicit_sizes.emb", "explicit_sizes.emb.h"),
+    ("float.emb", "float.emb.h"),
+    ("imported.emb", "imported.emb.h"),
+    ("inline_type.emb", "inline_type.emb.h"),
+    ("int_sizes.emb", "int_sizes.emb.h"),
+    ("large_array.emb", "large_array.emb.h"),
+    ("nested_structure.emb", "nested_structure.emb.h"),
+    ("next_keyword.emb", "next_keyword.emb.h"),
+    ("no_cpp_namespace.emb", "no_cpp_namespace.emb.h"),
+    ("no_enum_traits.emb", "no_enum_traits.emb.h"),
+    ("parameters.emb", "parameters.emb.h"),
+    ("requires.emb", "requires.emb.h"),
+    ("start_size_range.emb", "start_size_range.emb.h"),
+    ("subtypes.emb", "subtypes.emb.h"),
+    ("text_format.emb", "text_format.emb.h"),
+    ("uint_sizes.emb", "uint_sizes.emb.h"),
+    ("virtual_field.emb", "virtual_field.emb.h"),
+    ("importer.emb", "importer.emb.h"),
+    ("importer2.emb", "importer2.emb.h"),
+    ("many_conditionals.emb", "many_conditionals.emb.h"),
+]
+
+
+# imported_genfiles.emb is produced by a genrule from imported.emb.
+# Reproduce that sed transformation so the golden stays in sync.
+def _ensure_imported_genfiles():
+    src = os.path.join(REPO, "testdata", "imported.emb")
+    dst = os.path.join(REPO, "testdata", "imported_genfiles.emb")
+    with open(src, "r") as f:
+        contents = f.read()
+    contents = contents.replace("emboss::test", "emboss::test::generated")
+    with open(dst, "w") as f:
+        f.write(contents)
+
+
+def _no_enum_traits_args(emb_file):
+    # The golden test for no_enum_traits.emb actually runs with default
+    # (traits-enabled) settings; the file just verifies that the schema
+    # parses when traits are disabled in downstream tests.
+    return []
+
+
+def main():
+    _ensure_imported_genfiles()
+    try:
+        embossc = os.path.join(REPO, "embossc")
+        out_dir = os.path.join(REPO, "testdata", "golden_cpp")
+        failures = []
+        for emb, golden in GOLDENS + [
+            ("imported_genfiles.emb", "imported_genfiles.emb.h")
+        ]:
+            emb_path = os.path.join("testdata", emb)
+            cmd = (
+                [
+                    embossc,
+                    "--import-dir=.",
+                    "--import-dir=testdata",
+                    "--output-file=" + golden,
+                    "--output-path=" + out_dir,
+                ]
+                + _no_enum_traits_args(emb)
+                + [emb_path]
+            )
+            result = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True)
+            if result.returncode != 0:
+                failures.append((emb, result.stderr))
+                print(f"FAIL {emb}\n{result.stderr}", file=sys.stderr)
+            else:
+                print(f"OK   {emb}")
+        if failures:
+            sys.exit(1)
+    finally:
+        # imported_genfiles.emb is a build artifact; remove it so it doesn't
+        # collide with the genrule that produces it during a bazel build.
+        gen = os.path.join(REPO, "testdata", "imported_genfiles.emb")
+        if os.path.exists(gen):
+            os.remove(gen)
+
+
+if __name__ == "__main__":
+    main()