mirror of
https://github.com/arendst/Tasmota.git
synced 2026-07-28 04:15:53 +00:00
414 lines
14 KiB
Python
414 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Compare the output of native C Berry solidification against the new
|
||
Python-port solidification.
|
||
|
||
For each Berry module under lib/libesp32{,_lvgl}/<module>/ that has both a
|
||
solidify_all.be (C) and a solidify_all_python.be (Python) script:
|
||
|
||
1. Run the native C berry binary against solidify_all.be, capturing every
|
||
generated .h file under src/solidify/ into a per-module temp dir.
|
||
2. Run "python3 -m berry_port -s -g solidify_all_python.be", capturing
|
||
every generated .h file under src/solidify/ into another temp dir.
|
||
3. Diff the two trees and report any difference (missing / extra / changed
|
||
files) per module.
|
||
|
||
The original src/solidify/ contents are preserved: we snapshot, restore, and
|
||
clean up between runs.
|
||
|
||
Usage:
|
||
python3 pio-tools/compare-berry-solidify.py # all modules
|
||
python3 pio-tools/compare-berry-solidify.py -m <name> # single module
|
||
python3 pio-tools/compare-berry-solidify.py --keep # keep temp dirs
|
||
|
||
Exit code: 0 if every module is identical, 1 if any difference is found.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import difflib
|
||
import filecmp
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from os.path import join, isfile, isdir, basename, dirname, abspath
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Paths
|
||
# ---------------------------------------------------------------------------
|
||
SCRIPT_DIR = dirname(abspath(__file__))
|
||
PROJECT_DIR = abspath(join(SCRIPT_DIR, ".."))
|
||
BERRY_ROOT = join(PROJECT_DIR, "lib", "libesp32", "berry")
|
||
BERRY_PORT_DIR = BERRY_ROOT # parent of berry_port package
|
||
|
||
# tasmota_defines_for_berry.be is imported only by solidify_all_python.be.
|
||
# The native C berry never sees those defines, so for a fair comparison we
|
||
# replace the file with a minimal stub during the Python run.
|
||
DEFINES_FILE = join(PROJECT_DIR, "tasmota", "tasmota_defines_for_berry.be")
|
||
DEFINES_STUB = (
|
||
"# Empty stub used by compare-berry-solidify.py to neutralize\n"
|
||
"# tasmota_defines_for_berry.be while diffing native vs. python output.\n"
|
||
"import preproc\n"
|
||
"preproc.clear()\n"
|
||
)
|
||
|
||
# Module dirs to compare (path is relative to PROJECT_DIR)
|
||
MODULES = [
|
||
join("lib", "libesp32", "berry_tasmota"),
|
||
join("lib", "libesp32", "berry_matter"),
|
||
join("lib", "libesp32", "berry_animation"),
|
||
join("lib", "libesp32", "berry_custom"),
|
||
join("lib", "libesp32_lvgl", "lv_binding_berry"),
|
||
join("lib", "libesp32_lvgl", "lv_haspmota"),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
def _native_berry_binary() -> str:
|
||
"""Return the native Berry binary appropriate for the host."""
|
||
if sys.platform == "win32":
|
||
bin_path = join(BERRY_ROOT, "berry.exe")
|
||
else:
|
||
bin_path = join(BERRY_ROOT, "berry")
|
||
if not isfile(bin_path):
|
||
sys.exit(
|
||
f"error: native Berry binary not found at {bin_path}\n"
|
||
f" build it first with `make -C {BERRY_ROOT}`"
|
||
)
|
||
if not os.access(bin_path, os.X_OK):
|
||
sys.exit(f"error: {bin_path} is not executable")
|
||
return bin_path
|
||
|
||
|
||
def _snapshot_dir(src: str) -> str | None:
|
||
"""Move the contents of src to a fresh temp dir, return the temp dir.
|
||
Returns None if src does not exist."""
|
||
if not isdir(src):
|
||
return None
|
||
tmp = tempfile.mkdtemp(prefix="solidify_backup_")
|
||
for fn in os.listdir(src):
|
||
shutil.move(join(src, fn), join(tmp, fn))
|
||
return tmp
|
||
|
||
|
||
def _restore_dir(src: str, backup: str | None) -> None:
|
||
"""Restore a previously snapshotted directory (and remove the backup)."""
|
||
if backup is None:
|
||
return
|
||
# clear src first
|
||
if isdir(src):
|
||
for fn in os.listdir(src):
|
||
p = join(src, fn)
|
||
if os.path.isdir(p):
|
||
shutil.rmtree(p)
|
||
else:
|
||
os.remove(p)
|
||
else:
|
||
os.makedirs(src, exist_ok=True)
|
||
for fn in os.listdir(backup):
|
||
shutil.move(join(backup, fn), join(src, fn))
|
||
shutil.rmtree(backup, ignore_errors=True)
|
||
|
||
|
||
def _clear_dir(d: str) -> None:
|
||
"""Empty (but keep) a directory."""
|
||
if not isdir(d):
|
||
os.makedirs(d, exist_ok=True)
|
||
return
|
||
for fn in os.listdir(d):
|
||
p = join(d, fn)
|
||
if os.path.isdir(p):
|
||
shutil.rmtree(p)
|
||
else:
|
||
os.remove(p)
|
||
|
||
|
||
def _copy_dir(src: str, dst: str) -> None:
|
||
"""Recursively copy src -> dst (dst must not exist)."""
|
||
shutil.copytree(src, dst)
|
||
|
||
|
||
class _StubDefines:
|
||
"""Context manager: replace tasmota_defines_for_berry.be with an empty
|
||
stub for the duration of the with-block, restoring the original on exit
|
||
(even on exception)."""
|
||
|
||
def __init__(self, target: str, stub_content: str):
|
||
self.target = target
|
||
self.stub_content = stub_content
|
||
self.backup = None # type: str | None
|
||
|
||
def __enter__(self):
|
||
if isfile(self.target):
|
||
fd, self.backup = tempfile.mkstemp(prefix="defines_backup_",
|
||
suffix=".be")
|
||
os.close(fd)
|
||
shutil.copy2(self.target, self.backup)
|
||
else:
|
||
self.backup = None
|
||
os.makedirs(dirname(self.target), exist_ok=True)
|
||
with open(self.target, "w", encoding="utf-8") as f:
|
||
f.write(self.stub_content)
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc, tb):
|
||
try:
|
||
if self.backup is not None:
|
||
shutil.copy2(self.backup, self.target)
|
||
os.remove(self.backup)
|
||
else:
|
||
# original did not exist – remove stub we created
|
||
if isfile(self.target):
|
||
os.remove(self.target)
|
||
except OSError as e:
|
||
print(f"WARN: could not restore {self.target}: {e}",
|
||
file=sys.stderr)
|
||
return False # never swallow exceptions
|
||
|
||
|
||
def _filter_chatter(line: str) -> bool:
|
||
"""Return True if the line is routine chatter that should be silenced."""
|
||
s = line.strip()
|
||
if s.startswith("Parsing:"):
|
||
return True
|
||
if s.startswith("Skipping:"):
|
||
return True
|
||
if s.startswith("# Output directory"):
|
||
return True
|
||
return False
|
||
|
||
|
||
def _run(cmd: list[str], cwd: str, env: dict[str, str], label: str) -> int:
|
||
"""Run a subprocess, streaming filtered output. Returns exit code."""
|
||
print(f" [{label}] $ {' '.join(cmd)}")
|
||
proc = subprocess.Popen(
|
||
cmd,
|
||
cwd=cwd,
|
||
env=env,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True,
|
||
)
|
||
other = []
|
||
assert proc.stdout is not None
|
||
for raw in proc.stdout:
|
||
line = raw.rstrip("\r\n")
|
||
if _filter_chatter(line):
|
||
continue
|
||
other.append(line)
|
||
rc = proc.wait()
|
||
for line in other:
|
||
if line:
|
||
print(f" [{label}] {line}")
|
||
return rc
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Solidification runners
|
||
# ---------------------------------------------------------------------------
|
||
def run_native(module_dir: str, out_dir: str, native_bin: str) -> bool:
|
||
"""Run the native C berry against solidify_all.be. Return True on success."""
|
||
script = join(module_dir, "solidify_all.be")
|
||
if not isfile(script):
|
||
print(f" [native] no solidify_all.be in {module_dir} – skipping")
|
||
return False
|
||
|
||
solidify_dir = join(module_dir, "src", "solidify")
|
||
os.makedirs(solidify_dir, exist_ok=True)
|
||
|
||
env = os.environ.copy()
|
||
env["PYTHONUTF8"] = "1" # harmless for the C binary
|
||
rc = _run([native_bin, "-s", "-g", "solidify_all.be"],
|
||
cwd=module_dir, env=env, label="native")
|
||
if rc != 0:
|
||
print(f" [native] FAILED (rc={rc})")
|
||
return False
|
||
|
||
# snapshot output
|
||
if isdir(out_dir):
|
||
shutil.rmtree(out_dir)
|
||
_copy_dir(solidify_dir, out_dir)
|
||
return True
|
||
|
||
|
||
def run_python(module_dir: str, out_dir: str) -> bool:
|
||
"""Run the Python port against solidify_all_python.be. Return True on success."""
|
||
script = join(module_dir, "solidify_all_python.be")
|
||
if not isfile(script):
|
||
print(f" [python] no solidify_all_python.be in {module_dir} – skipping")
|
||
return False
|
||
|
||
solidify_dir = join(module_dir, "src", "solidify")
|
||
os.makedirs(solidify_dir, exist_ok=True)
|
||
|
||
env = os.environ.copy()
|
||
existing_pp = env.get("PYTHONPATH", "")
|
||
env["PYTHONPATH"] = (
|
||
BERRY_PORT_DIR + (os.pathsep + existing_pp if existing_pp else "")
|
||
)
|
||
env["PYTHONUTF8"] = "1"
|
||
|
||
rc = _run([sys.executable, "-m", "berry_port", "-s", "-g",
|
||
"solidify_all_python.be"],
|
||
cwd=module_dir, env=env, label="python")
|
||
if rc != 0:
|
||
print(f" [python] FAILED (rc={rc})")
|
||
return False
|
||
|
||
if isdir(out_dir):
|
||
shutil.rmtree(out_dir)
|
||
_copy_dir(solidify_dir, out_dir)
|
||
return True
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Diff logic
|
||
# ---------------------------------------------------------------------------
|
||
def _list_h(d: str) -> set[str]:
|
||
if not isdir(d):
|
||
return set()
|
||
return {fn for fn in os.listdir(d) if fn.endswith(".h")}
|
||
|
||
|
||
def diff_trees(native_dir: str, python_dir: str, max_diff_lines: int = 60) -> bool:
|
||
"""Compare two trees of .h files. Returns True if identical."""
|
||
a = _list_h(native_dir)
|
||
b = _list_h(python_dir)
|
||
|
||
only_native = sorted(a - b)
|
||
only_python = sorted(b - a)
|
||
common = sorted(a & b)
|
||
|
||
differing = []
|
||
for fn in common:
|
||
if not filecmp.cmp(join(native_dir, fn), join(python_dir, fn),
|
||
shallow=False):
|
||
differing.append(fn)
|
||
|
||
identical = (not only_native and not only_python and not differing)
|
||
|
||
print(f" files: native={len(a)}, python={len(b)}, common={len(common)}, "
|
||
f"differing={len(differing)}")
|
||
if only_native:
|
||
print(f" only in native ({len(only_native)}): "
|
||
f"{', '.join(only_native[:10])}"
|
||
+ (" ..." if len(only_native) > 10 else ""))
|
||
if only_python:
|
||
print(f" only in python ({len(only_python)}): "
|
||
f"{', '.join(only_python[:10])}"
|
||
+ (" ..." if len(only_python) > 10 else ""))
|
||
|
||
for fn in differing:
|
||
print(f" DIFF: {fn}")
|
||
with open(join(native_dir, fn), "r", errors="replace") as f:
|
||
a_lines = f.readlines()
|
||
with open(join(python_dir, fn), "r", errors="replace") as f:
|
||
b_lines = f.readlines()
|
||
diff = list(difflib.unified_diff(
|
||
a_lines, b_lines,
|
||
fromfile=f"native/{fn}",
|
||
tofile=f"python/{fn}",
|
||
n=2,
|
||
))
|
||
if len(diff) > max_diff_lines:
|
||
for line in diff[:max_diff_lines]:
|
||
sys.stdout.write(" " + line)
|
||
print(f" ... ({len(diff) - max_diff_lines} more lines)")
|
||
else:
|
||
for line in diff:
|
||
sys.stdout.write(" " + line)
|
||
|
||
return identical
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Per-module driver
|
||
# ---------------------------------------------------------------------------
|
||
def compare_module(rel_module: str, work_root: str, native_bin: str) -> bool:
|
||
"""Run both solidifiers for one module and diff results. Returns True
|
||
if the trees are identical."""
|
||
module_dir = join(PROJECT_DIR, rel_module)
|
||
solidify_dir = join(module_dir, "src", "solidify")
|
||
print(f"\n=== {rel_module} ===")
|
||
|
||
native_out = join(work_root, basename(rel_module) + "__native")
|
||
python_out = join(work_root, basename(rel_module) + "__python")
|
||
|
||
backup = _snapshot_dir(solidify_dir)
|
||
try:
|
||
# ensure a clean output dir for the native run
|
||
_clear_dir(solidify_dir)
|
||
ok_n = run_native(module_dir, native_out, native_bin)
|
||
# ensure a clean output dir for the python run
|
||
_clear_dir(solidify_dir)
|
||
# Neutralize tasmota_defines_for_berry.be so the Python run sees no
|
||
# preproc defines (matching what the native C berry sees, since it
|
||
# doesn't import that file at all).
|
||
with _StubDefines(DEFINES_FILE, DEFINES_STUB):
|
||
ok_p = run_python(module_dir, python_out)
|
||
finally:
|
||
_restore_dir(solidify_dir, backup)
|
||
|
||
if not ok_n or not ok_p:
|
||
return False
|
||
|
||
return diff_trees(native_out, python_out)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main
|
||
# ---------------------------------------------------------------------------
|
||
def main():
|
||
p = argparse.ArgumentParser(
|
||
description="Compare native C and Python-port Berry solidification.")
|
||
p.add_argument("-m", "--module", action="append",
|
||
help="restrict to a module (basename, may be repeated). "
|
||
"Default: all known modules.")
|
||
p.add_argument("--keep", action="store_true",
|
||
help="keep the temp directory with both output trees.")
|
||
args = p.parse_args()
|
||
|
||
selected = MODULES
|
||
if args.module:
|
||
wanted = set(args.module)
|
||
selected = [m for m in MODULES if basename(m) in wanted]
|
||
if not selected:
|
||
sys.exit(f"error: none of {sorted(wanted)} matches known modules: "
|
||
f"{[basename(m) for m in MODULES]}")
|
||
|
||
native_bin = _native_berry_binary()
|
||
print(f"Native berry: {native_bin}")
|
||
print(f"Python: {sys.executable}")
|
||
|
||
work_root = tempfile.mkdtemp(prefix="solidify_compare_")
|
||
print(f"Workdir: {work_root}")
|
||
|
||
overall_ok = True
|
||
summary = []
|
||
for rel in selected:
|
||
ok = compare_module(rel, work_root, native_bin)
|
||
summary.append((rel, ok))
|
||
if not ok:
|
||
overall_ok = False
|
||
|
||
print("\n=== Summary ===")
|
||
for rel, ok in summary:
|
||
print(f" {'OK ' if ok else 'DIFFER'} {rel}")
|
||
|
||
if args.keep:
|
||
print(f"\nKept output trees in: {work_root}")
|
||
else:
|
||
shutil.rmtree(work_root, ignore_errors=True)
|
||
|
||
sys.exit(0 if overall_ok else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|