Files
renderdoc/util/test/rdtest/util.py
T
thisisjimmyfb b1d3123583 Remote Android and Linux test support part 1
Add remote server support to test framework
2025-06-19 11:45:04 +01:00

384 lines
11 KiB
Python

import sys
import os
import re
import time
import math
import struct
import platform
import hashlib
import zipfile
import subprocess
from typing import Tuple, List
from . import png
from rdtest.remoteserver import RemoteServer, AndroidRemoteServer
# Android app IDs for the demos
ADRD_DEMO_APP32 = 'renderdoc.org.demos.arm32'
ADRD_DEMO_APP64 = 'renderdoc.org.demos.arm64'
def _timestr():
return time.strftime("%Y%m%d_%H_%M_%S", time.gmtime()) + "_" + str(round(time.time() % 1000))
# Thanks to https://stackoverflow.com/a/3431838 for this file definition
def _md5_file(fname):
hash_md5 = hashlib.md5()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
_root_dir = os.getcwd()
_artifact_dir = os.path.realpath('artifacts')
_data_dir = os.path.realpath('data')
_data_extra_dir = os.path.realpath('data_extra')
_temp_dir = os.path.realpath('tmp')
_test_name = 'Unknown_Test'
_demos_bin = os.path.realpath('demos_x64')
_demos_timeout = None
def set_root_dir(path: str):
global _root_dir
_root_dir = path
def set_data_dir(path: str):
global _data_dir
_data_dir = os.path.abspath(path)
def set_data_extra_dir(path: str):
global _data_extra_dir
_data_extra_dir = os.path.abspath(path)
def set_artifact_dir(path: str):
global _artifact_dir
_artifact_dir = os.path.abspath(path)
def set_temp_dir(path: str):
global _temp_dir
_temp_dir = os.path.abspath(path)
def set_demos_binary(path: str):
global _demos_bin
if path == "":
# Try to guess where the demos binary might be, but if we can't find it fall back to just trying it in PATH
if struct.calcsize("P") == 8:
_demos_bin = 'demos_x64'
else:
_demos_bin = 'demos_x86'
if platform.system() == 'Windows':
_demos_bin += '.exe'
# Try a few common locations
attempts = [
os.path.join(get_root_dir(), _demos_bin), # in the root (default for Windows)
os.path.join(get_root_dir(), 'build', _demos_bin), # in a build folder (potential for linux)
os.path.join(get_root_dir(), 'demos', _demos_bin), # in the demos folder (ditto)
os.path.join(get_root_dir(), 'demos', 'build', _demos_bin), # in build in the demos folder (ditto)
]
for attempt in attempts:
if os.path.exists(attempt):
_demos_bin = os.path.abspath(attempt)
break
else:
_demos_bin = os.path.abspath(path)
def set_remote_server(server: RemoteServer):
global _remote_server
_remote_server = server
def get_remote_server():
return _remote_server
def create_adb_device(name: str):
server = AndroidRemoteServer(name)
set_remote_server(server)
def get_current_test():
return _test_name
def set_demos_timeout(timeout: int):
global _demos_timeout
_demos_timeout = timeout
def set_current_test(name: str):
global _test_name
_test_name = name
def get_root_dir():
return _root_dir
def get_data_dir():
return _data_dir
def get_data_path(name: str):
return os.path.join(_data_dir, name)
def get_data_extra_dir():
return _data_extra_dir
def get_data_extra_path(name: str):
return os.path.join(_data_extra_dir, name)
def get_artifact_dir():
return _artifact_dir
def get_artifact_path(name: str):
return os.path.join(_artifact_dir, name)
def get_tmp_dir():
return _temp_dir
def get_demos_binary():
return _demos_bin
def get_demos_timeout():
return _demos_timeout
def get_tmp_path(name: str):
os.makedirs(os.path.join(_temp_dir, _test_name), exist_ok=True)
return os.path.join(_temp_dir, _test_name, name)
def get_android_demo_app_name():
if ADRD_DEMO_APP64 in get_demos_binary():
return ADRD_DEMO_APP64
return ADRD_DEMO_APP32
def sanitise_filename(name: str):
name = name.replace(_artifact_dir, '') \
.replace(get_tmp_dir(), '') \
.replace(get_root_dir(), '') \
.replace('\\', '/')
return re.sub('^/', '', name)
def linear_to_SRGB(val):
if type(val) == float:
if val <= 0.0031308:
return val * 12.92
else:
return 1.055 * math.pow(val, 1.0 / 2.4) - 0.055
return [linear_to_SRGB(v) for v in val]
def png_save(out_path: str, rows: List[bytes], dimensions: Tuple[int, int], has_alpha: bool):
try:
f = open(out_path, 'wb')
except Exception as ex:
raise FileNotFoundError("Can't open {} for write".format(sanitise_filename(out_path)))
writer = png.Writer(dimensions[0], dimensions[1], alpha=has_alpha, greyscale=False, compression=7)
writer.write(f, rows)
f.close()
def png_load_data(in_path: str):
reader = png.Reader(filename=in_path)
return list(reader.read()[2])
def png_load_dimensions(in_path: str):
reader = png.Reader(filename=in_path)
info = reader.read()
return (info[0], info[1])
def png_compare(test_img: str, ref_img: str, tolerance: int = 2):
test_reader = png.Reader(filename=test_img)
ref_reader = png.Reader(filename=ref_img)
test_w, test_h, test_data, test_info = test_reader.read()
ref_w, ref_h, ref_data, ref_info = ref_reader.read()
# lookup rgba data straight
rgba_get = lambda data, x: data[x]
# lookup rgb data and return 255 for alpha
rgb_get = lambda data, x: data[ (x >> 2)*3 + (x % 4) ] if (x % 4) < 3 else 255
test_get = (rgba_get if test_info['alpha'] else rgb_get)
ref_get = (rgba_get if ref_info['alpha'] else rgb_get)
if test_w != ref_w or test_h != test_h:
return False
is_same = True
diff_data = []
for test_row, ref_row in zip(test_data, ref_data):
diff = [min(255, abs(test_get(test_row, i) - ref_get(ref_row, i))*4) for i in range(0, test_w*4)]
is_same = is_same and not any([d > tolerance*4 for d in diff])
diff_data.append([255 if i % 4 == 3 else d for i, d in enumerate(diff)])
if is_same:
return True
# If the diff fails, dump the difference to a file
diff_file = get_tmp_path('diff.png')
if os.path.exists(diff_file):
os.remove(diff_file)
png_save(diff_file, diff_data, (test_w, test_h), True)
return False
def md5_compare(test_file: str, ref_file: str):
return _md5_file(test_file) == _md5_file(ref_file)
def zip_compare(test_file: str, ref_file: str):
test = zipfile.ZipFile(test_file)
ref = zipfile.ZipFile(ref_file)
test_files = []
for file in test.infolist():
hash_md5 = hashlib.md5()
with test.open(file.filename) as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
test_files.append((file.filename, file.file_size, hash_md5.hexdigest()))
ref_files = []
for file in ref.infolist():
hash_md5 = hashlib.md5()
with test.open(file.filename) as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
ref_files.append((file.filename, file.file_size, hash_md5.hexdigest()))
test.close()
ref.close()
return test_files == ref_files
# Use the 32-bit float epsilon, not sys.float_info.epsilon which is for double floats
FLT_EPSILON = 2.0*1.19209290E-07
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
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))
# 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)
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 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)
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
def value_compare(ref, data, eps=FLT_EPSILON):
is_eq, diff_amt = value_compare_diff(ref, data, eps)
return is_eq
def run_demo_blocking(args: [str], timeout=100):
"""
Executes the demo application with the given args and returns the stdout.
:return: stdout, on Android this includes stripping of logcat prefixes
"""
if get_remote_server() is None:
raw = subprocess.run([get_demos_binary()] + args, stdout=subprocess.PIPE).stdout
return str(raw, 'utf-8')
return get_remote_server().run_demos(args, timeout)
def target_path_exists(path: str, timeout=10):
if get_remote_server() is None:
return os.path.exists(path)
return get_remote_server().path_exists(path)