From 89cb6e41089629dbdb1b46be2faa57a041619d82 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 2 Feb 2016 21:58:47 -0800 Subject: [PATCH 1/9] Import image testing code from vispy --- pyqtgraph/tests/image_testing.py | 434 +++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 pyqtgraph/tests/image_testing.py diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py new file mode 100644 index 00000000..b7283d5a --- /dev/null +++ b/pyqtgraph/tests/image_testing.py @@ -0,0 +1,434 @@ +# Image-based testing borrowed from vispy + +""" +Procedure for unit-testing with images: + +1. Run unit tests at least once; this initializes a git clone of + pyqtgraph/test-data in ~/.pyqtgraph. + +2. Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set: + + $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotItem.py + + Any failing tests will + display the test results, standard image, and the differences between the + two. If the test result is bad, then press (f)ail. If the test result is + good, then press (p)ass and the new image will be saved to the test-data + directory. + +3. After adding or changing test images, create a new commit: + + $ cd ~/.pyqtgraph/test-data + $ git add ... + $ git commit -a + +4. Look up the most recent tag name from the `test_data_tag` variable in + get_test_data_repo() below. Increment the tag name by 1 in the function + and create a new tag in the test-data repository: + + $ git tag test-data-NNN + $ git push --tags origin master + + This tag is used to ensure that each pyqtgraph commit is linked to a specific + commit in the test-data repository. This makes it possible to push new + commits to the test-data repository without interfering with existing + tests, and also allows unit tests to continue working on older pyqtgraph + versions. + + Finally, update the tag name in ``get_test_data_repo`` to the new name. + +""" + +import time +import os +import sys +import inspect +import base64 +from subprocess import check_call, CalledProcessError +import numpy as np + +from ..ext.six.moves import http_client as httplib +from ..ext.six.moves import urllib_parse as urllib +from .. import scene, config +from ..util import run_subprocess + + +tester = None + + +def _get_tester(): + global tester + if tester is None: + tester = ImageTester() + return tester + + +def assert_image_approved(image, standard_file, message=None, **kwargs): + """Check that an image test result matches a pre-approved standard. + + If the result does not match, then the user can optionally invoke a GUI + to compare the images and decide whether to fail the test or save the new + image as the standard. + + This function will automatically clone the test-data repository into + ~/.pyqtgraph/test-data. However, it is up to the user to ensure this repository + is kept up to date and to commit/push new images after they are saved. + + Run the test with the environment variable PYQTGRAPH_AUDIT=1 to bring up + the auditing GUI. + + Parameters + ---------- + image : (h, w, 4) ndarray + standard_file : str + The name of the approved test image to check against. This file name + is relative to the root of the pyqtgraph test-data repository and will + be automatically fetched. + message : str + A string description of the image. It is recommended to describe + specific features that an auditor should look for when deciding whether + to fail a test. + + Extra keyword arguments are used to set the thresholds for automatic image + comparison (see ``assert_image_match()``). + """ + + if message is None: + code = inspect.currentframe().f_back.f_code + message = "%s::%s" % (code.co_filename, code.co_name) + + # Make sure we have a test data repo available, possibly invoking git + data_path = get_test_data_repo() + + # Read the standard image if it exists + std_file = os.path.join(data_path, standard_file) + if not os.path.isfile(std_file): + std_image = None + else: + std_image = read_png(std_file) + + # If the test image does not match, then we go to audit if requested. + try: + if image.shape != std_image.shape: + # Allow im1 to be an integer multiple larger than im2 to account + # for high-resolution displays + ims1 = np.array(image.shape).astype(float) + ims2 = np.array(std_image.shape).astype(float) + sr = ims1 / ims2 + if (sr[0] != sr[1] or not np.allclose(sr, np.round(sr)) or + sr[0] < 1): + raise TypeError("Test result shape %s is not an integer factor" + " larger than standard image shape %s." % + (ims1, ims2)) + sr = np.round(sr).astype(int) + image = downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) + + assert_image_match(image, std_image, **kwargs) + except Exception: + if standard_file in git_status(data_path): + print("\n\nWARNING: unit test failed against modified standard " + "image %s.\nTo revert this file, run `cd %s; git checkout " + "%s`\n" % (std_file, data_path, standard_file)) + if os.getenv('PYQTGRAPH_AUDIT') == '1': + sys.excepthook(*sys.exc_info()) + _get_tester().test(image, std_image, message) + std_path = os.path.dirname(std_file) + print('Saving new standard image to "%s"' % std_file) + if not os.path.isdir(std_path): + os.makedirs(std_path) + write_png(std_file, image) + else: + if std_image is None: + raise Exception("Test standard %s does not exist." % std_file) + else: + if os.getenv('TRAVIS') is not None: + _save_failed_test(image, std_image, standard_file) + raise + + +def assert_image_match(im1, im2, min_corr=0.9, px_threshold=50., + px_count=None, max_px_diff=None, avg_px_diff=None, + img_diff=None): + """Check that two images match. + + Images that differ in shape or dtype will fail unconditionally. + Further tests for similarity depend on the arguments supplied. + + Parameters + ---------- + im1 : (h, w, 4) ndarray + Test output image + im2 : (h, w, 4) ndarray + Test standard image + min_corr : float or None + Minimum allowed correlation coefficient between corresponding image + values (see numpy.corrcoef) + px_threshold : float + Minimum value difference at which two pixels are considered different + px_count : int or None + Maximum number of pixels that may differ + max_px_diff : float or None + Maximum allowed difference between pixels + avg_px_diff : float or None + Average allowed difference between pixels + img_diff : float or None + Maximum allowed summed difference between images + + """ + assert im1.ndim == 3 + assert im1.shape[2] == 4 + assert im1.dtype == im2.dtype + + diff = im1.astype(float) - im2.astype(float) + if img_diff is not None: + assert np.abs(diff).sum() <= img_diff + + pxdiff = diff.max(axis=2) # largest value difference per pixel + mask = np.abs(pxdiff) >= px_threshold + if px_count is not None: + assert mask.sum() <= px_count + + masked_diff = diff[mask] + if max_px_diff is not None and masked_diff.size > 0: + assert masked_diff.max() <= max_px_diff + if avg_px_diff is not None and masked_diff.size > 0: + assert masked_diff.mean() <= avg_px_diff + + if min_corr is not None: + with np.errstate(invalid='ignore'): + corr = np.corrcoef(im1.ravel(), im2.ravel())[0, 1] + assert corr >= min_corr + + +def _save_failed_test(data, expect, filename): + from ..io import _make_png + commit, error = run_subprocess(['git', 'rev-parse', 'HEAD']) + name = filename.split('/') + name.insert(-1, commit.strip()) + filename = '/'.join(name) + host = 'data.pyqtgraph.org' + + # concatenate data, expect, and diff into a single image + ds = data.shape + es = expect.shape + + shape = (max(ds[0], es[0]) + 4, ds[1] + es[1] + 8 + max(ds[1], es[1]), 4) + img = np.empty(shape, dtype=np.ubyte) + img[..., :3] = 100 + img[..., 3] = 255 + + img[2:2+ds[0], 2:2+ds[1], :ds[2]] = data + img[2:2+es[0], ds[1]+4:ds[1]+4+es[1], :es[2]] = expect + + diff = make_diff_image(data, expect) + img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff + + png = _make_png(img) + conn = httplib.HTTPConnection(host) + req = urllib.urlencode({'name': filename, + 'data': base64.b64encode(png)}) + conn.request('POST', '/upload.py', req) + response = conn.getresponse().read() + conn.close() + print("\nImage comparison failed. Test result: %s %s Expected result: " + "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + print("Uploaded to: \nhttp://%s/data/%s" % (host, filename)) + if not response.startswith(b'OK'): + print("WARNING: Error uploading data to %s" % host) + print(response) + + +def make_diff_image(im1, im2): + """Return image array showing the differences between im1 and im2. + + Handles images of different shape. Alpha channels are not compared. + """ + ds = im1.shape + es = im2.shape + + diff = np.empty((max(ds[0], es[0]), max(ds[1], es[1]), 4), dtype=int) + diff[..., :3] = 128 + diff[..., 3] = 255 + diff[:ds[0], :ds[1], :min(ds[2], 3)] += im1[..., :3] + diff[:es[0], :es[1], :min(es[2], 3)] -= im2[..., :3] + diff = np.clip(diff, 0, 255).astype(np.ubyte) + return diff + + +class ImageTester(QtGui.QWidget): + """Graphical interface for auditing image comparison tests. + """ + def __init__(self): + self.lastKey = None + + QtGui.QWidget.__init__(self) + + layout = QtGui.QGridLayout() + self.setLayout(self.layout) + + view = GraphicsLayoutWidget() + self.layout.addWidget(view, 0, 0, 1, 2) + + self.label = QtGui.QLabel() + self.layout.addWidget(self.label, 1, 0, 1, 2) + + #self.passBtn = QtGui.QPushButton('Pass') + #self.failBtn = QtGui.QPushButton('Fail') + #self.layout.addWidget(self.passBtn, 2, 0) + #self.layout.addWidget(self.failBtn, 2, 0) + + self.views = (self.view.addViewBox(row=0, col=0), + self.view.addViewBox(row=0, col=1), + self.view.addViewBox(row=0, col=2)) + labelText = ['test output', 'standard', 'diff'] + for i, v in enumerate(self.views): + v.setAspectLocked(1) + v.invertY() + v.image = ImageItem() + v.addItem(v.image) + v.label = TextItem(labelText[i]) + + self.views[1].setXLink(self.views[0]) + self.views[2].setXLink(self.views[0]) + + def test(self, im1, im2, message): + self.show() + if im2 is None: + message += 'Image1: %s %s Image2: [no standard]' % (im1.shape, im1.dtype) + im2 = np.zeros((1, 1, 3), dtype=np.ubyte) + else: + message += 'Image1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) + self.label.setText(message) + + self.views[0].image.setImage(im1) + self.views[1].image.setImage(im2) + diff = make_diff_image(im1, im2) + + self.views[2].image.setImage(diff) + self.views[0].autoRange() + + while True: + self.app.process_events() + lastKey = self.lastKey + self.lastKey = None + if lastKey is None: + pass + elif lastKey.lower() == 'p': + break + elif lastKey.lower() in ('f', 'esc'): + raise Exception("User rejected test result.") + time.sleep(0.03) + + for v in self.views: + v.image.setImage(np.zeros((1, 1, 3), dtype=np.ubyte)) + + def keyPressEvent(self, event): + self.lastKey = event.text() + + +def get_test_data_repo(): + """Return the path to a git repository with the required commit checked + out. + + If the repository does not exist, then it is cloned from + https://github.com/vispy/test-data. If the repository already exists + then the required commit is checked out. + """ + + # This tag marks the test-data commit that this version of vispy should + # be tested against. When adding or changing test images, create + # and push a new tag and update this variable. + test_data_tag = 'test-data-4' + + data_path = config['test_data_path'] + git_path = 'https://github.com/pyqtgraph/test-data' + gitbase = git_cmd_base(data_path) + + if os.path.isdir(data_path): + # Already have a test-data repository to work with. + + # Get the commit ID of test_data_tag. Do a fetch if necessary. + try: + tag_commit = git_commit_id(data_path, test_data_tag) + except NameError: + cmd = gitbase + ['fetch', '--tags', 'origin'] + print(' '.join(cmd)) + check_call(cmd) + try: + tag_commit = git_commit_id(data_path, test_data_tag) + except NameError: + raise Exception("Could not find tag '%s' in test-data repo at" + " %s" % (test_data_tag, data_path)) + except Exception: + if not os.path.exists(os.path.join(data_path, '.git')): + raise Exception("Directory '%s' does not appear to be a git " + "repository. Please remove this directory." % + data_path) + else: + raise + + # If HEAD is not the correct commit, then do a checkout + if git_commit_id(data_path, 'HEAD') != tag_commit: + print("Checking out test-data tag '%s'" % test_data_tag) + check_call(gitbase + ['checkout', test_data_tag]) + + else: + print("Attempting to create git clone of test data repo in %s.." % + data_path) + + parent_path = os.path.split(data_path)[0] + if not os.path.isdir(parent_path): + os.makedirs(parent_path) + + if os.getenv('TRAVIS') is not None: + # Create a shallow clone of the test-data repository (to avoid + # downloading more data than is necessary) + os.makedirs(data_path) + cmds = [ + gitbase + ['init'], + gitbase + ['remote', 'add', 'origin', git_path], + gitbase + ['fetch', '--tags', 'origin', test_data_tag, + '--depth=1'], + gitbase + ['checkout', '-b', 'master', 'FETCH_HEAD'], + ] + else: + # Create a full clone + cmds = [['git', 'clone', git_path, data_path]] + + for cmd in cmds: + print(' '.join(cmd)) + rval = check_call(cmd) + if rval == 0: + continue + raise RuntimeError("Test data path '%s' does not exist and could " + "not be created with git. Either create a git " + "clone of %s or set the test_data_path " + "variable to an existing clone." % + (data_path, git_path)) + + return data_path + + +def git_cmd_base(path): + return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path] + + +def git_status(path): + """Return a string listing all changes to the working tree in a git + repository. + """ + cmd = git_cmd_base(path) + ['status', '--porcelain'] + return run_subprocess(cmd, stderr=None, universal_newlines=True)[0] + + +def git_commit_id(path, ref): + """Return the commit id of *ref* in the git repository at *path*. + """ + cmd = git_cmd_base(path) + ['show', ref] + try: + output = run_subprocess(cmd, stderr=None, universal_newlines=True)[0] + except CalledProcessError: + raise NameError("Unknown git reference '%s'" % ref) + commit = output.split('\n')[0] + assert commit[:7] == 'commit ' + return commit[7:] From f2a72bf78049312050309fd1e9a4e51fd5208955 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 12 Feb 2016 03:03:52 -0800 Subject: [PATCH 2/9] Image tester is working --- pyqtgraph/functions.py | 5 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 8 + .../graphicsItems/tests/test_PlotCurveItem.py | 28 ++ pyqtgraph/tests/__init__.py | 1 + pyqtgraph/tests/image_testing.py | 321 +++++++++++------- 5 files changed, 245 insertions(+), 118 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py create mode 100644 pyqtgraph/tests/__init__.py diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 894d33e5..ad398079 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1179,10 +1179,9 @@ def imageToArray(img, copy=False, transpose=True): # If this works on all platforms, then there is no need to use np.asarray.. arr = np.frombuffer(ptr, np.ubyte, img.byteCount()) + arr = arr.reshape(img.height(), img.width(), 4) if fmt == img.Format_RGB32: - arr = arr.reshape(img.height(), img.width(), 3) - elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: - arr = arr.reshape(img.height(), img.width(), 4) + arr[...,3] = 255 if copy: arr = arr.copy() diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 3d3e969d..d66a8a99 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -126,10 +126,18 @@ class PlotCurveItem(GraphicsObject): ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: + # include complete data range + # first try faster nanmin/max function, then cut out infs if needed. b = (np.nanmin(d), np.nanmax(d)) + if any(np.isinf(b)): + mask = np.isfinite(d) + d = d[mask] + b = (d.min(), d.max()) + elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: + # include a percentile of data range mask = np.isfinite(d) d = d[mask] b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py new file mode 100644 index 00000000..56722848 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -0,0 +1,28 @@ +import numpy as np +import pyqtgraph as pg +from pyqtgraph.tests import assertImageApproved + + +def test_PlotCurveItem(): + p = pg.plot() + p.resize(200, 150) + data = np.array([1,4,2,3,np.inf,5,7,6,-np.inf,8,10,9,np.nan,-1,-2,0]) + c = pg.PlotCurveItem(data) + p.addItem(c) + p.autoRange() + + assertImageApproved(p, 'plotcurveitem/connectall', "Plot curve with all points connected.") + + c.setData(data, connect='pairs') + assertImageApproved(p, 'plotcurveitem/connectpairs', "Plot curve with pairs connected.") + + c.setData(data, connect='finite') + assertImageApproved(p, 'plotcurveitem/connectfinite', "Plot curve with finite points connected.") + + c.setData(data, connect=np.array([1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,0])) + assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.") + + + +if __name__ == '__main__': + test_PlotCurveItem() diff --git a/pyqtgraph/tests/__init__.py b/pyqtgraph/tests/__init__.py new file mode 100644 index 00000000..7a6e1173 --- /dev/null +++ b/pyqtgraph/tests/__init__.py @@ -0,0 +1 @@ +from .image_testing import assertImageApproved diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index b7283d5a..622ab0f0 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -22,8 +22,8 @@ Procedure for unit-testing with images: $ git add ... $ git commit -a -4. Look up the most recent tag name from the `test_data_tag` variable in - get_test_data_repo() below. Increment the tag name by 1 in the function +4. Look up the most recent tag name from the `testDataTag` variable in + getTestDataRepo() below. Increment the tag name by 1 in the function and create a new tag in the test-data repository: $ git tag test-data-NNN @@ -35,7 +35,7 @@ Procedure for unit-testing with images: tests, and also allows unit tests to continue working on older pyqtgraph versions. - Finally, update the tag name in ``get_test_data_repo`` to the new name. + Finally, update the tag name in ``getTestDataRepo`` to the new name. """ @@ -44,26 +44,36 @@ import os import sys import inspect import base64 -from subprocess import check_call, CalledProcessError +from subprocess import check_call, check_output, CalledProcessError import numpy as np -from ..ext.six.moves import http_client as httplib -from ..ext.six.moves import urllib_parse as urllib -from .. import scene, config -from ..util import run_subprocess +#from ..ext.six.moves import http_client as httplib +#from ..ext.six.moves import urllib_parse as urllib +import httplib +import urllib +from ..Qt import QtGui, QtCore +from .. import functions as fn +from .. import GraphicsLayoutWidget +from .. import ImageItem, TextItem + + +# This tag marks the test-data commit that this version of vispy should +# be tested against. When adding or changing test images, create +# and push a new tag and update this variable. +testDataTag = 'test-data-2' tester = None -def _get_tester(): +def getTester(): global tester if tester is None: tester = ImageTester() return tester -def assert_image_approved(image, standard_file, message=None, **kwargs): +def assertImageApproved(image, standardFile, message=None, **kwargs): """Check that an image test result matches a pre-approved standard. If the result does not match, then the user can optionally invoke a GUI @@ -80,7 +90,7 @@ def assert_image_approved(image, standard_file, message=None, **kwargs): Parameters ---------- image : (h, w, 4) ndarray - standard_file : str + standardFile : str The name of the approved test image to check against. This file name is relative to the root of the pyqtgraph test-data repository and will be automatically fetched. @@ -90,30 +100,39 @@ def assert_image_approved(image, standard_file, message=None, **kwargs): to fail a test. Extra keyword arguments are used to set the thresholds for automatic image - comparison (see ``assert_image_match()``). + comparison (see ``assertImageMatch()``). """ + if isinstance(image, QtGui.QWidget): + w = image + image = np.zeros((w.height(), w.width(), 4), dtype=np.ubyte) + qimg = fn.makeQImage(image, alpha=True, copy=False, transpose=False) + painter = QtGui.QPainter(qimg) + w.render(painter) + painter.end() if message is None: code = inspect.currentframe().f_back.f_code message = "%s::%s" % (code.co_filename, code.co_name) # Make sure we have a test data repo available, possibly invoking git - data_path = get_test_data_repo() + dataPath = getTestDataRepo() # Read the standard image if it exists - std_file = os.path.join(data_path, standard_file) - if not os.path.isfile(std_file): - std_image = None + stdFileName = os.path.join(dataPath, standardFile + '.png') + if not os.path.isfile(stdFileName): + stdImage = None else: - std_image = read_png(std_file) + pxm = QtGui.QPixmap() + pxm.load(stdFileName) + stdImage = fn.imageToArray(pxm.toImage(), copy=True, transpose=False) # If the test image does not match, then we go to audit if requested. try: - if image.shape != std_image.shape: + if image.shape != stdImage.shape: # Allow im1 to be an integer multiple larger than im2 to account # for high-resolution displays ims1 = np.array(image.shape).astype(float) - ims2 = np.array(std_image.shape).astype(float) + ims2 = np.array(stdImage.shape).astype(float) sr = ims1 / ims2 if (sr[0] != sr[1] or not np.allclose(sr, np.round(sr)) or sr[0] < 1): @@ -123,32 +142,34 @@ def assert_image_approved(image, standard_file, message=None, **kwargs): sr = np.round(sr).astype(int) image = downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) - assert_image_match(image, std_image, **kwargs) + assertImageMatch(image, stdImage, **kwargs) except Exception: - if standard_file in git_status(data_path): + if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " "image %s.\nTo revert this file, run `cd %s; git checkout " - "%s`\n" % (std_file, data_path, standard_file)) + "%s`\n" % (stdFileName, dataPath, standardFile)) if os.getenv('PYQTGRAPH_AUDIT') == '1': sys.excepthook(*sys.exc_info()) - _get_tester().test(image, std_image, message) - std_path = os.path.dirname(std_file) - print('Saving new standard image to "%s"' % std_file) - if not os.path.isdir(std_path): - os.makedirs(std_path) - write_png(std_file, image) + getTester().test(image, stdImage, message) + stdPath = os.path.dirname(stdFileName) + print('Saving new standard image to "%s"' % stdFileName) + if not os.path.isdir(stdPath): + os.makedirs(stdPath) + img = fn.makeQImage(image, alpha=True, copy=False, transpose=False) + img.save(stdFileName) else: - if std_image is None: - raise Exception("Test standard %s does not exist." % std_file) + if stdImage is None: + raise Exception("Test standard %s does not exist. Set " + "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) else: if os.getenv('TRAVIS') is not None: - _save_failed_test(image, std_image, standard_file) + saveFailedTest(image, stdImage, standardFile) raise -def assert_image_match(im1, im2, min_corr=0.9, px_threshold=50., - px_count=None, max_px_diff=None, avg_px_diff=None, - img_diff=None): +def assertImageMatch(im1, im2, minCorr=0.9, pxThreshold=50., + pxCount=None, maxPxDiff=None, avgPxDiff=None, + imgDiff=None): """Check that two images match. Images that differ in shape or dtype will fail unconditionally. @@ -160,18 +181,18 @@ def assert_image_match(im1, im2, min_corr=0.9, px_threshold=50., Test output image im2 : (h, w, 4) ndarray Test standard image - min_corr : float or None + minCorr : float or None Minimum allowed correlation coefficient between corresponding image values (see numpy.corrcoef) - px_threshold : float + pxThreshold : float Minimum value difference at which two pixels are considered different - px_count : int or None + pxCount : int or None Maximum number of pixels that may differ - max_px_diff : float or None + maxPxDiff : float or None Maximum allowed difference between pixels - avg_px_diff : float or None + avgPxDiff : float or None Average allowed difference between pixels - img_diff : float or None + imgDiff : float or None Maximum allowed summed difference between images """ @@ -180,29 +201,30 @@ def assert_image_match(im1, im2, min_corr=0.9, px_threshold=50., assert im1.dtype == im2.dtype diff = im1.astype(float) - im2.astype(float) - if img_diff is not None: - assert np.abs(diff).sum() <= img_diff + if imgDiff is not None: + assert np.abs(diff).sum() <= imgDiff pxdiff = diff.max(axis=2) # largest value difference per pixel - mask = np.abs(pxdiff) >= px_threshold - if px_count is not None: - assert mask.sum() <= px_count + mask = np.abs(pxdiff) >= pxThreshold + if pxCount is not None: + assert mask.sum() <= pxCount - masked_diff = diff[mask] - if max_px_diff is not None and masked_diff.size > 0: - assert masked_diff.max() <= max_px_diff - if avg_px_diff is not None and masked_diff.size > 0: - assert masked_diff.mean() <= avg_px_diff + maskedDiff = diff[mask] + if maxPxDiff is not None and maskedDiff.size > 0: + assert maskedDiff.max() <= maxPxDiff + if avgPxDiff is not None and maskedDiff.size > 0: + assert maskedDiff.mean() <= avgPxDiff - if min_corr is not None: + if minCorr is not None: with np.errstate(invalid='ignore'): corr = np.corrcoef(im1.ravel(), im2.ravel())[0, 1] - assert corr >= min_corr + assert corr >= minCorr -def _save_failed_test(data, expect, filename): - from ..io import _make_png - commit, error = run_subprocess(['git', 'rev-parse', 'HEAD']) +def saveFailedTest(data, expect, filename): + """Upload failed test images to web server to allow CI test debugging. + """ + commit, error = check_output(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) @@ -220,7 +242,7 @@ def _save_failed_test(data, expect, filename): img[2:2+ds[0], 2:2+ds[1], :ds[2]] = data img[2:2+es[0], ds[1]+4:ds[1]+4+es[1], :es[2]] = expect - diff = make_diff_image(data, expect) + diff = makeDiffImage(data, expect) img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = _make_png(img) @@ -238,7 +260,7 @@ def _save_failed_test(data, expect, filename): print(response) -def make_diff_image(im1, im2): +def makeDiffImage(im1, im2): """Return image array showing the differences between im1 and im2. Handles images of different shape. Alpha channels are not compared. @@ -262,20 +284,25 @@ class ImageTester(QtGui.QWidget): self.lastKey = None QtGui.QWidget.__init__(self) + self.resize(1200, 800) + self.showFullScreen() - layout = QtGui.QGridLayout() + self.layout = QtGui.QGridLayout() self.setLayout(self.layout) - view = GraphicsLayoutWidget() - self.layout.addWidget(view, 0, 0, 1, 2) + self.view = GraphicsLayoutWidget() + self.layout.addWidget(self.view, 0, 0, 1, 2) self.label = QtGui.QLabel() self.layout.addWidget(self.label, 1, 0, 1, 2) + self.label.setWordWrap(True) + font = QtGui.QFont("monospace", 14, QtGui.QFont.Bold) + self.label.setFont(font) - #self.passBtn = QtGui.QPushButton('Pass') - #self.failBtn = QtGui.QPushButton('Fail') - #self.layout.addWidget(self.passBtn, 2, 0) - #self.layout.addWidget(self.failBtn, 2, 0) + self.passBtn = QtGui.QPushButton('Pass') + self.failBtn = QtGui.QPushButton('Fail') + self.layout.addWidget(self.passBtn, 2, 0) + self.layout.addWidget(self.failBtn, 2, 1) self.views = (self.view.addViewBox(row=0, col=0), self.view.addViewBox(row=0, col=1), @@ -285,48 +312,61 @@ class ImageTester(QtGui.QWidget): v.setAspectLocked(1) v.invertY() v.image = ImageItem() + v.image.setAutoDownsample(True) v.addItem(v.image) v.label = TextItem(labelText[i]) + v.setBackgroundColor(0.5) self.views[1].setXLink(self.views[0]) + self.views[1].setYLink(self.views[0]) self.views[2].setXLink(self.views[0]) + self.views[2].setYLink(self.views[0]) def test(self, im1, im2, message): + """Ask the user to decide whether an image test passes or fails. + + This method displays the test image, reference image, and the difference + between the two. It then blocks until the user selects the test output + by clicking a pass/fail button or typing p/f. If the user fails the test, + then an exception is raised. + """ self.show() if im2 is None: - message += 'Image1: %s %s Image2: [no standard]' % (im1.shape, im1.dtype) + message += '\nImage1: %s %s Image2: [no standard]' % (im1.shape, im1.dtype) im2 = np.zeros((1, 1, 3), dtype=np.ubyte) else: - message += 'Image1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) + message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) self.label.setText(message) - self.views[0].image.setImage(im1) - self.views[1].image.setImage(im2) - diff = make_diff_image(im1, im2) + self.views[0].image.setImage(im1.transpose(1, 0, 2)) + self.views[1].image.setImage(im2.transpose(1, 0, 2)) + diff = makeDiffImage(im1, im2).transpose(1, 0, 2) self.views[2].image.setImage(diff) self.views[0].autoRange() while True: - self.app.process_events() + QtGui.QApplication.processEvents() lastKey = self.lastKey + self.lastKey = None - if lastKey is None: - pass - elif lastKey.lower() == 'p': - break - elif lastKey.lower() in ('f', 'esc'): + if lastKey in ('f', 'esc') or not self.isVisible(): raise Exception("User rejected test result.") + elif lastKey == 'p': + break time.sleep(0.03) for v in self.views: v.image.setImage(np.zeros((1, 1, 3), dtype=np.ubyte)) def keyPressEvent(self, event): - self.lastKey = event.text() + if event.key() == QtCore.Qt.Key_Escape: + self.lastKey = 'esc' + else: + self.lastKey = str(event.text()).lower() -def get_test_data_repo(): +def getTestDataRepo(): """Return the path to a git repository with the required commit checked out. @@ -334,66 +374,62 @@ def get_test_data_repo(): https://github.com/vispy/test-data. If the repository already exists then the required commit is checked out. """ + global testDataTag - # This tag marks the test-data commit that this version of vispy should - # be tested against. When adding or changing test images, create - # and push a new tag and update this variable. - test_data_tag = 'test-data-4' + dataPath = os.path.expanduser('~/.pyqtgraph/test-data') + gitPath = 'https://github.com/pyqtgraph/test-data' + gitbase = gitCmdBase(dataPath) - data_path = config['test_data_path'] - git_path = 'https://github.com/pyqtgraph/test-data' - gitbase = git_cmd_base(data_path) - - if os.path.isdir(data_path): + if os.path.isdir(dataPath): # Already have a test-data repository to work with. - # Get the commit ID of test_data_tag. Do a fetch if necessary. + # Get the commit ID of testDataTag. Do a fetch if necessary. try: - tag_commit = git_commit_id(data_path, test_data_tag) + tagCommit = gitCommitId(dataPath, testDataTag) except NameError: cmd = gitbase + ['fetch', '--tags', 'origin'] print(' '.join(cmd)) check_call(cmd) try: - tag_commit = git_commit_id(data_path, test_data_tag) + tagCommit = gitCommitId(dataPath, testDataTag) except NameError: raise Exception("Could not find tag '%s' in test-data repo at" - " %s" % (test_data_tag, data_path)) + " %s" % (testDataTag, dataPath)) except Exception: - if not os.path.exists(os.path.join(data_path, '.git')): + if not os.path.exists(os.path.join(dataPath, '.git')): raise Exception("Directory '%s' does not appear to be a git " "repository. Please remove this directory." % - data_path) + dataPath) else: raise # If HEAD is not the correct commit, then do a checkout - if git_commit_id(data_path, 'HEAD') != tag_commit: - print("Checking out test-data tag '%s'" % test_data_tag) - check_call(gitbase + ['checkout', test_data_tag]) + if gitCommitId(dataPath, 'HEAD') != tagCommit: + print("Checking out test-data tag '%s'" % testDataTag) + check_call(gitbase + ['checkout', testDataTag]) else: print("Attempting to create git clone of test data repo in %s.." % - data_path) + dataPath) - parent_path = os.path.split(data_path)[0] - if not os.path.isdir(parent_path): - os.makedirs(parent_path) + parentPath = os.path.split(dataPath)[0] + if not os.path.isdir(parentPath): + os.makedirs(parentPath) if os.getenv('TRAVIS') is not None: # Create a shallow clone of the test-data repository (to avoid # downloading more data than is necessary) - os.makedirs(data_path) + os.makedirs(dataPath) cmds = [ gitbase + ['init'], - gitbase + ['remote', 'add', 'origin', git_path], - gitbase + ['fetch', '--tags', 'origin', test_data_tag, + gitbase + ['remote', 'add', 'origin', gitPath], + gitbase + ['fetch', '--tags', 'origin', testDataTag, '--depth=1'], gitbase + ['checkout', '-b', 'master', 'FETCH_HEAD'], ] else: # Create a full clone - cmds = [['git', 'clone', git_path, data_path]] + cmds = [['git', 'clone', gitPath, dataPath]] for cmd in cmds: print(' '.join(cmd)) @@ -401,34 +437,89 @@ def get_test_data_repo(): if rval == 0: continue raise RuntimeError("Test data path '%s' does not exist and could " - "not be created with git. Either create a git " - "clone of %s or set the test_data_path " - "variable to an existing clone." % - (data_path, git_path)) + "not be created with git. Please create a git " + "clone of %s at this path." % + (dataPath, gitPath)) - return data_path + return dataPath -def git_cmd_base(path): +def gitCmdBase(path): return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path] -def git_status(path): +def gitStatus(path): """Return a string listing all changes to the working tree in a git repository. """ - cmd = git_cmd_base(path) + ['status', '--porcelain'] - return run_subprocess(cmd, stderr=None, universal_newlines=True)[0] + cmd = gitCmdBase(path) + ['status', '--porcelain'] + return check_output(cmd, stderr=None, universal_newlines=True) -def git_commit_id(path, ref): +def gitCommitId(path, ref): """Return the commit id of *ref* in the git repository at *path*. """ - cmd = git_cmd_base(path) + ['show', ref] + cmd = gitCmdBase(path) + ['show', ref] try: - output = run_subprocess(cmd, stderr=None, universal_newlines=True)[0] + output = check_output(cmd, stderr=None, universal_newlines=True) except CalledProcessError: + print(cmd) raise NameError("Unknown git reference '%s'" % ref) commit = output.split('\n')[0] assert commit[:7] == 'commit ' return commit[7:] + + +#import subprocess +#def run_subprocess(command, return_code=False, **kwargs): + #"""Run command using subprocess.Popen + + #Run command and wait for command to complete. If the return code was zero + #then return, otherwise raise CalledProcessError. + #By default, this will also add stdout= and stderr=subproces.PIPE + #to the call to Popen to suppress printing to the terminal. + + #Parameters + #---------- + #command : list of str + #Command to run as subprocess (see subprocess.Popen documentation). + #return_code : bool + #If True, the returncode will be returned, and no error checking + #will be performed (so this function should always return without + #error). + #**kwargs : dict + #Additional kwargs to pass to ``subprocess.Popen``. + + #Returns + #------- + #stdout : str + #Stdout returned by the process. + #stderr : str + #Stderr returned by the process. + #code : int + #The command exit code. Only returned if ``return_code`` is True. + #""" + ## code adapted with permission from mne-python + #use_kwargs = dict(stderr=subprocess.PIPE, stdout=subprocess.PIPE) + #use_kwargs.update(kwargs) + + #p = subprocess.Popen(command, **use_kwargs) + #output = p.communicate() + + ## communicate() may return bytes, str, or None depending on the kwargs + ## passed to Popen(). Convert all to unicode str: + #output = ['' if s is None else s for s in output] + #output = [s.decode('utf-8') if isinstance(s, bytes) else s for s in output] + #output = tuple(output) + + #if not return_code and p.returncode: + #print(output[0]) + #print(output[1]) + #err_fun = subprocess.CalledProcessError.__init__ + #if 'output' in inspect.getargspec(err_fun).args: + #raise subprocess.CalledProcessError(p.returncode, command, output) + #else: + #raise subprocess.CalledProcessError(p.returncode, command) + #if return_code: + #output = output + (p.returncode,) + #return output From 879f341913190c17553750f30aafaca50c37e14c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 12 Feb 2016 17:51:34 -0800 Subject: [PATCH 3/9] fix: no check_output in py 2.6 --- pyqtgraph/tests/image_testing.py | 100 ++++++++++++++----------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 622ab0f0..0a91b036 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -44,7 +44,7 @@ import os import sys import inspect import base64 -from subprocess import check_call, check_output, CalledProcessError +import subprocess as sp import numpy as np #from ..ext.six.moves import http_client as httplib @@ -224,7 +224,7 @@ def assertImageMatch(im1, im2, minCorr=0.9, pxThreshold=50., def saveFailedTest(data, expect, filename): """Upload failed test images to web server to allow CI test debugging. """ - commit, error = check_output(['git', 'rev-parse', 'HEAD']) + commit, error = runSubprocess(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) @@ -389,7 +389,7 @@ def getTestDataRepo(): except NameError: cmd = gitbase + ['fetch', '--tags', 'origin'] print(' '.join(cmd)) - check_call(cmd) + sp.check_call(cmd) try: tagCommit = gitCommitId(dataPath, testDataTag) except NameError: @@ -406,7 +406,7 @@ def getTestDataRepo(): # If HEAD is not the correct commit, then do a checkout if gitCommitId(dataPath, 'HEAD') != tagCommit: print("Checking out test-data tag '%s'" % testDataTag) - check_call(gitbase + ['checkout', testDataTag]) + sp.check_call(gitbase + ['checkout', testDataTag]) else: print("Attempting to create git clone of test data repo in %s.." % @@ -433,7 +433,7 @@ def getTestDataRepo(): for cmd in cmds: print(' '.join(cmd)) - rval = check_call(cmd) + rval = sp.check_call(cmd) if rval == 0: continue raise RuntimeError("Test data path '%s' does not exist and could " @@ -453,7 +453,7 @@ def gitStatus(path): repository. """ cmd = gitCmdBase(path) + ['status', '--porcelain'] - return check_output(cmd, stderr=None, universal_newlines=True) + return runSubprocess(cmd, stderr=None, universal_newlines=True) def gitCommitId(path, ref): @@ -461,8 +461,8 @@ def gitCommitId(path, ref): """ cmd = gitCmdBase(path) + ['show', ref] try: - output = check_output(cmd, stderr=None, universal_newlines=True) - except CalledProcessError: + output = runSubprocess(cmd, stderr=None, universal_newlines=True) + except sp.CalledProcessError: print(cmd) raise NameError("Unknown git reference '%s'" % ref) commit = output.split('\n')[0] @@ -470,56 +470,46 @@ def gitCommitId(path, ref): return commit[7:] -#import subprocess -#def run_subprocess(command, return_code=False, **kwargs): - #"""Run command using subprocess.Popen +def runSubprocess(command, return_code=False, **kwargs): + """Run command using subprocess.Popen + + Similar to subprocess.check_output(), which is not available in 2.6. - #Run command and wait for command to complete. If the return code was zero - #then return, otherwise raise CalledProcessError. - #By default, this will also add stdout= and stderr=subproces.PIPE - #to the call to Popen to suppress printing to the terminal. + Run command and wait for command to complete. If the return code was zero + then return, otherwise raise CalledProcessError. + By default, this will also add stdout= and stderr=subproces.PIPE + to the call to Popen to suppress printing to the terminal. - #Parameters - #---------- - #command : list of str - #Command to run as subprocess (see subprocess.Popen documentation). - #return_code : bool - #If True, the returncode will be returned, and no error checking - #will be performed (so this function should always return without - #error). - #**kwargs : dict - #Additional kwargs to pass to ``subprocess.Popen``. + Parameters + ---------- + command : list of str + Command to run as subprocess (see subprocess.Popen documentation). + **kwargs : dict + Additional kwargs to pass to ``subprocess.Popen``. - #Returns - #------- - #stdout : str - #Stdout returned by the process. - #stderr : str - #Stderr returned by the process. - #code : int - #The command exit code. Only returned if ``return_code`` is True. - #""" - ## code adapted with permission from mne-python - #use_kwargs = dict(stderr=subprocess.PIPE, stdout=subprocess.PIPE) - #use_kwargs.update(kwargs) + Returns + ------- + stdout : str + Stdout returned by the process. + """ + # code adapted with permission from mne-python + use_kwargs = dict(stderr=None, stdout=sp.PIPE) + use_kwargs.update(kwargs) - #p = subprocess.Popen(command, **use_kwargs) - #output = p.communicate() + p = sp.Popen(command, **use_kwargs) + output = p.communicate()[0] - ## communicate() may return bytes, str, or None depending on the kwargs - ## passed to Popen(). Convert all to unicode str: - #output = ['' if s is None else s for s in output] - #output = [s.decode('utf-8') if isinstance(s, bytes) else s for s in output] - #output = tuple(output) + # communicate() may return bytes, str, or None depending on the kwargs + # passed to Popen(). Convert all to unicode str: + output = '' if output is None else output + output = output.decode('utf-8') if isinstance(output, bytes) else output - #if not return_code and p.returncode: - #print(output[0]) - #print(output[1]) - #err_fun = subprocess.CalledProcessError.__init__ - #if 'output' in inspect.getargspec(err_fun).args: - #raise subprocess.CalledProcessError(p.returncode, command, output) - #else: - #raise subprocess.CalledProcessError(p.returncode, command) - #if return_code: - #output = output + (p.returncode,) - #return output + if p.returncode != 0: + print(output) + err_fun = sp.CalledProcessError.__init__ + if 'output' in inspect.getargspec(err_fun).args: + raise sp.CalledProcessError(p.returncode, command, output) + else: + raise sp.CalledProcessError(p.returncode, command) + + return output From ebe422969e6d3403cfb809da6c2f7ab9d687ffe0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 13 Feb 2016 19:49:50 -0800 Subject: [PATCH 4/9] fix py3 imports --- pyqtgraph/tests/image_testing.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 0a91b036..75a83a7e 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -47,10 +47,12 @@ import base64 import subprocess as sp import numpy as np -#from ..ext.six.moves import http_client as httplib -#from ..ext.six.moves import urllib_parse as urllib -import httplib -import urllib +if sys.version[0] >= '3': + import http.client as httplib + import urllib.parse as urllib +else: + import httplib + import urllib from ..Qt import QtGui, QtCore from .. import functions as fn from .. import GraphicsLayoutWidget From e0a5dae1d5a8609ebe6b5bfa5fbb5291ebdc6092 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Feb 2016 12:56:11 -0800 Subject: [PATCH 5/9] Made default image comparison more strict. --- pyqtgraph/tests/image_testing.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 75a83a7e..16ed14d9 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -8,7 +8,7 @@ Procedure for unit-testing with images: 2. Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set: - $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotItem.py + $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py Any failing tests will display the test results, standard image, and the differences between the @@ -59,7 +59,7 @@ from .. import GraphicsLayoutWidget from .. import ImageItem, TextItem -# This tag marks the test-data commit that this version of vispy should +# This tag marks the test-data commit that this version of pyqtgraph should # be tested against. When adding or changing test images, create # and push a new tag and update this variable. testDataTag = 'test-data-2' @@ -169,14 +169,17 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): raise -def assertImageMatch(im1, im2, minCorr=0.9, pxThreshold=50., - pxCount=None, maxPxDiff=None, avgPxDiff=None, +def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., + pxCount=0, maxPxDiff=None, avgPxDiff=None, imgDiff=None): """Check that two images match. Images that differ in shape or dtype will fail unconditionally. Further tests for similarity depend on the arguments supplied. + By default, images may have no pixels that gave a value difference greater + than 50. + Parameters ---------- im1 : (h, w, 4) ndarray From 5171e1f1c7d1b2d0e010dcebb7b392d17b221c60 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Feb 2016 13:13:56 -0800 Subject: [PATCH 6/9] Remove axes from plotcurveitem test--fonts differ across platforms. --- pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py | 8 +++++--- pyqtgraph/tests/image_testing.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py index 56722848..e2a641e0 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -4,13 +4,15 @@ from pyqtgraph.tests import assertImageApproved def test_PlotCurveItem(): - p = pg.plot() + p = pg.GraphicsWindow() + v = p.addViewBox() p.resize(200, 150) data = np.array([1,4,2,3,np.inf,5,7,6,-np.inf,8,10,9,np.nan,-1,-2,0]) c = pg.PlotCurveItem(data) - p.addItem(c) - p.autoRange() + v.addItem(c) + v.autoRange() + assert np.allclose(v.viewRange(), [[-1.1457564053237301, 16.145756405323731], [-3.076811473165955, 11.076811473165955]]) assertImageApproved(p, 'plotcurveitem/connectall', "Plot curve with all points connected.") c.setData(data, connect='pairs') diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 16ed14d9..4dbc2b82 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -62,7 +62,7 @@ from .. import ImageItem, TextItem # This tag marks the test-data commit that this version of pyqtgraph should # be tested against. When adding or changing test images, create # and push a new tag and update this variable. -testDataTag = 'test-data-2' +testDataTag = 'test-data-3' tester = None From e712b86a3891086ac97ba1431d0098a676e552ad Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Feb 2016 13:29:20 -0800 Subject: [PATCH 7/9] relax auto-range check --- pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py index e2a641e0..17f5894b 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -12,7 +12,10 @@ def test_PlotCurveItem(): v.addItem(c) v.autoRange() - assert np.allclose(v.viewRange(), [[-1.1457564053237301, 16.145756405323731], [-3.076811473165955, 11.076811473165955]]) + # Check auto-range works. Some platform differences may be expected.. + checkRange = np.array([[-1.1457564053237301, 16.145756405323731], [-3.076811473165955, 11.076811473165955]]) + assert np.all(np.abs(np.array(v.viewRange()) - checkRange) < 0.1) + assertImageApproved(p, 'plotcurveitem/connectall', "Plot curve with all points connected.") c.setData(data, connect='pairs') From 0bdc89fa69828dbed6ee02aac93bf97079dd8c84 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Feb 2016 14:28:13 -0800 Subject: [PATCH 8/9] correction for plotcurveitem tests on osx --- pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py index 17f5894b..a3c34b11 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -5,6 +5,7 @@ from pyqtgraph.tests import assertImageApproved def test_PlotCurveItem(): p = pg.GraphicsWindow() + p.ci.layout.setContentsMargins(4, 4, 4, 4) # default margins vary by platform v = p.addViewBox() p.resize(200, 150) data = np.array([1,4,2,3,np.inf,5,7,6,-np.inf,8,10,9,np.nan,-1,-2,0]) @@ -14,7 +15,7 @@ def test_PlotCurveItem(): # Check auto-range works. Some platform differences may be expected.. checkRange = np.array([[-1.1457564053237301, 16.145756405323731], [-3.076811473165955, 11.076811473165955]]) - assert np.all(np.abs(np.array(v.viewRange()) - checkRange) < 0.1) + assert np.allclose(v.viewRange(), checkRange) assertImageApproved(p, 'plotcurveitem/connectall', "Plot curve with all points connected.") From 926fe1ec26c79fc46ccf97be18e04c60efea9ea8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 17 Feb 2016 08:38:22 -0800 Subject: [PATCH 9/9] image tester corrections --- pyqtgraph/tests/image_testing.py | 45 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 4dbc2b82..5d05c2c3 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -22,9 +22,9 @@ Procedure for unit-testing with images: $ git add ... $ git commit -a -4. Look up the most recent tag name from the `testDataTag` variable in - getTestDataRepo() below. Increment the tag name by 1 in the function - and create a new tag in the test-data repository: +4. Look up the most recent tag name from the `testDataTag` global variable + below. Increment the tag name by 1 and create a new tag in the test-data + repository: $ git tag test-data-NNN $ git push --tags origin master @@ -35,10 +35,15 @@ Procedure for unit-testing with images: tests, and also allows unit tests to continue working on older pyqtgraph versions. - Finally, update the tag name in ``getTestDataRepo`` to the new name. - """ + +# This is the name of a tag in the test-data repository that this version of +# pyqtgraph should be tested against. When adding or changing test images, +# create and push a new tag and update this variable. +testDataTag = 'test-data-3' + + import time import os import sys @@ -59,12 +64,6 @@ from .. import GraphicsLayoutWidget from .. import ImageItem, TextItem -# This tag marks the test-data commit that this version of pyqtgraph should -# be tested against. When adding or changing test images, create -# and push a new tag and update this variable. -testDataTag = 'test-data-3' - - tester = None @@ -130,16 +129,19 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): # If the test image does not match, then we go to audit if requested. try: + if image.shape[2] != stdImage.shape[2]: + raise Exception("Test result has different channel count than standard image" + "(%d vs %d)" % (image.shape[2], stdImage.shape[2])) if image.shape != stdImage.shape: # Allow im1 to be an integer multiple larger than im2 to account # for high-resolution displays ims1 = np.array(image.shape).astype(float) ims2 = np.array(stdImage.shape).astype(float) - sr = ims1 / ims2 + sr = ims1 / ims2 if ims1[0] > ims2[0] else ims2 / ims1 if (sr[0] != sr[1] or not np.allclose(sr, np.round(sr)) or sr[0] < 1): raise TypeError("Test result shape %s is not an integer factor" - " larger than standard image shape %s." % + " different than standard image shape %s." % (ims1, ims2)) sr = np.round(sr).astype(int) image = downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) @@ -250,7 +252,8 @@ def saveFailedTest(data, expect, filename): diff = makeDiffImage(data, expect) img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff - png = _make_png(img) + png = makePng(img) + conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, 'data': base64.b64encode(png)}) @@ -265,6 +268,16 @@ def saveFailedTest(data, expect, filename): print(response) +def makePng(img): + """Given an array like (H, W, 4), return a PNG-encoded byte string. + """ + io = QtCore.QBuffer() + qim = fn.makeQImage(img, alpha=False) + qim.save(io, format='png') + png = io.data().data().encode() + return png + + def makeDiffImage(im1, im2): """Return image array showing the differences between im1 and im2. @@ -376,12 +389,12 @@ def getTestDataRepo(): out. If the repository does not exist, then it is cloned from - https://github.com/vispy/test-data. If the repository already exists + https://github.com/pyqtgraph/test-data. If the repository already exists then the required commit is checked out. """ global testDataTag - dataPath = os.path.expanduser('~/.pyqtgraph/test-data') + dataPath = os.path.join(os.path.expanduser('~'), '.pyqtgraph', 'test-data') gitPath = 'https://github.com/pyqtgraph/test-data' gitbase = gitCmdBase(dataPath)