2016-02-03 05:58:47 +00:00
|
|
|
# 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:
|
|
|
|
|
2016-02-14 20:56:11 +00:00
|
|
|
$ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2016-02-17 16:38:22 +00:00
|
|
|
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:
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
$ 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.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2016-02-17 16:38:22 +00:00
|
|
|
|
|
|
|
# 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,
|
2016-06-15 04:56:25 +00:00
|
|
|
# create and push a new tag and update this variable. To test locally, begin
|
|
|
|
# by creating the tag in your ~/.pyqtgraph/test-data repository.
|
2016-05-31 01:00:19 +00:00
|
|
|
testDataTag = 'test-data-4'
|
2016-02-17 16:38:22 +00:00
|
|
|
|
|
|
|
|
2016-02-03 05:58:47 +00:00
|
|
|
import time
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import inspect
|
|
|
|
import base64
|
2016-02-13 01:51:34 +00:00
|
|
|
import subprocess as sp
|
2016-02-03 05:58:47 +00:00
|
|
|
import numpy as np
|
|
|
|
|
2016-02-14 03:49:50 +00:00
|
|
|
if sys.version[0] >= '3':
|
|
|
|
import http.client as httplib
|
|
|
|
import urllib.parse as urllib
|
|
|
|
else:
|
|
|
|
import httplib
|
|
|
|
import urllib
|
2016-02-12 11:03:52 +00:00
|
|
|
from ..Qt import QtGui, QtCore
|
|
|
|
from .. import functions as fn
|
|
|
|
from .. import GraphicsLayoutWidget
|
|
|
|
from .. import ImageItem, TextItem
|
|
|
|
|
|
|
|
|
2016-02-03 05:58:47 +00:00
|
|
|
tester = None
|
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def getTester():
|
2016-02-03 05:58:47 +00:00
|
|
|
global tester
|
|
|
|
if tester is None:
|
|
|
|
tester = ImageTester()
|
|
|
|
return tester
|
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def assertImageApproved(image, standardFile, message=None, **kwargs):
|
2016-02-03 05:58:47 +00:00
|
|
|
"""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
|
2016-02-12 11:03:52 +00:00
|
|
|
standardFile : str
|
2016-02-03 05:58:47 +00:00
|
|
|
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
|
2016-02-12 11:03:52 +00:00
|
|
|
comparison (see ``assertImageMatch()``).
|
2016-02-03 05:58:47 +00:00
|
|
|
"""
|
2016-02-12 11:03:52 +00:00
|
|
|
if isinstance(image, QtGui.QWidget):
|
|
|
|
w = image
|
2016-06-15 04:56:25 +00:00
|
|
|
graphstate = scenegraphState(w, standardFile)
|
2016-02-12 11:03:52 +00:00
|
|
|
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()
|
2016-05-09 15:56:21 +00:00
|
|
|
|
|
|
|
# transpose BGRA to RGBA
|
|
|
|
image = image[..., [2, 1, 0, 3]]
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
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
|
2016-02-12 11:03:52 +00:00
|
|
|
dataPath = getTestDataRepo()
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
# Read the standard image if it exists
|
2016-02-12 11:03:52 +00:00
|
|
|
stdFileName = os.path.join(dataPath, standardFile + '.png')
|
|
|
|
if not os.path.isfile(stdFileName):
|
|
|
|
stdImage = None
|
2016-02-03 05:58:47 +00:00
|
|
|
else:
|
2016-02-12 11:03:52 +00:00
|
|
|
pxm = QtGui.QPixmap()
|
|
|
|
pxm.load(stdFileName)
|
|
|
|
stdImage = fn.imageToArray(pxm.toImage(), copy=True, transpose=False)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
# If the test image does not match, then we go to audit if requested.
|
|
|
|
try:
|
2016-02-17 16:38:22 +00:00
|
|
|
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]))
|
2016-02-12 11:03:52 +00:00
|
|
|
if image.shape != stdImage.shape:
|
2016-02-03 05:58:47 +00:00
|
|
|
# Allow im1 to be an integer multiple larger than im2 to account
|
|
|
|
# for high-resolution displays
|
|
|
|
ims1 = np.array(image.shape).astype(float)
|
2016-02-12 11:03:52 +00:00
|
|
|
ims2 = np.array(stdImage.shape).astype(float)
|
2016-02-17 16:38:22 +00:00
|
|
|
sr = ims1 / ims2 if ims1[0] > ims2[0] else ims2 / ims1
|
2016-02-03 05:58:47 +00:00
|
|
|
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"
|
2016-02-17 16:38:22 +00:00
|
|
|
" different than standard image shape %s." %
|
2016-02-03 05:58:47 +00:00
|
|
|
(ims1, ims2))
|
|
|
|
sr = np.round(sr).astype(int)
|
2016-05-09 15:56:21 +00:00
|
|
|
image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
assertImageMatch(image, stdImage, **kwargs)
|
2016-06-15 04:56:25 +00:00
|
|
|
|
|
|
|
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
|
|
|
|
print graphstate
|
2016-02-03 05:58:47 +00:00
|
|
|
except Exception:
|
2016-02-12 11:03:52 +00:00
|
|
|
if stdFileName in gitStatus(dataPath):
|
2016-02-03 05:58:47 +00:00
|
|
|
print("\n\nWARNING: unit test failed against modified standard "
|
|
|
|
"image %s.\nTo revert this file, run `cd %s; git checkout "
|
2016-02-12 11:03:52 +00:00
|
|
|
"%s`\n" % (stdFileName, dataPath, standardFile))
|
2016-02-03 05:58:47 +00:00
|
|
|
if os.getenv('PYQTGRAPH_AUDIT') == '1':
|
|
|
|
sys.excepthook(*sys.exc_info())
|
2016-02-12 11:03:52 +00:00
|
|
|
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)
|
2016-05-09 15:56:21 +00:00
|
|
|
img = fn.makeQImage(image, alpha=True, transpose=False)
|
2016-02-12 11:03:52 +00:00
|
|
|
img.save(stdFileName)
|
2016-02-03 05:58:47 +00:00
|
|
|
else:
|
2016-02-12 11:03:52 +00:00
|
|
|
if stdImage is None:
|
|
|
|
raise Exception("Test standard %s does not exist. Set "
|
|
|
|
"PYQTGRAPH_AUDIT=1 to add this image." % stdFileName)
|
2016-02-03 05:58:47 +00:00
|
|
|
else:
|
|
|
|
if os.getenv('TRAVIS') is not None:
|
2016-02-12 11:03:52 +00:00
|
|
|
saveFailedTest(image, stdImage, standardFile)
|
2016-06-15 04:56:25 +00:00
|
|
|
print graphstate
|
2016-02-03 05:58:47 +00:00
|
|
|
raise
|
|
|
|
|
|
|
|
|
2016-02-14 20:56:11 +00:00
|
|
|
def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
|
|
|
pxCount=0, maxPxDiff=None, avgPxDiff=None,
|
2016-02-12 11:03:52 +00:00
|
|
|
imgDiff=None):
|
2016-02-03 05:58:47 +00:00
|
|
|
"""Check that two images match.
|
|
|
|
|
|
|
|
Images that differ in shape or dtype will fail unconditionally.
|
|
|
|
Further tests for similarity depend on the arguments supplied.
|
|
|
|
|
2016-02-14 20:56:11 +00:00
|
|
|
By default, images may have no pixels that gave a value difference greater
|
|
|
|
than 50.
|
|
|
|
|
2016-02-03 05:58:47 +00:00
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
im1 : (h, w, 4) ndarray
|
|
|
|
Test output image
|
|
|
|
im2 : (h, w, 4) ndarray
|
|
|
|
Test standard image
|
2016-02-12 11:03:52 +00:00
|
|
|
minCorr : float or None
|
2016-02-03 05:58:47 +00:00
|
|
|
Minimum allowed correlation coefficient between corresponding image
|
|
|
|
values (see numpy.corrcoef)
|
2016-02-12 11:03:52 +00:00
|
|
|
pxThreshold : float
|
2016-02-03 05:58:47 +00:00
|
|
|
Minimum value difference at which two pixels are considered different
|
2016-02-12 11:03:52 +00:00
|
|
|
pxCount : int or None
|
2016-02-03 05:58:47 +00:00
|
|
|
Maximum number of pixels that may differ
|
2016-02-12 11:03:52 +00:00
|
|
|
maxPxDiff : float or None
|
2016-02-03 05:58:47 +00:00
|
|
|
Maximum allowed difference between pixels
|
2016-02-12 11:03:52 +00:00
|
|
|
avgPxDiff : float or None
|
2016-02-03 05:58:47 +00:00
|
|
|
Average allowed difference between pixels
|
2016-02-12 11:03:52 +00:00
|
|
|
imgDiff : float or None
|
2016-02-03 05:58:47 +00:00
|
|
|
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)
|
2016-02-12 11:03:52 +00:00
|
|
|
if imgDiff is not None:
|
|
|
|
assert np.abs(diff).sum() <= imgDiff
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
pxdiff = diff.max(axis=2) # largest value difference per pixel
|
2016-02-12 11:03:52 +00:00
|
|
|
mask = np.abs(pxdiff) >= pxThreshold
|
|
|
|
if pxCount is not None:
|
|
|
|
assert mask.sum() <= pxCount
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
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
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
if minCorr is not None:
|
2016-02-03 05:58:47 +00:00
|
|
|
with np.errstate(invalid='ignore'):
|
|
|
|
corr = np.corrcoef(im1.ravel(), im2.ravel())[0, 1]
|
2016-02-12 11:03:52 +00:00
|
|
|
assert corr >= minCorr
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def saveFailedTest(data, expect, filename):
|
|
|
|
"""Upload failed test images to web server to allow CI test debugging.
|
|
|
|
"""
|
2016-05-31 03:28:59 +00:00
|
|
|
commit = runSubprocess(['git', 'rev-parse', 'HEAD'])
|
2016-02-03 05:58:47 +00:00
|
|
|
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
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
diff = makeDiffImage(data, expect)
|
2016-02-03 05:58:47 +00:00
|
|
|
img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
|
|
|
|
|
2016-02-17 16:38:22 +00:00
|
|
|
png = makePng(img)
|
|
|
|
|
2016-02-03 05:58:47 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2016-02-17 16:38:22 +00:00
|
|
|
def makePng(img):
|
|
|
|
"""Given an array like (H, W, 4), return a PNG-encoded byte string.
|
|
|
|
"""
|
|
|
|
io = QtCore.QBuffer()
|
2016-07-18 15:13:25 +00:00
|
|
|
qim = fn.makeQImage(img.transpose(1, 0, 2), alpha=False)
|
2016-06-05 07:15:51 +00:00
|
|
|
qim.save(io, 'PNG')
|
|
|
|
png = bytes(io.data().data())
|
2016-02-17 16:38:22 +00:00
|
|
|
return png
|
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def makeDiffImage(im1, im2):
|
2016-02-03 05:58:47 +00:00
|
|
|
"""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)
|
2016-02-12 11:03:52 +00:00
|
|
|
self.resize(1200, 800)
|
2016-05-31 01:00:19 +00:00
|
|
|
#self.showFullScreen()
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
self.layout = QtGui.QGridLayout()
|
2016-02-03 05:58:47 +00:00
|
|
|
self.setLayout(self.layout)
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
self.view = GraphicsLayoutWidget()
|
|
|
|
self.layout.addWidget(self.view, 0, 0, 1, 2)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
self.label = QtGui.QLabel()
|
|
|
|
self.layout.addWidget(self.label, 1, 0, 1, 2)
|
2016-02-12 11:03:52 +00:00
|
|
|
self.label.setWordWrap(True)
|
|
|
|
font = QtGui.QFont("monospace", 14, QtGui.QFont.Bold)
|
|
|
|
self.label.setFont(font)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
self.passBtn = QtGui.QPushButton('Pass')
|
|
|
|
self.failBtn = QtGui.QPushButton('Fail')
|
|
|
|
self.layout.addWidget(self.passBtn, 2, 0)
|
|
|
|
self.layout.addWidget(self.failBtn, 2, 1)
|
2016-05-31 01:00:19 +00:00
|
|
|
self.passBtn.clicked.connect(self.passTest)
|
|
|
|
self.failBtn.clicked.connect(self.failTest)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
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()
|
2016-02-12 11:03:52 +00:00
|
|
|
v.image.setAutoDownsample(True)
|
2016-02-03 05:58:47 +00:00
|
|
|
v.addItem(v.image)
|
|
|
|
v.label = TextItem(labelText[i])
|
2016-02-12 11:03:52 +00:00
|
|
|
v.setBackgroundColor(0.5)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
self.views[1].setXLink(self.views[0])
|
2016-02-12 11:03:52 +00:00
|
|
|
self.views[1].setYLink(self.views[0])
|
2016-02-03 05:58:47 +00:00
|
|
|
self.views[2].setXLink(self.views[0])
|
2016-02-12 11:03:52 +00:00
|
|
|
self.views[2].setYLink(self.views[0])
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
def test(self, im1, im2, message):
|
2016-02-12 11:03:52 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2016-02-03 05:58:47 +00:00
|
|
|
self.show()
|
|
|
|
if im2 is None:
|
2016-02-12 11:03:52 +00:00
|
|
|
message += '\nImage1: %s %s Image2: [no standard]' % (im1.shape, im1.dtype)
|
2016-02-03 05:58:47 +00:00
|
|
|
im2 = np.zeros((1, 1, 3), dtype=np.ubyte)
|
|
|
|
else:
|
2016-02-12 11:03:52 +00:00
|
|
|
message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype)
|
2016-02-03 05:58:47 +00:00
|
|
|
self.label.setText(message)
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
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)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
self.views[2].image.setImage(diff)
|
|
|
|
self.views[0].autoRange()
|
|
|
|
|
|
|
|
while True:
|
2016-02-12 11:03:52 +00:00
|
|
|
QtGui.QApplication.processEvents()
|
2016-02-03 05:58:47 +00:00
|
|
|
lastKey = self.lastKey
|
2016-02-12 11:03:52 +00:00
|
|
|
|
2016-02-03 05:58:47 +00:00
|
|
|
self.lastKey = None
|
2016-02-12 11:03:52 +00:00
|
|
|
if lastKey in ('f', 'esc') or not self.isVisible():
|
2016-02-03 05:58:47 +00:00
|
|
|
raise Exception("User rejected test result.")
|
2016-02-12 11:03:52 +00:00
|
|
|
elif lastKey == 'p':
|
|
|
|
break
|
2016-02-03 05:58:47 +00:00
|
|
|
time.sleep(0.03)
|
|
|
|
|
|
|
|
for v in self.views:
|
|
|
|
v.image.setImage(np.zeros((1, 1, 3), dtype=np.ubyte))
|
|
|
|
|
|
|
|
def keyPressEvent(self, event):
|
2016-02-12 11:03:52 +00:00
|
|
|
if event.key() == QtCore.Qt.Key_Escape:
|
|
|
|
self.lastKey = 'esc'
|
|
|
|
else:
|
|
|
|
self.lastKey = str(event.text()).lower()
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-05-31 01:00:19 +00:00
|
|
|
def passTest(self):
|
|
|
|
self.lastKey = 'p'
|
|
|
|
|
|
|
|
def failTest(self):
|
|
|
|
self.lastKey = 'f'
|
|
|
|
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def getTestDataRepo():
|
2016-02-03 05:58:47 +00:00
|
|
|
"""Return the path to a git repository with the required commit checked
|
|
|
|
out.
|
|
|
|
|
|
|
|
If the repository does not exist, then it is cloned from
|
2016-02-17 16:38:22 +00:00
|
|
|
https://github.com/pyqtgraph/test-data. If the repository already exists
|
2016-02-03 05:58:47 +00:00
|
|
|
then the required commit is checked out.
|
|
|
|
"""
|
2016-02-12 11:03:52 +00:00
|
|
|
global testDataTag
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-17 16:38:22 +00:00
|
|
|
dataPath = os.path.join(os.path.expanduser('~'), '.pyqtgraph', 'test-data')
|
2016-02-12 11:03:52 +00:00
|
|
|
gitPath = 'https://github.com/pyqtgraph/test-data'
|
|
|
|
gitbase = gitCmdBase(dataPath)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
if os.path.isdir(dataPath):
|
2016-02-03 05:58:47 +00:00
|
|
|
# Already have a test-data repository to work with.
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
# Get the commit ID of testDataTag. Do a fetch if necessary.
|
2016-02-03 05:58:47 +00:00
|
|
|
try:
|
2016-02-12 11:03:52 +00:00
|
|
|
tagCommit = gitCommitId(dataPath, testDataTag)
|
2016-02-03 05:58:47 +00:00
|
|
|
except NameError:
|
|
|
|
cmd = gitbase + ['fetch', '--tags', 'origin']
|
|
|
|
print(' '.join(cmd))
|
2016-02-13 01:51:34 +00:00
|
|
|
sp.check_call(cmd)
|
2016-02-03 05:58:47 +00:00
|
|
|
try:
|
2016-02-12 11:03:52 +00:00
|
|
|
tagCommit = gitCommitId(dataPath, testDataTag)
|
2016-02-03 05:58:47 +00:00
|
|
|
except NameError:
|
|
|
|
raise Exception("Could not find tag '%s' in test-data repo at"
|
2016-02-12 11:03:52 +00:00
|
|
|
" %s" % (testDataTag, dataPath))
|
2016-02-03 05:58:47 +00:00
|
|
|
except Exception:
|
2016-02-12 11:03:52 +00:00
|
|
|
if not os.path.exists(os.path.join(dataPath, '.git')):
|
2016-02-03 05:58:47 +00:00
|
|
|
raise Exception("Directory '%s' does not appear to be a git "
|
|
|
|
"repository. Please remove this directory." %
|
2016-02-12 11:03:52 +00:00
|
|
|
dataPath)
|
2016-02-03 05:58:47 +00:00
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
|
|
|
# If HEAD is not the correct commit, then do a checkout
|
2016-02-12 11:03:52 +00:00
|
|
|
if gitCommitId(dataPath, 'HEAD') != tagCommit:
|
|
|
|
print("Checking out test-data tag '%s'" % testDataTag)
|
2016-02-13 01:51:34 +00:00
|
|
|
sp.check_call(gitbase + ['checkout', testDataTag])
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
print("Attempting to create git clone of test data repo in %s.." %
|
2016-02-12 11:03:52 +00:00
|
|
|
dataPath)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
parentPath = os.path.split(dataPath)[0]
|
|
|
|
if not os.path.isdir(parentPath):
|
|
|
|
os.makedirs(parentPath)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
if os.getenv('TRAVIS') is not None:
|
|
|
|
# Create a shallow clone of the test-data repository (to avoid
|
|
|
|
# downloading more data than is necessary)
|
2016-02-12 11:03:52 +00:00
|
|
|
os.makedirs(dataPath)
|
2016-02-03 05:58:47 +00:00
|
|
|
cmds = [
|
|
|
|
gitbase + ['init'],
|
2016-02-12 11:03:52 +00:00
|
|
|
gitbase + ['remote', 'add', 'origin', gitPath],
|
|
|
|
gitbase + ['fetch', '--tags', 'origin', testDataTag,
|
2016-02-03 05:58:47 +00:00
|
|
|
'--depth=1'],
|
|
|
|
gitbase + ['checkout', '-b', 'master', 'FETCH_HEAD'],
|
|
|
|
]
|
|
|
|
else:
|
|
|
|
# Create a full clone
|
2016-02-12 11:03:52 +00:00
|
|
|
cmds = [['git', 'clone', gitPath, dataPath]]
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
for cmd in cmds:
|
|
|
|
print(' '.join(cmd))
|
2016-02-13 01:51:34 +00:00
|
|
|
rval = sp.check_call(cmd)
|
2016-02-03 05:58:47 +00:00
|
|
|
if rval == 0:
|
|
|
|
continue
|
|
|
|
raise RuntimeError("Test data path '%s' does not exist and could "
|
2016-02-12 11:03:52 +00:00
|
|
|
"not be created with git. Please create a git "
|
|
|
|
"clone of %s at this path." %
|
|
|
|
(dataPath, gitPath))
|
2016-02-03 05:58:47 +00:00
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
return dataPath
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def gitCmdBase(path):
|
2016-02-03 05:58:47 +00:00
|
|
|
return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path]
|
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def gitStatus(path):
|
2016-02-03 05:58:47 +00:00
|
|
|
"""Return a string listing all changes to the working tree in a git
|
|
|
|
repository.
|
|
|
|
"""
|
2016-02-12 11:03:52 +00:00
|
|
|
cmd = gitCmdBase(path) + ['status', '--porcelain']
|
2016-02-13 01:51:34 +00:00
|
|
|
return runSubprocess(cmd, stderr=None, universal_newlines=True)
|
2016-02-03 05:58:47 +00:00
|
|
|
|
|
|
|
|
2016-02-12 11:03:52 +00:00
|
|
|
def gitCommitId(path, ref):
|
2016-02-03 05:58:47 +00:00
|
|
|
"""Return the commit id of *ref* in the git repository at *path*.
|
|
|
|
"""
|
2016-02-12 11:03:52 +00:00
|
|
|
cmd = gitCmdBase(path) + ['show', ref]
|
2016-02-03 05:58:47 +00:00
|
|
|
try:
|
2016-02-13 01:51:34 +00:00
|
|
|
output = runSubprocess(cmd, stderr=None, universal_newlines=True)
|
|
|
|
except sp.CalledProcessError:
|
2016-02-12 11:03:52 +00:00
|
|
|
print(cmd)
|
2016-02-03 05:58:47 +00:00
|
|
|
raise NameError("Unknown git reference '%s'" % ref)
|
|
|
|
commit = output.split('\n')[0]
|
|
|
|
assert commit[:7] == 'commit '
|
|
|
|
return commit[7:]
|
2016-02-12 11:03:52 +00:00
|
|
|
|
|
|
|
|
2016-02-13 01:51:34 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
# code adapted with permission from mne-python
|
|
|
|
use_kwargs = dict(stderr=None, stdout=sp.PIPE)
|
|
|
|
use_kwargs.update(kwargs)
|
|
|
|
|
|
|
|
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 output is None else output
|
|
|
|
output = output.decode('utf-8') if isinstance(output, bytes) else 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
|
2016-06-15 04:56:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
def scenegraphState(view, name):
|
|
|
|
"""Return information about the scenegraph for debugging test failures.
|
|
|
|
"""
|
|
|
|
state = "====== Scenegraph state for %s ======\n" % name
|
|
|
|
state += "view size: %dx%d\n" % (view.width(), view.height())
|
|
|
|
state += "view transform:\n" + indent(transformStr(view.transform()), " ")
|
|
|
|
for item in view.scene().items():
|
|
|
|
if item.parentItem() is None:
|
|
|
|
state += itemState(item) + '\n'
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def itemState(root):
|
|
|
|
state = str(root) + '\n'
|
|
|
|
from .. import ViewBox
|
|
|
|
state += 'bounding rect: ' + str(root.boundingRect()) + '\n'
|
|
|
|
if isinstance(root, ViewBox):
|
|
|
|
state += "view range: " + str(root.viewRange()) + '\n'
|
|
|
|
state += "transform:\n" + indent(transformStr(root.transform()).strip(), " ") + '\n'
|
|
|
|
for item in root.childItems():
|
|
|
|
state += indent(itemState(item).strip(), " ") + '\n'
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
|
|
def transformStr(t):
|
|
|
|
return ("[%0.2f %0.2f %0.2f]\n"*3) % (t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), t.m31(), t.m32(), t.m33())
|
|
|
|
|
|
|
|
|
|
|
|
def indent(s, pfx):
|
|
|
|
return '\n'.join([pfx+line for line in s.split('\n')])
|