Pillow/Tests/helper.py
Jon Dufresne d50445ff30 Introduce isort to automate import ordering and formatting
Similar to the recent adoption of Black. isort is a Python utility to
sort imports alphabetically and automatically separate into sections. By
using isort, contributors can quickly and automatically conform to the
projects style without thinking. Just let the tool do it.

Uses the configuration recommended by the Black to avoid conflicts of
style.

Rewrite TestImageQt.test_deprecated to no rely on import order.
2019-07-06 16:11:35 -07:00

381 lines
11 KiB
Python

"""
Helper functions.
"""
from __future__ import print_function
import logging
import os
import sys
import tempfile
import unittest
from PIL import Image, ImageMath
from PIL._util import py3
logger = logging.getLogger(__name__)
HAS_UPLOADER = False
if os.environ.get("SHOW_ERRORS", None):
# local img.show for errors.
HAS_UPLOADER = True
class test_image_results:
@classmethod
def upload(self, a, b):
a.show()
b.show()
else:
try:
import test_image_results
HAS_UPLOADER = True
except ImportError:
pass
def convert_to_comparable(a, b):
new_a, new_b = a, b
if a.mode == "P":
new_a = Image.new("L", a.size)
new_b = Image.new("L", b.size)
new_a.putdata(a.getdata())
new_b.putdata(b.getdata())
elif a.mode == "I;16":
new_a = a.convert("I")
new_b = b.convert("I")
return new_a, new_b
class PillowTestCase(unittest.TestCase):
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
# holds last result object passed to run method:
self.currentResult = None
def run(self, result=None):
self.currentResult = result # remember result for use later
unittest.TestCase.run(self, result) # call superclass run method
def delete_tempfile(self, path):
try:
ok = self.currentResult.wasSuccessful()
except AttributeError: # for pytest
ok = True
if ok:
# only clean out tempfiles if test passed
try:
os.remove(path)
except OSError:
pass # report?
else:
print("=== orphaned temp file: %s" % path)
def assert_deep_equal(self, a, b, msg=None):
try:
self.assertEqual(
len(a), len(b), msg or "got length %s, expected %s" % (len(a), len(b))
)
self.assertTrue(
all(x == y for x, y in zip(a, b)), msg or "got %s, expected %s" % (a, b)
)
except Exception:
self.assertEqual(a, b, msg)
def assert_image(self, im, mode, size, msg=None):
if mode is not None:
self.assertEqual(
im.mode, mode, msg or "got mode %r, expected %r" % (im.mode, mode)
)
if size is not None:
self.assertEqual(
im.size, size, msg or "got size %r, expected %r" % (im.size, size)
)
def assert_image_equal(self, a, b, msg=None):
self.assertEqual(
a.mode, b.mode, msg or "got mode %r, expected %r" % (a.mode, b.mode)
)
self.assertEqual(
a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size)
)
if a.tobytes() != b.tobytes():
if HAS_UPLOADER:
try:
url = test_image_results.upload(a, b)
logger.error("Url for test images: %s" % url)
except Exception:
pass
self.fail(msg or "got different content")
def assert_image_equal_tofile(self, a, filename, msg=None, mode=None):
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
self.assert_image_equal(a, img, msg)
def assert_image_similar(self, a, b, epsilon, msg=None):
epsilon = float(epsilon)
self.assertEqual(
a.mode, b.mode, msg or "got mode %r, expected %r" % (a.mode, b.mode)
)
self.assertEqual(
a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size)
)
a, b = convert_to_comparable(a, b)
diff = 0
for ach, bch in zip(a.split(), b.split()):
chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L")
diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
ave_diff = float(diff) / (a.size[0] * a.size[1])
try:
self.assertGreaterEqual(
epsilon,
ave_diff,
(msg or "")
+ " average pixel value difference %.4f > epsilon %.4f"
% (ave_diff, epsilon),
)
except Exception as e:
if HAS_UPLOADER:
try:
url = test_image_results.upload(a, b)
logger.error("Url for test images: %s" % url)
except Exception:
pass
raise e
def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, mode=None):
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
self.assert_image_similar(a, img, epsilon, msg)
def assert_warning(self, warn_class, func, *args, **kwargs):
import warnings
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
# Hopefully trigger a warning.
result = func(*args, **kwargs)
# Verify some things.
if warn_class is None:
self.assertEqual(
len(w), 0, "Expected no warnings, got %s" % [v.category for v in w]
)
else:
self.assertGreaterEqual(len(w), 1)
found = False
for v in w:
if issubclass(v.category, warn_class):
found = True
break
self.assertTrue(found)
return result
def assert_all_same(self, items, msg=None):
self.assertEqual(items.count(items[0]), len(items), msg)
def assert_not_all_same(self, items, msg=None):
self.assertNotEqual(items.count(items[0]), len(items), msg)
def assert_tuple_approx_equal(self, actuals, targets, threshold, msg):
"""Tests if actuals has values within threshold from targets"""
value = True
for i, target in enumerate(targets):
value *= target - threshold <= actuals[i] <= target + threshold
self.assertTrue(value, msg + ": " + repr(actuals) + " != " + repr(targets))
def skipKnownBadTest(self, msg=None, platform=None, travis=None, interpreter=None):
# Skip if platform/travis matches, and
# PILLOW_RUN_KNOWN_BAD is not true in the environment.
if os.environ.get("PILLOW_RUN_KNOWN_BAD", False):
print(os.environ.get("PILLOW_RUN_KNOWN_BAD", False))
return
skip = True
if platform is not None:
skip = sys.platform.startswith(platform)
if travis is not None:
skip = skip and (travis == bool(os.environ.get("TRAVIS", False)))
if interpreter is not None:
skip = skip and (
interpreter == "pypy" and hasattr(sys, "pypy_version_info")
)
if skip:
self.skipTest(msg or "Known Bad Test")
def tempfile(self, template):
assert template[:5] in ("temp.", "temp_")
fd, path = tempfile.mkstemp(template[4:], template[:4])
os.close(fd)
self.addCleanup(self.delete_tempfile, path)
return path
def open_withImagemagick(self, f):
if not imagemagick_available():
raise IOError()
outfile = self.tempfile("temp.png")
if command_succeeds([IMCONVERT, f, outfile]):
return Image.open(outfile)
raise IOError()
@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS")
class PillowLeakTestCase(PillowTestCase):
# requires unix/macOS
iterations = 100 # count
mem_limit = 512 # k
def _get_mem_usage(self):
"""
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting
:returns: memory usage in kilobytes
"""
from resource import getrusage, RUSAGE_SELF
mem = getrusage(RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized (in bytes).
return mem / 1024 # Kb
else:
# linux
# man 2 getrusage
# ru_maxrss (since Linux 2.6.32)
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
def _test_leak(self, core):
start_mem = self._get_mem_usage()
for cycle in range(self.iterations):
core()
mem = self._get_mem_usage() - start_mem
msg = "memory usage limit exceeded in iteration %d" % cycle
self.assertLess(mem, self.mem_limit, msg)
# helpers
if not py3:
# Remove DeprecationWarning in Python 3
PillowTestCase.assertRaisesRegex = PillowTestCase.assertRaisesRegexp
PillowTestCase.assertRegex = PillowTestCase.assertRegexpMatches
def fromstring(data):
from io import BytesIO
return Image.open(BytesIO(data))
def tostring(im, string_format, **options):
from io import BytesIO
out = BytesIO()
im.save(out, string_format, **options)
return out.getvalue()
def hopper(mode=None, cache={}):
if mode is None:
# Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors
# what we should catch.
return Image.open("Tests/images/hopper.ppm")
# Use caching to reduce reading from disk but so an original copy is
# returned each time and the cached image isn't modified by tests
# (for fast, isolated, repeatable tests).
im = cache.get(mode)
if im is None:
if mode == "F":
im = hopper("L").convert(mode)
elif mode[:4] == "I;16":
im = hopper("I").convert(mode)
else:
im = hopper().convert(mode)
cache[mode] = im
return im.copy()
def command_succeeds(cmd):
"""
Runs the command, which must be a list of strings. Returns True if the
command succeeds, or False if an OSError was raised by subprocess.Popen.
"""
import subprocess
with open(os.devnull, "wb") as f:
try:
subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT)
except OSError:
return False
return True
def djpeg_available():
return command_succeeds(["djpeg", "-version"])
def cjpeg_available():
return command_succeeds(["cjpeg", "-version"])
def netpbm_available():
return command_succeeds(["ppmquant", "--version"]) and command_succeeds(
["ppmtogif", "--version"]
)
def imagemagick_available():
return IMCONVERT and command_succeeds([IMCONVERT, "-version"])
def on_appveyor():
return "APPVEYOR" in os.environ
if sys.platform == "win32":
IMCONVERT = os.environ.get("MAGICK_HOME", "")
if IMCONVERT:
IMCONVERT = os.path.join(IMCONVERT, "convert.exe")
else:
IMCONVERT = "convert"
def distro():
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
for line in f:
if "ID=" in line:
return line.strip().split("=")[1]
class cached_property(object):
def __init__(self, func):
self.func = func
def __get__(self, instance, cls=None):
result = instance.__dict__[self.func.__name__] = self.func(instance)
return result