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)