Move and Update test-data repo into pyqtgraph repo

To reduce complexity, and make it easier to add more images and tests,
the images in the `test-data` repository should be merged with the main
repository.  Furthermore, we can remove a lot of the subprocess work in
the image_testing.py file, as we no longer need to have it interact with
git.

The images are not the same.  Images were regenerated with Qt6, and now
have proper big and little endian handling thanks to @pijyoi

Second commit is a slightly modified variant of
2e135ab282d6007b34a3854921be54d0e9efb241 authored by @pijyoi
it is to convert qimages to RGBA8888 for testing.  Image
files were regenerated images for the big/little handling

Fixed issue with bogus test from test_NonUniformImage and generated a
new image
This commit is contained in:
Ogi Moore 2021-05-27 21:57:07 -07:00
parent e29f86578f
commit a6971c768d
127 changed files with 125 additions and 332 deletions

View File

@ -40,11 +40,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Checkout test-data
uses: actions/checkout@v2
with:
repository: pyqtgraph/test-data
path: .pyqtgraph/test-data
- name: Setup Python ${{ matrix.python-version }} - name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
@ -106,7 +101,7 @@ jobs:
- name: Run Tests - name: Run Tests
run: | run: |
mkdir $SCREENSHOT_DIR mkdir $SCREENSHOT_DIR
pytest pyqtgraph examples -v \ pytest tests examples -v \
--junitxml pytest.xml \ --junitxml pytest.xml \
shell: bash shell: bash
- name: Upload Screenshots - name: Upload Screenshots

View File

@ -1511,17 +1511,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
return ndarray_to_qimage(imgData, imgFormat) return ndarray_to_qimage(imgData, imgFormat)
def imageToArray(img, copy=False, transpose=True): def qimage_to_ndarray(qimg):
""" img_ptr = qimg.bits()
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
fmt = img.format()
img_ptr = img.bits()
if QT_LIB.startswith('PyQt'): if hasattr(img_ptr, 'setsize'): # PyQt sip.voidptr
# sizeInBytes() was introduced in Qt 5.10 # sizeInBytes() was introduced in Qt 5.10
# however PyQt5 5.12 will fail with: # however PyQt5 5.12 will fail with:
# "TypeError: QImage.sizeInBytes() is a private method" # "TypeError: QImage.sizeInBytes() is a private method"
@ -1529,14 +1522,37 @@ def imageToArray(img, copy=False, transpose=True):
# PyQt5 5.15, PySide2 5.12, PySide2 5.15 # PyQt5 5.15, PySide2 5.12, PySide2 5.15
try: try:
# 64-bits size # 64-bits size
nbytes = img.sizeInBytes() nbytes = qimg.sizeInBytes()
except (TypeError, AttributeError): except (TypeError, AttributeError):
# 32-bits size # 32-bits size
nbytes = img.byteCount() nbytes = qimg.byteCount()
img_ptr.setsize(nbytes) img_ptr.setsize(nbytes)
arr = np.frombuffer(img_ptr, dtype=np.ubyte) depth = qimg.depth()
arr = arr.reshape(img.height(), img.width(), 4) if depth in (8, 24, 32):
dtype = np.uint8
nchan = depth // 8
elif depth in (16, 64):
dtype = np.uint16
nchan = depth // 16
else:
raise ValueError("Unsupported Image Type")
shape = qimg.height(), qimg.width()
if nchan != 1:
shape = shape + (nchan,)
return np.frombuffer(img_ptr, dtype=dtype).reshape(shape)
def imageToArray(img, copy=False, transpose=True):
"""
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
arr = qimage_to_ndarray(img)
fmt = img.format()
if fmt == img.Format_RGB32: if fmt == img.Format_RGB32:
arr[...,3] = 255 arr[...,3] = 255

View File

@ -4,7 +4,7 @@ xvfb_height = 1080
# use this due to some issues with ndarray reshape errors on CI systems # use this due to some issues with ndarray reshape errors on CI systems
xvfb_colordepth = 24 xvfb_colordepth = 24
xvfb_args=-ac +extension GLX +render xvfb_args=-ac +extension GLX +render
faulthandler_timeout = 30 faulthandler_timeout = 60
filterwarnings = filterwarnings =
# re-enable standard library warnings # re-enable standard library warnings

View File

@ -5,7 +5,7 @@ import pytest
from pyqtgraph.Qt import QtCore, QtGui, QtTest from pyqtgraph.Qt import QtCore, QtGui, QtTest
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.tests import assertImageApproved, TransposedImageItem from tests.image_testing import assertImageApproved, TransposedImageItem
try: try:
import cupy import cupy
except ImportError: except ImportError:

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtTest from pyqtgraph.Qt import QtGui, QtCore, QtTest
from pyqtgraph.tests import mouseDrag, mouseMove from tests.ui_testing import mouseDrag, mouseMove
pg.mkQApp() pg.mkQApp()

View File

@ -2,7 +2,7 @@ import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import QtTest from pyqtgraph.Qt import QtTest
from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage
from pyqtgraph.tests import assertImageApproved from tests.image_testing import assertImageApproved
from pyqtgraph.colormap import ColorMap from pyqtgraph.colormap import ColorMap
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import pytest import pytest
@ -93,7 +93,7 @@ def test_NonUniformImage_colormap():
image = NonUniformImage(x, y, Z, border=fn.mkPen('g')) image = NonUniformImage(x, y, Z, border=fn.mkPen('g'))
cmap = ColorMap(pos=[0.0, 1.0], color=[(0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)]) cmap = ColorMap(pos=[0.0, 1.0], color=[(0, 0, 0), (255, 255, 255)])
image.setColorMap(cmap) image.setColorMap(cmap)
viewbox.addItem(image) viewbox.addItem(image)

View File

@ -1,6 +1,6 @@
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.tests import assertImageApproved from tests.image_testing import assertImageApproved
def test_PlotCurveItem(): def test_PlotCurveItem():

View File

@ -3,9 +3,10 @@ import sys
import numpy as np import numpy as np
import pytest import pytest
import pyqtgraph as pg import pyqtgraph as pg
import platform
from pyqtgraph.Qt import QtCore, QtGui, QtTest from pyqtgraph.Qt import QtCore, QtGui, QtTest
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow from tests.image_testing import assertImageApproved
import pytest from tests.ui_testing import mouseMove, mouseDrag, mouseClick, resizeWindow
app = pg.mkQApp() app = pg.mkQApp()
pg.setConfigOption("mouseRateLimit", 0) pg.setConfigOption("mouseRateLimit", 0)
@ -39,17 +40,20 @@ def test_getArrayRegion_axisorder():
def check_getArrayRegion(roi, name, testResize=True, transpose=False): def check_getArrayRegion(roi, name, testResize=True, transpose=False):
# on windows, edges corner pixels seem to be slightly different from other platforms
# giving a pxCount=2 for a fudge factor
if isinstance(roi, (pg.ROI, pg.RectROI)) and platform.system() == "Windows":
pxCount = 2
else:
pxCount=-1
initState = roi.getState() initState = roi.getState()
#win = pg.GraphicsLayoutWidget()
win = pg.GraphicsView() win = pg.GraphicsView()
win.show() win.show()
resizeWindow(win, 200, 400) resizeWindow(win, 200, 400)
# Don't use Qt's layouts for testing--these generate unpredictable results. # Don't use Qt's layouts for testing--these generate unpredictable results.
#vb1 = win.addViewBox()
#win.nextRow()
#vb2 = win.addViewBox()
# Instead, place the viewboxes manually # Instead, place the viewboxes manually
vb1 = pg.ViewBox() vb1 = pg.ViewBox()
win.scene().addItem(vb1) win.scene().addItem(vb1)
@ -97,7 +101,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
vb2.enableAutoRange(True, True) vb2.enableAutoRange(True, True)
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.') assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.', pxCount=pxCount)
with pytest.raises(TypeError): with pytest.raises(TypeError):
roi.setPos(0, False) roi.setPos(0, False)
@ -106,38 +110,33 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.') assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.', pxCount=pxCount)
roi.setAngle(45) roi.setAngle(45)
roi.setPos([3, 0]) roi.setPos([3, 0])
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.') assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.', pxCount=pxCount)
if testResize: if testResize:
roi.setSize([60, 60]) roi.setSize([60, 60])
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.') assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.', pxCount=pxCount)
img1.setPos(0, img1.height()) img1.setPos(0, img1.height())
img1.setTransform(QtGui.QTransform().scale(1, -1).rotate(20), True) img1.setTransform(QtGui.QTransform().scale(1, -1).rotate(20), True)
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.') assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.', pxCount=pxCount)
vb1.invertY() vb1.invertY()
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
# on windows, one edge of one ROI handle is shifted slightly; letting this slide with pxCount=10
if pg.Qt.QT_LIB in {'PyQt4', 'PySide'}:
pxCount = 10
else:
pxCount=-1
assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.', pxCount=pxCount) assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.', pxCount=pxCount)
roi.setState(initState) roi.setState(initState)
@ -146,7 +145,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.') assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.', pxCount=pxCount)
# allow the roi to be re-used # allow the roi to be re-used
roi.scene().removeItem(roi) roi.scene().removeItem(roi)

View File

@ -3,68 +3,32 @@
""" """
Procedure for unit-testing with images: Procedure for unit-testing with images:
1. Run unit tests at least once; this initializes a git clone of Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set:
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_PlotCurveItem.py $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py
Any failing tests will display the test results, standard image, and the 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. 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 If the test result is good, then press (p)ass and the new image will be
saved to the test-data directory. saved to the test-data directory.
To check all test results regardless of whether the test failed, set the
environment variable PYQTGRAPH_AUDIT_ALL=1.
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 `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
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.
To check all test results regardless of whether the test failed, set the
environment variable PYQTGRAPH_AUDIT_ALL=1.
""" """
# 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. To test locally, begin
# by creating the tag in your ~/.pyqtgraph/test-data repository.
testDataTag = 'test-data-8'
import time import time
import os import os
import sys import sys
import inspect import inspect
import base64 import warnings
import subprocess as sp
import numpy as np import numpy as np
if sys.version[0] >= '3': from pathlib import Path
import http.client as httplib
import urllib.parse as urllib from pyqtgraph.Qt import QtGui, QtCore
else: from pyqtgraph import functions as fn
import httplib from pyqtgraph import GraphicsLayoutWidget
import urllib from pyqtgraph import ImageItem, TextItem
from ..Qt import QtGui, QtCore, QtTest, QT_LIB
from .. import functions as fn
from .. import GraphicsLayoutWidget
from .. import ImageItem, TextItem
tester = None tester = None
@ -101,6 +65,21 @@ def getTester():
return tester return tester
def getImageFromWidget(widget):
# just to be sure the widget size is correct (new window may be resized):
QtGui.QApplication.processEvents()
qimg = QtGui.QImage(widget.size(), QtGui.QImage.Format.Format_ARGB32)
qimg.fill(QtCore.Qt.GlobalColor.transparent)
painter = QtGui.QPainter(qimg)
widget.render(painter)
painter.end()
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
return fn.qimage_to_ndarray(qimg).copy()
def assertImageApproved(image, standardFile, message=None, **kwargs): def assertImageApproved(image, standardFile, message=None, **kwargs):
"""Check that an image test result matches a pre-approved standard. """Check that an image test result matches a pre-approved standard.
@ -108,10 +87,6 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
to compare the images and decide whether to fail the test or save the new to compare the images and decide whether to fail the test or save the new
image as the standard. 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 Run the test with the environment variable PYQTGRAPH_AUDIT=1 to bring up
the auditing GUI. the auditing GUI.
@ -131,43 +106,28 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
comparison (see ``assertImageMatch()``). comparison (see ``assertImageMatch()``).
""" """
if isinstance(image, QtGui.QWidget): if isinstance(image, QtGui.QWidget):
w = image # just to be sure the widget size is correct (new window may be resized):
# just to be sure the widget size is correct (new window may be resized):
QtGui.QApplication.processEvents() QtGui.QApplication.processEvents()
graphstate = scenegraphState(w, standardFile) graphstate = scenegraphState(image, standardFile)
qimg = QtGui.QImage(w.size(), QtGui.QImage.Format.Format_ARGB32) image = getImageFromWidget(image)
qimg.fill(QtCore.Qt.GlobalColor.transparent)
painter = QtGui.QPainter(qimg)
w.render(painter)
painter.end()
image = fn.imageToArray(qimg, copy=False, transpose=False)
# the standard images seem to have their Red and Blue swapped
if sys.byteorder == 'little':
# transpose B,G,R,A to R,G,B,A
image = image[..., [2, 1, 0, 3]]
else:
# transpose A,R,G,B to A,B,G,R
image = image[..., [0, 3, 2, 1]]
if message is None: if message is None:
code = inspect.currentframe().f_back.f_code code = inspect.currentframe().f_back.f_code
message = "%s::%s" % (code.co_filename, code.co_name) message = "%s::%s" % (code.co_filename, code.co_name)
# Make sure we have a test data repo available, possibly invoking git # Make sure we have a test data repo available
dataPath = getTestDataRepo() dataPath = getTestDataDirectory()
# Read the standard image if it exists # Read the standard image if it exists
stdFileName = os.path.join(dataPath, standardFile + '.png') stdFileName = os.path.join(dataPath, standardFile + '.png')
if not os.path.isfile(stdFileName): if not os.path.isfile(stdFileName):
stdImage = None stdImage = None
else: else:
pxm = QtGui.QPixmap() qimg = QtGui.QImage(stdFileName)
pxm.load(stdFileName) qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
stdImage = fn.imageToArray(pxm.toImage(), copy=True, transpose=False) stdImage = fn.qimage_to_ndarray(qimg).copy()
del qimg
# If the test image does not match, then we go to audit if requested. # If the test image does not match, then we go to audit if requested.
try: try:
@ -191,18 +151,13 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype)
assertImageMatch(image, stdImage, **kwargs) assertImageMatch(image, stdImage, **kwargs)
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)): if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
print(graphstate) print(graphstate)
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.") raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
except Exception: except Exception:
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" % (stdFileName, dataPath, standardFile))
if os.getenv('PYQTGRAPH_AUDIT') == '1' or os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': if os.getenv('PYQTGRAPH_AUDIT') == '1' or os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
getTester().test(image, stdImage, message) getTester().test(image, stdImage, message)
@ -210,20 +165,18 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
print('Saving new standard image to "%s"' % stdFileName) print('Saving new standard image to "%s"' % stdFileName)
if not os.path.isdir(stdPath): if not os.path.isdir(stdPath):
os.makedirs(stdPath) os.makedirs(stdPath)
img = fn.makeQImage(image, alpha=True, transpose=False) qimg = fn.ndarray_to_qimage(image, QtGui.QImage.Format.Format_RGBA8888)
img.save(stdFileName) qimg.save(stdFileName)
del qimg
else: else:
if stdImage is None: if stdImage is None:
raise Exception("Test standard %s does not exist. Set " raise Exception("Test standard %s does not exist. Set "
"PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName)
else: if os.getenv('CI') is not None:
if os.getenv('TRAVIS') is not None: standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
saveFailedTest(image, stdImage, standardFile, upload=True) saveFailedTest(image, stdImage, standardFile)
elif os.getenv('CI') is not None: print(graphstate)
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile) raise
saveFailedTest(image, stdImage, standardFile)
print(graphstate)
raise
def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
@ -249,8 +202,8 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
pxThreshold : float pxThreshold : float
Minimum value difference at which two pixels are considered different Minimum value difference at which two pixels are considered different
pxCount : int or None pxCount : int or None
Maximum number of pixels that may differ. Default is 0 for Qt4 and Maximum number of pixels that may differ. Default is 0, on Windows some
1% of image size for Qt5. tests have a value of 2.
maxPxDiff : float or None maxPxDiff : float or None
Maximum allowed difference between pixels Maximum allowed difference between pixels
avgPxDiff : float or None avgPxDiff : float or None
@ -264,12 +217,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
assert im1.dtype == im2.dtype assert im1.dtype == im2.dtype
if pxCount == -1: if pxCount == -1:
if QT_LIB in {'PyQt5', 'PySide2', 'PySide6', 'PyQt6'}: pxCount = 0
# Qt5 generates slightly different results; relax the tolerance
# until test images are updated.
pxCount = int(im1.shape[0] * im1.shape[1] * 0.01)
else:
pxCount = 0
diff = im1.astype(float) - im2.astype(float) diff = im1.astype(float) - im2.astype(float)
if imgDiff is not None: if imgDiff is not None:
@ -292,9 +240,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
assert corr >= minCorr assert corr >= minCorr
def saveFailedTest(data, expect, filename, upload=False): def saveFailedTest(data, expect, filename):
"""Upload failed test images to web server to allow CI test debugging.
"""
# concatenate data, expect, and diff into a single image # concatenate data, expect, and diff into a single image
ds = data.shape ds = data.shape
es = expect.shape es = expect.shape
@ -310,7 +256,7 @@ def saveFailedTest(data, expect, filename, upload=False):
diff = makeDiffImage(data, expect) diff = makeDiffImage(data, expect)
img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
png = makePng(img) png = makePng(data) # change `img` to `data` to save just the failed image
directory = os.path.dirname(filename) directory = os.path.dirname(filename)
if not os.path.isdir(directory): if not os.path.isdir(directory):
os.makedirs(directory) os.makedirs(directory)
@ -318,38 +264,15 @@ def saveFailedTest(data, expect, filename, upload=False):
png_file.write(png) png_file.write(png)
print("\nImage comparison failed. Test result: %s %s Expected result: " print("\nImage comparison failed. Test result: %s %s Expected result: "
"%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype))
if upload:
uploadFailedTest(filename, png)
def uploadFailedTest(filename, png):
commit = runSubprocess(['git', 'rev-parse', 'HEAD'])
name = filename.split(os.path.sep)
name.insert(-1, commit.strip())
filename = os.path.sep.join(name)
host = 'data.pyqtgraph.org'
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("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 makePng(img): def makePng(img):
"""Given an array like (H, W, 4), return a PNG-encoded byte string. """Given an array like (H, W, 4), return a PNG-encoded byte string.
""" """
io = QtCore.QBuffer() io = QtCore.QBuffer()
qim = fn.makeQImage(img.transpose(1, 0, 2), alpha=False) qim = fn.ndarray_to_qimage(img, QtGui.QImage.Format.Format_RGBX8888)
qim.save(io, 'PNG') qim.save(io, 'PNG')
png = bytes(io.data().data()) return bytes(io.data().data())
return png
def makeDiffImage(im1, im2): def makeDiffImage(im1, im2):
@ -467,155 +390,18 @@ class ImageTester(QtGui.QWidget):
def getTestDataRepo(): def getTestDataRepo():
"""Return the path to a git repository with the required commit checked warnings.warn(
out. "Test data data repo has been merged with the main repo"
"use getTestDataDirectory() instead, this method will be removed"
If the repository does not exist, then it is cloned from "in a future version of pyqtgraph",
https://github.com/pyqtgraph/test-data. If the repository already exists DeprecationWarning, stacklevel=2
then the required commit is checked out. )
""" return getTestDataDirectory()
global testDataTag
if os.getenv("CI"):
dataPath = os.path.join(os.environ["GITHUB_WORKSPACE"], '.pyqtgraph', 'test-data')
else:
dataPath = os.path.join(os.path.expanduser('~'), '.pyqtgraph', 'test-data')
gitPath = 'https://github.com/pyqtgraph/test-data'
gitbase = gitCmdBase(dataPath)
if os.path.isdir(dataPath):
# Already have a test-data repository to work with.
# Get the commit ID of testDataTag. Do a fetch if necessary.
try:
tagCommit = gitCommitId(dataPath, testDataTag)
except NameError:
cmd = gitbase + ['fetch', '--tags', 'origin']
print(' '.join(cmd))
sp.check_call(cmd)
try:
tagCommit = gitCommitId(dataPath, testDataTag)
except NameError:
raise Exception("Could not find tag '%s' in test-data repo at"
" %s" % (testDataTag, dataPath))
except Exception:
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." %
dataPath)
else:
raise
# If HEAD is not the correct commit, then do a checkout
if gitCommitId(dataPath, 'HEAD') != tagCommit:
print("Checking out test-data tag '%s'" % testDataTag)
sp.check_call(gitbase + ['checkout', testDataTag])
else:
print("Attempting to create git clone of test data repo in %s.." %
dataPath)
parentPath = os.path.split(dataPath)[0]
if not os.path.isdir(parentPath):
os.makedirs(parentPath)
if os.getenv('TRAVIS') is not None or os.getenv('CI') is not None:
# Create a shallow clone of the test-data repository (to avoid
# downloading more data than is necessary)
os.makedirs(dataPath)
cmds = [
gitbase + ['init'],
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', gitPath, dataPath]]
for cmd in cmds:
print(' '.join(cmd))
rval = sp.check_call(cmd)
if rval == 0:
continue
raise RuntimeError("Test data path '%s' does not exist and could "
"not be created with git. Please create a git "
"clone of %s at this path." %
(dataPath, gitPath))
return dataPath
def gitCmdBase(path): def getTestDataDirectory():
return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path] dataPath = Path(__file__).absolute().parent / "images"
return dataPath.as_posix()
def gitStatus(path):
"""Return a string listing all changes to the working tree in a git
repository.
"""
cmd = gitCmdBase(path) + ['status', '--porcelain']
return runSubprocess(cmd, stderr=None, universal_newlines=True)
def gitCommitId(path, ref):
"""Return the commit id of *ref* in the git repository at *path*.
"""
cmd = gitCmdBase(path) + ['show', ref]
try:
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]
assert commit[:7] == 'commit '
return commit[7:]
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.getfullargspec(err_fun).args:
raise sp.CalledProcessError(p.returncode, command, output)
else:
raise sp.CalledProcessError(p.returncode, command)
return output
def scenegraphState(view, name): def scenegraphState(view, name):
@ -632,7 +418,7 @@ def scenegraphState(view, name):
def itemState(root): def itemState(root):
state = str(root) + '\n' state = str(root) + '\n'
from .. import ViewBox from pyqtgraph import ViewBox
state += 'bounding rect: ' + str(root.boundingRect()) + '\n' state += 'bounding rect: ' + str(root.boundingRect()) + '\n'
if isinstance(root, ViewBox): if isinstance(root, ViewBox):
state += "view range: " + str(root.viewRange()) + '\n' state += "view range: " + str(root.viewRange()) + '\n'
@ -647,7 +433,7 @@ def transformStr(t):
def indent(s, pfx): def indent(s, pfx):
return '\n'.join([pfx+line for line in s.split('\n')]) return '\n'.join(pfx+line for line in s.split('\n'))
class TransposedImageItem(ImageItem): class TransposedImageItem(ImageItem):

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Some files were not shown because too many files have changed in this diff Show More