Split apart util.value_compare_diff to have better type annotation

This commit is contained in:
baldurk
2026-09-11 15:04:05 +01:00
parent 12636a1607
commit 1409c071a9
+62 -58
View File
@@ -1,4 +1,4 @@
import sys
from __future__ import annotations
import os
import re
import time
@@ -8,7 +8,7 @@ import platform
import hashlib
import zipfile
import subprocess
from typing import Tuple, List
from typing import Tuple, List, Union
from . import png
from rdtest.remoteserver import RemoteServer, AndroidRemoteServer
@@ -290,74 +290,78 @@ def zip_compare(test_file: str, ref_file: str):
# Use the 32-bit float epsilon, not sys.float_info.epsilon which is for double floats
FLT_EPSILON = 2.0*1.19209290E-07
# Python 3.8 doesn't support | for complex types even with future
VectorValue = Union[Tuple[int,...], List[int], Tuple[float,...], List[float]]
ScalarValue = Union[int, float]
ScalarOrVectorValue = Union[VectorValue, ScalarValue]
def value_compare_diff(ref, data, eps=FLT_EPSILON):
# if we're comparing scalar to a 1-length tuple or list, compare against the first element. We only expect this for
# data where it's possibly autogenerated
try:
from typing import TypeGuard
except ImportError:
pass
def is_scalar(x: object) -> TypeGuard[ScalarValue]:
return isinstance(x, int) or isinstance(x, float)
def is_vector(x: object) -> TypeGuard[VectorValue]:
return isinstance(x, list) or isinstance(x, tuple)
def _scalar_compare_diff(ref: int | float, data: int | float, eps=FLT_EPSILON) -> Tuple[bool, float]:
# if the types are different this is probably 0.0 == 0 or something. Just compare straight by casting to floats
if type(data) != type(data):
return float(data) == float(ref), abs(float(data)-float(ref))
# Special handling for NaNs - NaNs are always equal to NaNs, but NaN is never equal to any other value
if math.isnan(ref) and math.isnan(data):
return True, 0.0
elif math.isnan(ref) != math.isnan(data):
return False, 0.0
# Same as above for infs, but check the sign
if math.isinf(ref) and math.isinf(data):
return math.copysign(1.0, ref) == math.copysign(1.0, data), 0.0
elif math.isinf(ref) != math.isinf(data):
return False, 0.0
# Floats are equal if the absolute difference is less than epsilon times the largest.
largest = max(abs(ref), abs(data))
eps = largest * eps if largest > 1.0 else eps
return abs(ref-data) <= eps, abs(ref-data)
def value_compare_diff(ref: ScalarOrVectorValue, data: ScalarOrVectorValue, eps=FLT_EPSILON) -> Tuple[bool, float]:
if (type(data) == list or type(data) == tuple) and len(data) == 1 and type(data[0]) == type(ref):
return value_compare_diff(ref, data[0], eps)
if type(ref) == float or type(data) == float:
# if the types are different this is probably 0.0 == 0 or something. Just compare straight by casting to floats
if type(data) != type(data):
return float(data) == float(ref), abs(float(data)-float(ref))
if is_scalar(ref) and is_scalar(data):
return _scalar_compare_diff(ref, data, eps)
# Special handling for NaNs - NaNs are always equal to NaNs, but NaN is never equal to any other value
if math.isnan(ref) and math.isnan(data):
return True, 0.0
elif math.isnan(ref) != math.isnan(data):
return False, 0.0
# if we're comparing scalar to a 1-length tuple or list, compare against the first element. We only expect this for
# data where it's possibly autogenerated
if is_scalar(ref):
assert is_vector(data) and len(data) == 1
return value_compare_diff(ref, data[0], eps)
# Same as above for infs, but check the sign
if math.isinf(ref) and math.isinf(data):
return math.copysign(1.0, ref) == math.copysign(1.0, data), 0.0
elif math.isinf(ref) != math.isinf(data):
return False, 0.0
assert is_vector(ref) and is_vector(data)
# Floats are equal if the absolute difference is less than epsilon times the largest.
largest = max(abs(ref), abs(data))
eps = largest * eps if largest > 1.0 else eps
return abs(ref-data) <= eps, abs(ref-data)
elif type(ref) == list or type(ref) == tuple:
# tuples and lists can be treated interchangeably
if type(data) != list and type(data) != tuple:
return False, 0.0
# Lists/tuples are not equal if they have different lengths
if len(ref) != len(data):
return False, 0.0
# Lists are equal if they have the same length and all members have value_compare(i, j) == True
if len(ref) != len(data):
return False, 0.0
ret = (True, 0.0)
ret = (True, 0.0)
for i in range(len(ref)):
is_eq, diff_amt = value_compare_diff(ref[i], data[i], eps)
if not is_eq:
ret = (False, max(ret[1], diff_amt))
for i in range(len(ref)):
is_eq, diff_amt = value_compare_diff(ref[i], data[i], eps)
if not is_eq:
ret = (False, max(ret[1], diff_amt))
return ret
elif type(ref) == dict:
if type(data) != dict:
return False, 0.0
# Similarly, dicts are equal if both have the same set of keys and
# corresponding values are value_compare(i, j) == True
if ref.keys() != data.keys():
return False, 0.0
ret = (True, 0.0)
for i in ref.keys():
is_eq, diff_amt = value_compare_diff(ref[i], data[i], eps)
if not is_eq:
ret = (False, max(ret[1], diff_amt))
return ret
else:
# For other types, just use normal comparison
return ref == data, 0.0
return ret
def value_compare(ref, data, eps=FLT_EPSILON):
def value_compare(ref: ScalarOrVectorValue | str | None, data: ScalarOrVectorValue | str | None, eps=FLT_EPSILON):
# some simple cases that don't need diff compares and we allow for uniformity
if ref is None or data is None: return data is ref and data is None
if isinstance(ref, str) or isinstance(data, str): return ref == data
is_eq, diff_amt = value_compare_diff(ref, data, eps)
return is_eq