Skip to content

Commit 278f91b

Browse files
j-piaseckifacebook-github-bot
authored andcommitted
Add basic snapshot diffing mechanism (#55906)
Summary: Changelog: [Internal] Adds a simple snapshot diffing mode to the snapshot generation (`--check` argument). Removes `RUN_ON_REACT_NATIVE` constant in favor of `--test` argument. Reviewed By: cipolleschi Differential Revision: D93407648
1 parent 2cbe47e commit 278f91b

2 files changed

Lines changed: 158 additions & 36 deletions

File tree

scripts/cxx-api/parser/__main__.py

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
import argparse
1818
import os
1919
import subprocess
20+
import sys
21+
import tempfile
2022

2123
from .config import parse_config_file
2224
from .main import build_snapshot
2325
from .path_utils import get_react_native_dir
26+
from .snapshot_diff import check_snapshots
2427

2528
DOXYGEN_CONFIG_FILE = ".doxygen.config.generated"
2629

27-
RUN_ON_REACT_NATIVE = False
28-
2930

3031
def build_doxygen_config(
3132
directory: str,
@@ -75,10 +76,11 @@ def build_snapshot_for_view(
7576
exclude_patterns: list[str],
7677
definitions: dict[str, str | int],
7778
output_dir: str,
79+
verbose: bool = True,
7880
) -> None:
79-
print(f"Generating API view: {api_view}")
80-
# Generate the Doxygen config file
81-
print("Generating Doxygen config file")
81+
if verbose:
82+
print(f"Generating API view: {api_view}")
83+
print("Generating Doxygen config file")
8284

8385
build_doxygen_config(
8486
react_native_dir,
@@ -89,10 +91,12 @@ def build_snapshot_for_view(
8991

9092
# If there is already a doxygen output directory, delete it
9193
if os.path.exists(os.path.join(react_native_dir, "api")):
92-
print("Deleting existing output directory")
94+
if verbose:
95+
print("Deleting existing output directory")
9396
subprocess.run(["rm", "-rf", os.path.join(react_native_dir, "api")])
9497

95-
print("Running Doxygen")
98+
if verbose:
99+
print("Running Doxygen")
96100

97101
# Run doxygen with the config file
98102
doxygen_bin = os.environ.get("DOXYGEN_BIN", "doxygen")
@@ -105,12 +109,15 @@ def build_snapshot_for_view(
105109

106110
# Check the result
107111
if result.returncode != 0:
108-
print(f"Doxygen finished with error: {result.stderr}")
112+
if verbose:
113+
print(f"Doxygen finished with error: {result.stderr}")
109114
else:
110-
print("Doxygen finished successfully")
115+
if verbose:
116+
print("Doxygen finished successfully")
111117

112118
# Delete the Doxygen config file
113-
print("Deleting Doxygen config file")
119+
if verbose:
120+
print("Deleting Doxygen config file")
114121
subprocess.run(["rm", DOXYGEN_CONFIG_FILE], cwd=react_native_dir)
115122

116123
# build snapshot, convert to string, and save to file
@@ -124,10 +131,7 @@ def build_snapshot_for_view(
124131
f.write("// @" + "generated by scripts/cxx-api\n\n")
125132
f.write(snapshot_string)
126133

127-
print(f"Snapshot written to: {os.path.abspath(output_file)}")
128-
129-
if not RUN_ON_REACT_NATIVE:
130-
print(snapshot_string)
134+
return snapshot_string
131135

132136

133137
def main():
@@ -144,29 +148,46 @@ def main():
144148
type=str,
145149
help="Path to codegen generated code",
146150
)
151+
parser.add_argument(
152+
"--check",
153+
action="store_true",
154+
help="Generate snapshots to a temp directory and compare against committed ones",
155+
)
156+
parser.add_argument(
157+
"--snapshot-dir",
158+
type=str,
159+
help="Directory containing committed snapshots for comparison (used with --check)",
160+
)
161+
parser.add_argument(
162+
"--test",
163+
action="store_true",
164+
help="Run on the local test directory instead of the react-native directory",
165+
)
147166
args = parser.parse_args()
148167

168+
verbose = not args.check
169+
149170
doxygen_bin = os.environ.get("DOXYGEN_BIN", "doxygen")
150171
version_result = subprocess.run(
151172
[doxygen_bin, "--version"],
152173
capture_output=True,
153174
text=True,
154175
)
155-
print(f"Using Doxygen {version_result.stdout.strip()} ({doxygen_bin})")
176+
if verbose:
177+
print(f"Using Doxygen {version_result.stdout.strip()} ({doxygen_bin})")
156178

157179
# Define the path to the react-native directory
158-
react_native_dir = (
180+
react_native_package_dir = (
159181
os.path.join(get_react_native_dir(), "packages", "react-native")
160-
if RUN_ON_REACT_NATIVE
182+
if not args.test
161183
else os.path.join(get_react_native_dir(), "scripts", "cxx-api", "manual_test")
162184
)
163-
print(f"Running in directory: {react_native_dir}")
185+
if verbose:
186+
print(f"Running in directory: {react_native_package_dir}")
164187

165-
if args.codegen_path:
188+
if verbose and args.codegen_path:
166189
print(f"Codegen output path: {os.path.abspath(args.codegen_path)}")
167190

168-
output_dir = args.output_dir if args.output_dir else react_native_dir
169-
170191
# Parse config file
171192
config_path = os.path.join(
172193
get_react_native_dir(), "scripts", "cxx-api", "config.yml"
@@ -177,25 +198,53 @@ def main():
177198
codegen_path=args.codegen_path,
178199
)
179200

180-
if RUN_ON_REACT_NATIVE:
181-
for config in snapshot_configs:
182-
build_snapshot_for_view(
183-
api_view=config.snapshot_name,
184-
react_native_dir=react_native_dir,
185-
include_directories=config.inputs,
186-
exclude_patterns=config.exclude_patterns,
187-
definitions=config.definitions,
201+
def build_snapshots(output_dir: str, verbose: bool) -> None:
202+
if not args.test:
203+
for config in snapshot_configs:
204+
build_snapshot_for_view(
205+
api_view=config.snapshot_name,
206+
react_native_dir=react_native_package_dir,
207+
include_directories=config.inputs,
208+
exclude_patterns=config.exclude_patterns,
209+
definitions=config.definitions,
210+
output_dir=output_dir,
211+
verbose=verbose,
212+
)
213+
else:
214+
snapshot = build_snapshot_for_view(
215+
api_view="Test",
216+
react_native_dir=react_native_package_dir,
217+
include_directories=[],
218+
exclude_patterns=[],
219+
definitions={},
188220
output_dir=output_dir,
221+
verbose=verbose,
189222
)
223+
224+
if verbose:
225+
print(snapshot)
226+
227+
if args.check:
228+
with tempfile.TemporaryDirectory() as tmpdir:
229+
build_snapshots(tmpdir, verbose=False)
230+
231+
snapshot_dir = args.snapshot_dir or os.path.join(
232+
get_react_native_dir(), "scripts", "cxx-api", "api-snapshots"
233+
)
234+
235+
if not check_snapshots(tmpdir, snapshot_dir):
236+
sys.exit(1)
237+
238+
print("All snapshot checks passed")
190239
else:
191-
build_snapshot_for_view(
192-
api_view="Test",
193-
react_native_dir=react_native_dir,
194-
include_directories=[],
195-
exclude_patterns=[],
196-
definitions={},
197-
output_dir=output_dir,
240+
output_dir = (
241+
args.output_dir
242+
if args.output_dir
243+
else os.path.join(
244+
get_react_native_dir(), "scripts", "cxx-api", "api-snapshots"
245+
)
198246
)
247+
build_snapshots(output_dir, verbose=True)
199248

200249

201250
if __name__ == "__main__":
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
"""
7+
Utilities for comparing API snapshots.
8+
"""
9+
10+
import difflib
11+
import os
12+
13+
14+
def check_snapshots(generated_dir: str, committed_dir: str) -> bool:
15+
"""Compare generated snapshots against committed ones.
16+
17+
Returns True if check passes (snapshots match or no committed snapshots).
18+
Returns False if snapshots differ.
19+
"""
20+
if not os.path.isdir(committed_dir):
21+
print(f"No committed snapshots directory found at: {committed_dir}")
22+
print("Skipping comparison (no baseline to compare against)")
23+
return True
24+
25+
committed_files = sorted(f for f in os.listdir(committed_dir) if f.endswith(".api"))
26+
generated_files = sorted(f for f in os.listdir(generated_dir) if f.endswith(".api"))
27+
28+
if not committed_files:
29+
print("No committed snapshot files found")
30+
print("Skipping comparison (no baseline to compare against)")
31+
return True
32+
33+
committed_set = set(committed_files)
34+
generated_set = set(generated_files)
35+
all_passed = True
36+
37+
for filename in sorted(committed_set | generated_set):
38+
committed_path = os.path.join(committed_dir, filename)
39+
generated_path = os.path.join(generated_dir, filename)
40+
41+
if filename not in generated_set:
42+
print(
43+
f"FAIL: {filename} exists in committed snapshots but was not generated"
44+
)
45+
all_passed = False
46+
continue
47+
48+
if filename not in committed_set:
49+
print(f"OK: {filename} generated (no committed baseline)")
50+
continue
51+
52+
with open(committed_path) as f:
53+
committed_content = f.read()
54+
with open(generated_path) as f:
55+
generated_content = f.read()
56+
57+
if committed_content == generated_content:
58+
print(f"OK: {filename} matches committed snapshot")
59+
else:
60+
print(f"FAIL: {filename} differs from committed snapshot")
61+
diff = "\n".join(
62+
difflib.unified_diff(
63+
committed_content.splitlines(),
64+
generated_content.splitlines(),
65+
fromfile=f"committed/{filename}",
66+
tofile=f"generated/{filename}",
67+
lineterm="",
68+
)
69+
)
70+
print(diff)
71+
all_passed = False
72+
73+
return all_passed

0 commit comments

Comments
 (0)