replace dictdiffer with native capnp differ (#37279)

* replace dictdiffer with native capnp differ

* capnp diff
This commit is contained in:
Adeeb Shihadeh
2026-02-20 14:20:02 -08:00
committed by GitHub
parent b27fa58444
commit 66687746f9
3 changed files with 60 additions and 33 deletions

View File

@@ -96,7 +96,6 @@ testing = [
dev = [
"av",
"dictdiffer",
"matplotlib",
"opencv-python-headless",
"parameterized >=0.8, <0.9",

View File

@@ -3,13 +3,16 @@ import sys
import math
import capnp
import numbers
import dictdiffer
from collections import Counter
from openpilot.tools.lib.logreader import LogReader
EPSILON = sys.float_info.epsilon
_DynamicStructReader = capnp.lib.capnp._DynamicStructReader
_DynamicListReader = capnp.lib.capnp._DynamicListReader
_DynamicEnum = capnp.lib.capnp._DynamicEnum
def remove_ignored_fields(msg, ignore):
msg = msg.as_builder()
@@ -39,6 +42,61 @@ def remove_ignored_fields(msg, ignore):
return msg
def _diff_capnp(r1, r2, path, tolerance):
"""Walk two capnp struct readers and yield (action, dotted_path, value) diffs.
Floats are compared with the given tolerance (combined absolute+relative).
"""
schema = r1.schema
for fname in schema.non_union_fields:
child_path = path + (fname,)
v1 = getattr(r1, fname)
v2 = getattr(r2, fname)
yield from _diff_capnp_values(v1, v2, child_path, tolerance)
if schema.union_fields:
w1, w2 = r1.which(), r2.which()
if w1 != w2:
yield 'change', '.'.join(path), (w1, w2)
else:
child_path = path + (w1,)
v1, v2 = getattr(r1, w1), getattr(r2, w2)
yield from _diff_capnp_values(v1, v2, child_path, tolerance)
def _diff_capnp_values(v1, v2, path, tolerance):
if isinstance(v1, _DynamicStructReader):
yield from _diff_capnp(v1, v2, path, tolerance)
elif isinstance(v1, _DynamicListReader):
dot = '.'.join(path)
n1, n2 = len(v1), len(v2)
n = min(n1, n2)
for i in range(n):
yield from _diff_capnp_values(v1[i], v2[i], path + (str(i),), tolerance)
if n2 > n:
yield 'add', dot, list(enumerate(v2[n:], n))
if n1 > n:
yield 'remove', dot, list(reversed([(i, v1[i]) for i in range(n, n1)]))
elif isinstance(v1, _DynamicEnum):
s1, s2 = str(v1), str(v2)
if s1 != s2:
yield 'change', '.'.join(path), (s1, s2)
elif isinstance(v1, float):
if not (v1 == v2 or (
math.isfinite(v1) and math.isfinite(v2) and
abs(v1 - v2) <= max(tolerance, tolerance * max(abs(v1), abs(v2)))
)):
yield 'change', '.'.join(path), (v1, v2)
else:
if v1 != v2:
yield 'change', '.'.join(path), (v1, v2)
def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=None,):
if ignore_fields is None:
ignore_fields = []
@@ -65,26 +123,7 @@ def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=Non
msg2 = remove_ignored_fields(msg2, ignore_fields)
if msg1.to_bytes() != msg2.to_bytes():
msg1_dict = msg1.as_reader().to_dict(verbose=True)
msg2_dict = msg2.as_reader().to_dict(verbose=True)
dd = dictdiffer.diff(msg1_dict, msg2_dict, ignore=ignore_fields)
# Dictdiffer only supports relative tolerance, we also want to check for absolute
# TODO: add this to dictdiffer
def outside_tolerance(diff):
try:
if diff[0] == "change":
a, b = diff[2]
finite = math.isfinite(a) and math.isfinite(b)
if finite and isinstance(a, numbers.Number) and isinstance(b, numbers.Number):
return abs(a - b) > max(tolerance, tolerance * max(abs(a), abs(b)))
except TypeError:
pass
return True
dd = list(filter(outside_tolerance, dd))
dd = list(_diff_capnp(msg1.as_reader(), msg2.as_reader(), (), tolerance))
diff.extend(dd)
return diff

11
uv.lock generated
View File

@@ -356,15 +356,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/58/d01538556103d544a5a5b4cbcb00646ff92d8a97f0a6283a56bede4307c8/dearpygui-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f2291313d2035f8a4108e13f60d8c1a0e7c19af7554a7739a3fd15b3d5af8f7", size = 1808971, upload-time = "2025-11-14T14:47:28.15Z" },
]
[[package]]
name = "dictdiffer"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/7b/35cbccb7effc5d7e40f4c55e2b79399e1853041997fcda15c9ff160abba0/dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578", size = 31513, upload-time = "2021-07-22T13:24:29.276Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" },
]
[[package]]
name = "dnspython"
version = "2.8.0"
@@ -846,7 +837,6 @@ dependencies = [
[package.optional-dependencies]
dev = [
{ name = "av" },
{ name = "dictdiffer" },
{ name = "matplotlib" },
{ name = "opencv-python-headless" },
{ name = "parameterized" },
@@ -887,7 +877,6 @@ requires-dist = [
{ name = "crcmod-plus" },
{ name = "cython" },
{ name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" },
{ name = "dictdiffer", marker = "extra == 'dev'" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
{ name = "inputs" },
{ name = "jeepney" },