1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
|
import sys
import os
import re
import time
import hashlib
import zipfile
from PIL import Image, ImageChops, ImageStat
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'
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_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_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 sanitise_filename(name: str):
name = name.replace(_artifact_dir, '') \
.replace(get_tmp_dir(), '') \
.replace(get_root_dir(), '') \
.replace('\\', '/')
return re.sub('^/', '', name)
def image_compare(test_img: str, ref_img: str, tolerance: int = 2):
try:
out = Image.open(test_img)
except Exception as ex:
raise FileNotFoundError("Can't open {}".format(sanitise_filename(test_img)))
try:
ref = Image.open(ref_img)
except Exception as ex:
raise FileNotFoundError("Can't open {}".format(sanitise_filename(ref_img)))
if out.mode != ref.mode or out.size != ref.size:
return False
# Generate the difference
diff = ImageChops.difference(out, ref)
# Subtract N from the difference, to allow for off-by-N errors that can be caused by rounding.
# For example clearing to 0.5, 0.5, 0.5 has two valid representations: 127,127,127 and 128,128,128
# which are equally far from true 0.5.
diff = ImageChops.subtract(diff, Image.new(diff.mode, (diff.width, diff.height),
(tolerance, tolerance, tolerance, tolerance)))
# 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)
if sum(ImageStat.Stat(diff).sum) > 0:
# this does (img1 + img2) / scale, so scale=0.5 means we multiply the image by 2/0.5 = 4
diff = ImageChops.add(diff, diff, scale=0.5)
diff.convert("RGB").save(diff_file)
return False
return True
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(ref, data):
if type(ref) == float:
if type(data) != float:
return False
# Floats are equal if the absolute difference is less than epsilon times the largest.
largest = max(abs(ref), abs(data))
eps = largest * FLT_EPSILON if largest > 1.0 else FLT_EPSILON
return abs(ref-data) < eps
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
# 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
for i in range(len(ref)):
if not value_compare(ref[i], data[i]):
return False
return True
elif type(ref) == dict:
if type(data) != dict:
return False
# 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
for i in ref.keys():
if not value_compare(ref[i], data[i]):
return False
return True
else:
# For other types, just use normal comparison
return ref == data
|