Skip to content

Commit 5aa7aca

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Add support for updating more compliex formatting
Summary: In addition to the ifdef another problem with the LIR tests is we only support const strings. This is Claude's solution to that. Reviewed By: yoney Differential Revision: D95962248 fbshipit-source-id: 8dca261c8bd3a636d743973a98727a1600253d0e
1 parent aa94ec5 commit 5aa7aca

1 file changed

Lines changed: 183 additions & 10 deletions

File tree

cinderx/TestScripts/update_hir_expected.py

Lines changed: 183 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from enum import Enum
1313
from typing import Generator, Iterator, Sequence
1414

15-
# Maps HIR variable to its HIR output.
16-
VarOutputDict = dict[str, Sequence[str]]
15+
# Maps HIR variable to (actual_output, expected_output) tuples.
16+
VarOutputDict = dict[str, tuple[Sequence[str], Sequence[str] | None]]
1717

1818
# Maps all test cases to their variables.
1919
TestOutputDict = dict[str, VarOutputDict]
@@ -108,7 +108,7 @@ def parse_stdout(stdout: str) -> tuple[str, SuiteOutputDict]:
108108

109109
failed_tests = collections.defaultdict(lambda: {})
110110
line_iter = timestamp_stripped_lineiter(iter(stdout.split("\n")))
111-
test_dict: dict[str, list[str]] = {}
111+
test_dict: dict[str, tuple[list[str], Sequence[str] | None]] = {}
112112
test_name: tuple[str, str] = ("??", "??")
113113
for line in line_iter:
114114
if m := VERSION_RE.match(line):
@@ -130,15 +130,21 @@ def parse_stdout(stdout: str) -> tuple[str, SuiteOutputDict]:
130130
if not m:
131131
raise RuntimeError(f"Unexpected line '{line}' after actual text")
132132
varname = m[1]
133+
if varname.endswith(".c_str()"):
134+
varname = varname[:-8]
133135
if varname in test_dict:
134136
raise RuntimeError(
135137
f"Duplicate expect variable name '{varname}' in {test_name[0]}.{test_name[1]}"
136138
)
137-
test_dict[varname] = actual_text
138-
failed_tests[test_name[0]][test_name[1]] = test_dict
139139

140-
# Skip the "Which is: ..." line after the expect variable name.
141-
next(line_iter)
140+
# Capture old expected text from second "Which is:" line
141+
expected_line = next(line_iter)
142+
expected_m = ACTUAL_TEXT_RE.match(expected_line)
143+
expected_text: Sequence[str] | None = None
144+
if expected_m:
145+
expected_text = unescape_gtest_string(expected_m[1]).split("\n")
146+
test_dict[varname] = (actual_text, expected_text)
147+
failed_tests[test_name[0]][test_name[1]] = test_dict
142148

143149
if py_version == unknown_version:
144150
raise RuntimeError("Couldn't figure out Python version from test output")
@@ -258,7 +264,7 @@ def expect(exp: str, actual: str | None = None) -> None:
258264
new_lines.append(expected_version)
259265
# For text HIR tests, there should only be one element in the
260266
# failed test dict.
261-
hir_lines = next(iter(failed_tests[test_case].values()))
267+
hir_lines = next(iter(failed_tests[test_case].values()))[0]
262268
new_lines += hir_lines
263269
while not peek_line().startswith("---"):
264270
next_line()
@@ -333,6 +339,9 @@ def version_compare(v1: str, v2: str) -> int:
333339

334340

335341
CPP_EXPECTED_START_RE: re.Pattern[str] = re.compile(r"^( const char\* ([^ ]+) =)")
342+
CPP_FMT_FORMAT_START_RE: re.Pattern[str] = re.compile(
343+
r"^( (auto|std::string) ([^ ]+) = fmt::format\()$"
344+
)
336345
CPP_EXPECTED_END = ')";'
337346
CPP_TEST_END = "}"
338347

@@ -355,6 +364,85 @@ class State(Enum):
355364
# Active while skipping lines of an expected variable.
356365
SKIP_EXPECTED = 3
357366

367+
# Active while collecting lines of a fmt::format raw string.
368+
COLLECT_FMT_RAW_STRING = 4
369+
370+
371+
_PLACEHOLDER = "\x00PLACEHOLDER\x00"
372+
373+
374+
def escape_literal_braces(s: str) -> str:
375+
"""Escape { -> {{ and } -> }} for use in fmt::format templates."""
376+
return s.replace("{", "{{").replace("}", "}}")
377+
378+
379+
def re_templatize(
380+
old_template_lines: Sequence[str],
381+
old_expanded_lines: Sequence[str],
382+
new_actual_lines: Sequence[str],
383+
) -> list[str] | None:
384+
"""Re-apply {} placeholders from old_template into new_actual output.
385+
386+
Uses the old format template and its gtest-reported expanded form to
387+
discover what {} placeholders expanded to, then replaces those concrete
388+
values in the new actual output with {} placeholders.
389+
390+
Returns None if the old template cannot be matched against the old expanded
391+
output (e.g. if the format changed too much).
392+
"""
393+
old_template = "\n".join(old_template_lines)
394+
old_expanded = "\n".join(old_expanded_lines)
395+
new_actual = "\n".join(new_actual_lines)
396+
397+
# Build a regex from the old template:
398+
# - {{ -> literal {
399+
# - }} -> literal }
400+
# - {} -> capture group (.+?)
401+
# - everything else is escaped
402+
parts = []
403+
expansions_count = 0
404+
i = 0
405+
while i < len(old_template):
406+
if i + 1 < len(old_template) and old_template[i : i + 2] == "{{":
407+
parts.append(re.escape("{"))
408+
i += 2
409+
elif i + 1 < len(old_template) and old_template[i : i + 2] == "}}":
410+
parts.append(re.escape("}"))
411+
i += 2
412+
elif i + 1 < len(old_template) and old_template[i : i + 2] == "{}":
413+
parts.append("(.+?)")
414+
expansions_count += 1
415+
i += 2
416+
else:
417+
parts.append(re.escape(old_template[i]))
418+
i += 1
419+
420+
if expansions_count == 0:
421+
# No placeholders to re-templatize; just return the new actual lines
422+
# with literal braces escaped.
423+
return escape_literal_braces(new_actual).split("\n")
424+
425+
pattern = "".join(parts)
426+
m = re.match(pattern, old_expanded, re.DOTALL)
427+
if m is None:
428+
return None
429+
430+
# Extract what each {} expanded to
431+
expansion_values = list(m.groups())
432+
433+
# Replace each expansion value in the new actual output with a sentinel
434+
result = new_actual
435+
for val in expansion_values:
436+
result = result.replace(val, _PLACEHOLDER, 1)
437+
438+
# Escape remaining literal braces
439+
result = escape_literal_braces(result)
440+
441+
# Replace sentinels with {}
442+
result = result.replace(_PLACEHOLDER, "{}")
443+
444+
return result.split("\n")
445+
358446

359447
# pyre-ignore[30]
360448
def update_cpp_tests( # noqa: C901
@@ -403,7 +491,14 @@ def expect_empty_test_dict() -> None:
403491
in_version_block = None
404492
non_version_pp_depth = 0
405493
needs_to_close_upgraded_block = False
494+
fmt_collecting_template = False
495+
fmt_old_template_lines: list[str] = []
496+
fmt_raw_prefix = ""
497+
fmt_varname = ""
498+
fmt_actual_lines: Sequence[str] = []
499+
fmt_expected_lines: Sequence[str] | None = None
406500
new_lines = []
501+
suite_name = test_name = ""
407502
for lineno, line in enumerate(old_lines, 1): # noqa: B007
408503
m = CPP_TEST_NAME_RE.match(line)
409504
if m is not None:
@@ -428,6 +523,53 @@ def expect_empty_test_dict() -> None:
428523
new_lines.append(line)
429524
continue
430525

526+
# Handle COLLECT_FMT_RAW_STRING before preprocessor handling
527+
# to avoid misinterpreting raw string content as #if directives.
528+
if state is State.COLLECT_FMT_RAW_STRING:
529+
if fmt_collecting_template:
530+
# We're inside the raw string collecting template lines.
531+
# The raw string ends with )" optionally followed by , and whitespace.
532+
end_m = re.match(r'^(\s*\)"\s*,?\s*)$', line)
533+
if end_m:
534+
# Re-templatize the new actual output
535+
new_template_lines = None
536+
if fmt_expected_lines is not None:
537+
new_template_lines = re_templatize(
538+
fmt_old_template_lines,
539+
fmt_expected_lines,
540+
fmt_actual_lines,
541+
)
542+
if new_template_lines is None:
543+
print(
544+
f" Warning: couldn't re-templatize {fmt_varname} "
545+
f"in {suite_name}.{test_name}, using escaped actual output"
546+
)
547+
new_template_lines = escape_literal_braces(
548+
"\n".join(fmt_actual_lines)
549+
).split("\n")
550+
551+
# Output: R"( prefix + new template + )" suffix
552+
new_lines.append(fmt_raw_prefix + new_template_lines[0])
553+
new_lines += new_template_lines[1:]
554+
new_lines.append(line) # the )" line (preserved as-is)
555+
state = State.PROCESS_FAILED_TEST
556+
else:
557+
fmt_old_template_lines.append(line)
558+
else:
559+
# First line in COLLECT_FMT_RAW_STRING: look for R"( prefix
560+
raw_m = re.match(r'^(\s*R"\()(.*)', line)
561+
if raw_m:
562+
fmt_raw_prefix = raw_m[1]
563+
first_content = raw_m[2]
564+
fmt_old_template_lines = []
565+
if first_content:
566+
fmt_old_template_lines.append(first_content)
567+
fmt_collecting_template = True
568+
else:
569+
# Not a raw string start, pass through
570+
new_lines.append(line)
571+
continue
572+
431573
# Dynamically detect version-specific preprocessor directives
432574
detected_version = parse_version_from_line(line)
433575
if detected_version is not None:
@@ -479,11 +621,12 @@ def expect_empty_test_dict() -> None:
479621
decl = m[1]
480622
varname = m[2]
481623

482-
actual_lines = test_dict.pop(varname, None)
483-
if actual_lines is None:
624+
entry = test_dict.pop(varname, None)
625+
if entry is None:
484626
# This test has multiple expected variables, and this one is OK.
485627
new_lines.append(line)
486628
continue
629+
actual_lines, _expected_lines = entry
487630

488631
# Handle upgrading existing version block to accommodate new version
489632
if (
@@ -549,6 +692,36 @@ def expect_empty_test_dict() -> None:
549692
state = State.PROCESS_FAILED_TEST
550693
continue
551694

695+
m = CPP_FMT_FORMAT_START_RE.match(line)
696+
if m is not None:
697+
expect_state(State.PROCESS_FAILED_TEST)
698+
fmt_varname = m[2]
699+
700+
entry = test_dict.pop(fmt_varname, None)
701+
if entry is None:
702+
# This test has multiple expected variables, and this one is OK.
703+
new_lines.append(line)
704+
continue
705+
fmt_actual_lines, fmt_expected_lines = entry
706+
707+
# For version block mismatches, pass through unchanged
708+
if in_version_block is not None and in_version_block != py_version:
709+
if version_compare(py_version, in_version_block) > 0:
710+
print(
711+
f" Warning: fmt::format version upgrade not yet supported "
712+
f"for {fmt_varname} in {suite_name}.{test_name}"
713+
)
714+
new_lines.append(line)
715+
continue
716+
717+
# Enter COLLECT_FMT_RAW_STRING state
718+
new_lines.append(line)
719+
fmt_collecting_template = False
720+
fmt_old_template_lines = []
721+
fmt_raw_prefix = ""
722+
state = State.COLLECT_FMT_RAW_STRING
723+
continue
724+
552725
new_lines.append(line)
553726

554727
if line == CPP_EXPECTED_END and needs_to_close_upgraded_block:

0 commit comments

Comments
 (0)