From 1409c071a940b6dbfa272d0d5c1707520e7eb0de Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 9 Sep 2026 20:35:39 +0100 Subject: [PATCH] Split apart util.value_compare_diff to have better type annotation --- util/test/rdtest/util.py | 120 ++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/util/test/rdtest/util.py b/util/test/rdtest/util.py index 08034cc7b..692671ec3 100644 --- a/util/test/rdtest/util.py +++ b/util/test/rdtest/util.py @@ -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