Merge pull request #1807 from j9ac9k/merge-test-data
Merge test data + make tests directory which contain tests
19
.github/workflows/main.yml
vendored
@ -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:
|
||||||
@ -69,10 +64,10 @@ jobs:
|
|||||||
shell: cmd
|
shell: cmd
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pip
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
pip install ${{ matrix.qt-version }} numpy${{ matrix.numpy-version }} scipy pyopengl h5py matplotlib numba
|
python -m pip install ${{ matrix.qt-version }} numpy${{ matrix.numpy-version }} scipy pyopengl h5py matplotlib numba
|
||||||
pip install .
|
python -m pip install --use-feature=in-tree-build .
|
||||||
pip install pytest
|
python -m pip install pytest
|
||||||
- name: "Install Linux VirtualDisplay"
|
- name: "Install Linux VirtualDisplay"
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
@ -80,14 +75,14 @@ jobs:
|
|||||||
sudo apt-get install -y libxkbcommon-x11-0 x11-utils
|
sudo apt-get install -y libxkbcommon-x11-0 x11-utils
|
||||||
sudo apt-get install --no-install-recommends -y libyaml-dev libegl1-mesa libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0
|
sudo apt-get install --no-install-recommends -y libyaml-dev libegl1-mesa libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0
|
||||||
sudo apt-get install -y libopengl0
|
sudo apt-get install -y libopengl0
|
||||||
pip install pytest-xvfb
|
python -m pip install pytest-xvfb
|
||||||
- name: 'Debug Info'
|
- name: 'Debug Info'
|
||||||
run: |
|
run: |
|
||||||
echo python location: `which python`
|
echo python location: `which python`
|
||||||
echo python version: `python --version`
|
echo python version: `python --version`
|
||||||
echo pytest location: `which pytest`
|
echo pytest location: `which pytest`
|
||||||
echo installed packages
|
echo installed packages
|
||||||
pip list
|
python -m pip list
|
||||||
echo pyqtgraph system info
|
echo pyqtgraph system info
|
||||||
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -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
|
||||||
|
23
examples/RunExampleApp.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import initExample ## Add path to library (just for examples; you do not need this)
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtTest
|
||||||
|
|
||||||
|
from examples.ExampleApp import ExampleLoader
|
||||||
|
"""
|
||||||
|
This file is used by test_examples.py for ensuring the Example App works.
|
||||||
|
It is not named test_ExampleApp.py as that way the Example application is
|
||||||
|
not run twice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pg.mkQApp()
|
||||||
|
|
||||||
|
def test_ExampleLoader():
|
||||||
|
loader = ExampleLoader()
|
||||||
|
QtTest.QTest.qWaitForWindowExposed(loader)
|
||||||
|
QtTest.QTest.qWait(200)
|
||||||
|
loader.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_ExampleLoader()
|
||||||
|
pg.exec()
|
@ -1,11 +0,0 @@
|
|||||||
import initExample ## Add path to library (just for examples; you do not need this)
|
|
||||||
|
|
||||||
import pyqtgraph as pg
|
|
||||||
from pyqtgraph.Qt import QtGui, QtCore
|
|
||||||
|
|
||||||
from examples.ExampleApp import ExampleLoader
|
|
||||||
|
|
||||||
loader = ExampleLoader()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pg.exec()
|
|
@ -1,16 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import print_function, division, absolute_import
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from pyqtgraph import Qt
|
from pyqtgraph import Qt
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
import time
|
||||||
import importlib
|
import importlib
|
||||||
import itertools
|
import itertools
|
||||||
import pytest
|
import pytest
|
||||||
import os, sys
|
import os, sys
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
if __name__ == "__main__" and (__package__ is None or __package__==''):
|
if __name__ == "__main__" and (__package__ is None or __package__==''):
|
||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@ -34,7 +31,7 @@ def buildFileList(examples, files=None):
|
|||||||
|
|
||||||
|
|
||||||
path = os.path.abspath(os.path.dirname(__file__))
|
path = os.path.abspath(os.path.dirname(__file__))
|
||||||
files = [("Example App", "test_ExampleApp.py")]
|
files = [("Example App", "RunExampleApp.py")]
|
||||||
for ex in [utils.examples, utils.others]:
|
for ex in [utils.examples, utils.others]:
|
||||||
files = buildFileList(ex, files)
|
files = buildFileList(ex, files)
|
||||||
files = sorted(set(files))
|
files = sorted(set(files))
|
||||||
@ -143,13 +140,11 @@ conditionalExamples = {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
def testExamples(frontend, f):
|
def testExamples(frontend, f):
|
||||||
# runExampleFile(f[0], f[1], sys.executable, frontend)
|
|
||||||
|
|
||||||
name, file = f
|
name, file = f
|
||||||
global path
|
global path
|
||||||
fn = os.path.join(path, file)
|
fn = os.path.join(path, file)
|
||||||
os.chdir(path)
|
os.chdir(path)
|
||||||
sys.stdout.write("{} ".format(name))
|
sys.stdout.write(f"{name}")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
import1 = "import %s" % frontend if frontend != '' else ''
|
import1 = "import %s" % frontend if frontend != '' else ''
|
||||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||||
@ -172,24 +167,19 @@ except:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
""".format(import1, import2)
|
""".format(import1, import2)
|
||||||
if sys.platform.startswith('win'):
|
process = subprocess.Popen([sys.executable],
|
||||||
process = subprocess.Popen([sys.executable],
|
stdin=subprocess.PIPE,
|
||||||
stdin=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE)
|
text=True)
|
||||||
else:
|
process.stdin.write(code)
|
||||||
process = subprocess.Popen(['exec %s -i' % (sys.executable)],
|
|
||||||
shell=True,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE)
|
|
||||||
process.stdin.write(code.encode('UTF-8'))
|
|
||||||
process.stdin.close()
|
process.stdin.close()
|
||||||
|
|
||||||
output = ''
|
output = ''
|
||||||
fail = False
|
fail = False
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
c = process.stdout.read(1).decode()
|
c = process.stdout.read(1)
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
if err.errno == errno.EINTR:
|
if err.errno == errno.EINTR:
|
||||||
# Interrupted system call; just try again.
|
# Interrupted system call; just try again.
|
||||||
@ -197,7 +187,6 @@ except:
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
output += c
|
output += c
|
||||||
|
|
||||||
if output.endswith('test complete'):
|
if output.endswith('test complete'):
|
||||||
break
|
break
|
||||||
if output.endswith('test failed'):
|
if output.endswith('test failed'):
|
||||||
@ -210,16 +199,25 @@ except:
|
|||||||
if time.time() - start > 2.0 and not killed:
|
if time.time() - start > 2.0 and not killed:
|
||||||
process.kill()
|
process.kill()
|
||||||
killed = True
|
killed = True
|
||||||
#res = process.communicate()
|
|
||||||
res = (process.stdout.read(), process.stderr.read())
|
stdout, stderr = (process.stdout.read(), process.stderr.read())
|
||||||
|
process.stdout.close()
|
||||||
|
process.stderr.close()
|
||||||
|
|
||||||
if (fail or
|
if (fail or
|
||||||
'exception' in res[1].decode().lower() or
|
'exception' in stderr.lower() or
|
||||||
'error' in res[1].decode().lower()):
|
'error' in stderr.lower()):
|
||||||
print(res[0].decode())
|
if (not fail
|
||||||
print(res[1].decode())
|
and name == "RemoteGraphicsView"
|
||||||
pytest.fail("{}\n{}\nFailed {} Example Test Located in {} "
|
and "pyqtgraph.multiprocess.remoteproxy.ClosedError" in stderr):
|
||||||
.format(res[0].decode(), res[1].decode(), name, file),
|
# This test can intermittently fail when the subprocess is killed
|
||||||
pytrace=False)
|
return None
|
||||||
|
print(stdout)
|
||||||
|
print(stderr)
|
||||||
|
pytest.fail(
|
||||||
|
f"{stdout}\n{stderr}\nFailed {name} Example Test Located in {file}",
|
||||||
|
pytrace=False
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.cmdline.main()
|
pytest.cmdline.main()
|
||||||
|
@ -12,6 +12,7 @@ as it can be converted to/from a string using repr and eval.
|
|||||||
import re, os, sys, datetime
|
import re, os, sys, datetime
|
||||||
import numpy
|
import numpy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
import tempfile
|
||||||
from . import units
|
from . import units
|
||||||
from .python2_3 import asUnicode, basestring
|
from .python2_3 import asUnicode, basestring
|
||||||
from .Qt import QtCore
|
from .Qt import QtCore
|
||||||
@ -187,11 +188,8 @@ def measureIndent(s):
|
|||||||
while n < len(s) and s[n] == ' ':
|
while n < len(s) and s[n] == ' ':
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import tempfile
|
|
||||||
cf = """
|
cf = """
|
||||||
key: 'value'
|
key: 'value'
|
||||||
key2: ##comment
|
key2: ##comment
|
||||||
@ -201,16 +199,13 @@ key2: ##comment
|
|||||||
key22: [1,2,3]
|
key22: [1,2,3]
|
||||||
key23: 234 #comment
|
key23: 234 #comment
|
||||||
"""
|
"""
|
||||||
fn = tempfile.mktemp()
|
with tempfile.NamedTemporaryFile(encoding="utf-8") as tf:
|
||||||
with open(fn, 'w') as tf:
|
tf.write(cf.encode("utf-8"))
|
||||||
tf.write(cf)
|
print("=== Test:===")
|
||||||
print("=== Test:===")
|
for num, line in enumerate(cf.split('\n'), start=1):
|
||||||
num = 1
|
print("%02d %s" % (num, line))
|
||||||
for line in cf.split('\n'):
|
print(cf)
|
||||||
print("%02d %s" % (num, line))
|
print("============")
|
||||||
num += 1
|
data = readConfigFile(tf.name)
|
||||||
print(cf)
|
|
||||||
print("============")
|
|
||||||
data = readConfigFile(fn)
|
|
||||||
print(data)
|
print(data)
|
||||||
os.remove(fn)
|
|
||||||
|
@ -113,11 +113,10 @@ def getExc(indent=4, prefix='| ', skip=1):
|
|||||||
def printExc(msg='', indent=4, prefix='|'):
|
def printExc(msg='', indent=4, prefix='|'):
|
||||||
"""Print an error message followed by an indented exception backtrace
|
"""Print an error message followed by an indented exception backtrace
|
||||||
(This function is intended to be called within except: blocks)"""
|
(This function is intended to be called within except: blocks)"""
|
||||||
exc = getExc(indent, prefix + ' ', skip=2)
|
exc = getExc(indent=0, prefix="", skip=2)
|
||||||
print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg))
|
# print(" "*indent + prefix + '='*30 + '>>')
|
||||||
print(" "*indent + prefix + '='*30 + '>>')
|
warnings.warn("\n".join([msg, exc]), RuntimeWarning, stacklevel=2)
|
||||||
print(exc)
|
# print(" "*indent + prefix + '='*30 + '<<')
|
||||||
print(" "*indent + prefix + '='*30 + '<<')
|
|
||||||
|
|
||||||
|
|
||||||
def printTrace(msg='', indent=4, prefix='|'):
|
def printTrace(msg='', indent=4, prefix='|'):
|
||||||
|
@ -1507,16 +1507,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)).
|
|
||||||
"""
|
|
||||||
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"
|
||||||
@ -1524,14 +1518,39 @@ 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:
|
||||||
|
arr[...,3] = 255
|
||||||
|
|
||||||
if copy:
|
if copy:
|
||||||
arr = arr.copy()
|
arr = arr.copy()
|
||||||
|
@ -795,17 +795,28 @@ class ROI(GraphicsObject):
|
|||||||
self.mouseDragHandler.mouseDragEvent(ev)
|
self.mouseDragHandler.mouseDragEvent(ev)
|
||||||
|
|
||||||
def mouseClickEvent(self, ev):
|
def mouseClickEvent(self, ev):
|
||||||
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
|
with warnings.catch_warnings():
|
||||||
ev.accept()
|
# warning present on pyqt5 5.12 + python 3.8
|
||||||
self.cancelMove()
|
warnings.filterwarnings(
|
||||||
if ev.button() == QtCore.Qt.RightButton and self.contextMenuEnabled():
|
"ignore",
|
||||||
self.raiseContextMenu(ev)
|
message=(
|
||||||
ev.accept()
|
".*Implicit conversion to integers using __int__ is "
|
||||||
elif ev.button() & self.acceptedMouseButtons() > 0:
|
"deprecated, and may be removed in a future version of "
|
||||||
ev.accept()
|
"Python."
|
||||||
self.sigClicked.emit(self, ev)
|
),
|
||||||
else:
|
category=DeprecationWarning
|
||||||
ev.ignore()
|
)
|
||||||
|
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
|
||||||
|
ev.accept()
|
||||||
|
self.cancelMove()
|
||||||
|
if ev.button() == QtCore.Qt.RightButton and self.contextMenuEnabled():
|
||||||
|
self.raiseContextMenu(ev)
|
||||||
|
ev.accept()
|
||||||
|
elif ev.button() & self.acceptedMouseButtons():
|
||||||
|
ev.accept()
|
||||||
|
self.sigClicked.emit(self, ev)
|
||||||
|
else:
|
||||||
|
ev.ignore()
|
||||||
|
|
||||||
def _moveStarted(self):
|
def _moveStarted(self):
|
||||||
self.isMoving = True
|
self.isMoving = True
|
||||||
@ -1400,18 +1411,29 @@ class Handle(UIGraphicsItem):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def mouseClickEvent(self, ev):
|
def mouseClickEvent(self, ev):
|
||||||
## right-click cancels drag
|
with warnings.catch_warnings():
|
||||||
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
|
# warning present on pyqt5 5.12 + python 3.8
|
||||||
self.isMoving = False ## prevents any further motion
|
warnings.filterwarnings(
|
||||||
self.movePoint(self.startPos, finish=True)
|
"ignore",
|
||||||
ev.accept()
|
message=(
|
||||||
elif ev.button() & self.acceptedMouseButtons():
|
".*Implicit conversion to integers using __int__ is "
|
||||||
ev.accept()
|
"deprecated, and may be removed in a future version of "
|
||||||
if ev.button() == QtCore.Qt.RightButton and self.deletable:
|
"Python."
|
||||||
self.raiseContextMenu(ev)
|
),
|
||||||
self.sigClicked.emit(self, ev)
|
category=DeprecationWarning
|
||||||
else:
|
)
|
||||||
ev.ignore()
|
## right-click cancels drag
|
||||||
|
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
|
||||||
|
self.isMoving = False ## prevents any further motion
|
||||||
|
self.movePoint(self.startPos, finish=True)
|
||||||
|
ev.accept()
|
||||||
|
elif ev.button() & self.acceptedMouseButtons():
|
||||||
|
ev.accept()
|
||||||
|
if ev.button() == QtCore.Qt.RightButton and self.deletable:
|
||||||
|
self.raiseContextMenu(ev)
|
||||||
|
self.sigClicked.emit(self, ev)
|
||||||
|
else:
|
||||||
|
ev.ignore()
|
||||||
|
|
||||||
def buildMenu(self):
|
def buildMenu(self):
|
||||||
menu = QtGui.QMenu()
|
menu = QtGui.QMenu()
|
||||||
|
@ -1330,8 +1330,6 @@ if __name__ == '__main__':
|
|||||||
#### File I/O tests
|
#### File I/O tests
|
||||||
|
|
||||||
print("\n================ File I/O Tests ===================\n")
|
print("\n================ File I/O Tests ===================\n")
|
||||||
import tempfile
|
|
||||||
tf = tempfile.mktemp()
|
|
||||||
tf = 'test.ma'
|
tf = 'test.ma'
|
||||||
# write whole array
|
# write whole array
|
||||||
|
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import pyqtgraph as pg
|
|
||||||
import pytest
|
|
||||||
import textwrap
|
|
||||||
import time
|
|
||||||
|
|
||||||
code = """
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, '{path}')
|
|
||||||
import pyqtgraph as pg
|
|
||||||
app = pg.mkQApp()
|
|
||||||
w = pg.{classname}({args})
|
|
||||||
"""
|
|
||||||
|
|
||||||
skipmessage = ('unclear why this test is failing. skipping until someone has'
|
|
||||||
' time to fix it')
|
|
||||||
|
|
||||||
|
|
||||||
def call_with_timeout(*args, **kwargs):
|
|
||||||
"""Mimic subprocess.call with timeout for python < 3.3"""
|
|
||||||
wait_per_poll = 0.1
|
|
||||||
try:
|
|
||||||
timeout = kwargs.pop('timeout')
|
|
||||||
except KeyError:
|
|
||||||
timeout = 10
|
|
||||||
|
|
||||||
rc = None
|
|
||||||
p = subprocess.Popen(*args, **kwargs)
|
|
||||||
for i in range(int(timeout/wait_per_poll)):
|
|
||||||
rc = p.poll()
|
|
||||||
if rc is not None:
|
|
||||||
break
|
|
||||||
time.sleep(wait_per_poll)
|
|
||||||
return rc
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(True, reason=skipmessage)
|
|
||||||
def test_exit_crash():
|
|
||||||
# For each Widget subclass, run a simple python script that creates an
|
|
||||||
# instance and then shuts down. The intent is to check for segmentation
|
|
||||||
# faults when each script exits.
|
|
||||||
tmp = tempfile.mktemp(".py")
|
|
||||||
path = os.path.dirname(pg.__file__)
|
|
||||||
|
|
||||||
initArgs = {
|
|
||||||
'CheckTable': "[]",
|
|
||||||
'ProgressDialog': '"msg"',
|
|
||||||
'VerticalLabel': '"msg"',
|
|
||||||
}
|
|
||||||
|
|
||||||
for name in dir(pg):
|
|
||||||
obj = getattr(pg, name)
|
|
||||||
if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget):
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(name)
|
|
||||||
argstr = initArgs.get(name, "")
|
|
||||||
with open(tmp, 'w') as f:
|
|
||||||
f.write(code.format(path=path, classname=name, args=argstr))
|
|
||||||
proc = subprocess.Popen([sys.executable, tmp])
|
|
||||||
assert proc.wait() == 0
|
|
||||||
|
|
||||||
os.remove(tmp)
|
|
||||||
|
|
||||||
@pytest.mark.skipif(pg.Qt.QtVersion.startswith("5.9"), reason="Functionality not well supported, failing only on this config")
|
|
||||||
def test_pg_exit():
|
|
||||||
# test the pg.exit() function
|
|
||||||
code = textwrap.dedent("""
|
|
||||||
import pyqtgraph as pg
|
|
||||||
app = pg.mkQApp()
|
|
||||||
pg.plot()
|
|
||||||
pg.exit()
|
|
||||||
""")
|
|
||||||
rc = call_with_timeout([sys.executable, '-c', code], timeout=5, shell=False)
|
|
||||||
assert rc == 0
|
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
|
import warnings
|
||||||
|
|
||||||
__all__ = ['VerticalLabel']
|
__all__ = ['VerticalLabel']
|
||||||
#class VerticalLabel(QtGui.QLabel):
|
#class VerticalLabel(QtGui.QLabel):
|
||||||
@ -46,8 +47,9 @@ class VerticalLabel(QtGui.QLabel):
|
|||||||
rgn = self.contentsRect()
|
rgn = self.contentsRect()
|
||||||
align = self.alignment()
|
align = self.alignment()
|
||||||
#align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter
|
#align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter
|
||||||
|
with warnings.catch_warnings():
|
||||||
self.hint = p.drawText(rgn, align, self.text())
|
warnings.simplefilter("ignore")
|
||||||
|
self.hint = p.drawText(rgn, align, self.text())
|
||||||
p.end()
|
p.end()
|
||||||
|
|
||||||
if self.orientation == 'vertical':
|
if self.orientation == 'vertical':
|
||||||
|
@ -4,9 +4,10 @@ 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 =
|
||||||
|
error
|
||||||
# re-enable standard library warnings
|
# re-enable standard library warnings
|
||||||
once::DeprecationWarning
|
once::DeprecationWarning
|
||||||
once::PendingDeprecationWarning
|
once::PendingDeprecationWarning
|
||||||
@ -19,3 +20,5 @@ filterwarnings =
|
|||||||
ignore:Visible window deleted. To prevent this, store a reference to the window object.
|
ignore:Visible window deleted. To prevent this, store a reference to the window object.
|
||||||
# xvfb warnings on non-linux systems
|
# xvfb warnings on non-linux systems
|
||||||
ignore:Unknown config option:pytest.PytestConfigWarning
|
ignore:Unknown config option:pytest.PytestConfigWarning
|
||||||
|
# pyreadline windows warning
|
||||||
|
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working:DeprecationWarning:pyreadline:8
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#try:
|
|
||||||
# from PyQt5 import sip
|
|
||||||
#except ImportError:
|
|
||||||
# import sip
|
|
||||||
# sip.setapi('QString', 1)
|
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
pg.mkQApp()
|
pg.mkQApp()
|
||||||
|
|
@ -1,11 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from collections import OrderedDict
|
import pyqtgraph.dockarea as da
|
||||||
|
|
||||||
pg.mkQApp()
|
pg.mkQApp()
|
||||||
|
|
||||||
import pyqtgraph.dockarea as da
|
|
||||||
|
|
||||||
def test_dockarea():
|
def test_dockarea():
|
||||||
a = da.DockArea()
|
a = da.DockArea()
|
||||||
@ -176,14 +174,14 @@ def test_dockarea():
|
|||||||
# a superfluous vertical splitter in state2 has been removed
|
# a superfluous vertical splitter in state2 has been removed
|
||||||
state4 = a4.saveState()
|
state4 = a4.saveState()
|
||||||
state4['main'][1][0] = state4['main'][1][0][1][0]
|
state4['main'][1][0] = state4['main'][1][0][1][0]
|
||||||
assert clean_state(state4['main']) == clean_state(state2['main'])
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
# this test doesn't work, likely due to clean_state not working as intended
|
||||||
|
assert clean_state(state4['main']) == clean_state(state2['main'])
|
||||||
|
|
||||||
|
|
||||||
def clean_state(state):
|
def clean_state(state):
|
||||||
# return state dict with sizes removed
|
# return state dict with sizes removed
|
||||||
ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
|
ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
|
||||||
state = (state[0], ch, {})
|
state = (state[0], ch, {})
|
||||||
|
return state
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_dockarea()
|
|
@ -4,7 +4,6 @@ CSV export test
|
|||||||
from __future__ import division, print_function, absolute_import
|
from __future__ import division, print_function, absolute_import
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import csv
|
import csv
|
||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
app = pg.mkQApp()
|
app = pg.mkQApp()
|
||||||
@ -15,44 +14,33 @@ def approxeq(a, b):
|
|||||||
|
|
||||||
|
|
||||||
def test_CSVExporter():
|
def test_CSVExporter():
|
||||||
tempfilename = tempfile.NamedTemporaryFile(suffix='.csv').name
|
|
||||||
print("using %s as a temporary file" % tempfilename)
|
|
||||||
|
|
||||||
plt = pg.plot()
|
plt = pg.plot()
|
||||||
y1 = [1,3,2,3,1,6,9,8,4,2]
|
y1 = [1,3,2,3,1,6,9,8,4,2]
|
||||||
plt.plot(y=y1, name='myPlot')
|
plt.plot(y=y1, name='myPlot')
|
||||||
|
|
||||||
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
|
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
|
||||||
x2 = pg.np.linspace(0, 1.0, len(y2))
|
x2 = pg.np.linspace(0, 1.0, len(y2))
|
||||||
plt.plot(x=x2, y=y2)
|
plt.plot(x=x2, y=y2)
|
||||||
|
|
||||||
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
|
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
|
||||||
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
|
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
|
||||||
plt.plot(x=x3, y=y3, stepMode="center")
|
plt.plot(x=x3, y=y3, stepMode="center")
|
||||||
|
|
||||||
ex = pg.exporters.CSVExporter(plt.plotItem)
|
|
||||||
ex.export(fileName=tempfilename)
|
|
||||||
|
|
||||||
with open(tempfilename, 'r') as csv_file:
|
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||||
r = csv.reader(csv_file)
|
with tempfile.NamedTemporaryFile(mode="w+t", suffix='.csv', encoding="utf-8", delete=False) as tf:
|
||||||
lines = [line for line in r]
|
print("using %s as a temporary file" % tf.name)
|
||||||
|
ex.export(fileName=tf.name)
|
||||||
|
lines = [line for line in csv.reader(tf)]
|
||||||
header = lines.pop(0)
|
header = lines.pop(0)
|
||||||
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
||||||
|
|
||||||
i = 0
|
for i, vals in enumerate(lines):
|
||||||
for vals in lines:
|
|
||||||
vals = list(map(str.strip, vals))
|
vals = list(map(str.strip, vals))
|
||||||
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
|
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
|
||||||
assert (i >= len(y1) and vals[1] == '') or approxeq(float(vals[1]), y1[i])
|
assert (i >= len(y1) and vals[1] == '') or approxeq(float(vals[1]), y1[i])
|
||||||
|
|
||||||
assert (i >= len(x2) and vals[2] == '') or approxeq(float(vals[2]), x2[i])
|
assert (i >= len(x2) and vals[2] == '') or approxeq(float(vals[2]), x2[i])
|
||||||
assert (i >= len(y2) and vals[3] == '') or approxeq(float(vals[3]), y2[i])
|
assert (i >= len(y2) and vals[3] == '') or approxeq(float(vals[3]), y2[i])
|
||||||
|
|
||||||
assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i])
|
assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i])
|
||||||
assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i])
|
assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i])
|
||||||
i += 1
|
|
||||||
|
|
||||||
os.unlink(tempfilename)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_CSVExporter()
|
|
@ -5,7 +5,6 @@ from pyqtgraph.exporters import HDF5Exporter
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.testing import assert_equal
|
from numpy.testing import assert_equal
|
||||||
import h5py
|
import h5py
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
@ -1,18 +1,10 @@
|
|||||||
"""
|
|
||||||
SVG export test
|
|
||||||
"""
|
|
||||||
from __future__ import division, print_function, absolute_import
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
app = pg.mkQApp()
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
|
||||||
def test_plotscene():
|
def test_plotscene(tmpdir):
|
||||||
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name
|
|
||||||
print("using %s as a temporary file" % tempfilename)
|
|
||||||
pg.setConfigOption('foreground', (0,0,0))
|
pg.setConfigOption('foreground', (0,0,0))
|
||||||
w = pg.GraphicsLayoutWidget()
|
w = pg.GraphicsLayoutWidget()
|
||||||
w.show()
|
w.show()
|
||||||
@ -25,15 +17,13 @@ def test_plotscene():
|
|||||||
app.processEvents()
|
app.processEvents()
|
||||||
|
|
||||||
ex = pg.exporters.SVGExporter(w.scene())
|
ex = pg.exporters.SVGExporter(w.scene())
|
||||||
ex.export(fileName=tempfilename)
|
|
||||||
|
tf = tmpdir.join("expot.svg")
|
||||||
|
ex.export(fileName=tf)
|
||||||
# clean up after the test is done
|
# clean up after the test is done
|
||||||
os.unlink(tempfilename)
|
|
||||||
w.close()
|
w.close()
|
||||||
|
|
||||||
def test_simple():
|
def test_simple(tmpdir):
|
||||||
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name
|
|
||||||
print("using %s as a temporary file" % tempfilename)
|
|
||||||
|
|
||||||
view = pg.GraphicsView()
|
view = pg.GraphicsView()
|
||||||
view.show()
|
view.show()
|
||||||
|
|
||||||
@ -78,5 +68,5 @@ def test_simple():
|
|||||||
grp2.addItem(rect3)
|
grp2.addItem(rect3)
|
||||||
|
|
||||||
ex = pg.exporters.SVGExporter(scene)
|
ex = pg.exporters.SVGExporter(scene)
|
||||||
ex.export(fileName=tempfilename)
|
tf = tmpdir.join("expot.svg")
|
||||||
os.unlink(tempfilename)
|
ex.export(fileName=tf)
|
@ -15,7 +15,7 @@ def test_PlotItem_shared_axis_items(orientation):
|
|||||||
|
|
||||||
layout = pg.GraphicsLayoutWidget()
|
layout = pg.GraphicsLayoutWidget()
|
||||||
|
|
||||||
pi1 = layout.addPlot(axisItems={orientation: ax1})
|
_ = layout.addPlot(axisItems={orientation: ax1})
|
||||||
|
|
||||||
pi2 = layout.addPlot()
|
pi2 = layout.addPlot()
|
||||||
# left or bottom replaces, right or top adds new
|
# left or bottom replaces, right or top adds new
|
@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import pytest
|
|
||||||
|
|
||||||
app = pg.mkQApp()
|
app = pg.mkQApp()
|
||||||
|
|
@ -1,16 +1,19 @@
|
|||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
from math import isclose
|
||||||
|
|
||||||
app = pg.mkQApp()
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
|
||||||
def test_AxisItem_stopAxisAtTick(monkeypatch):
|
def test_AxisItem_stopAxisAtTick(monkeypatch):
|
||||||
def test_bottom(p, axisSpec, tickSpecs, textSpecs):
|
def test_bottom(p, axisSpec, tickSpecs, textSpecs):
|
||||||
assert view.mapToView(axisSpec[1]).x() == 0.25
|
viewPixelSize = view.viewPixelSize()
|
||||||
assert view.mapToView(axisSpec[2]).x() == 0.75
|
assert isclose(view.mapToView(axisSpec[1]).x(), 0.25, abs_tol=viewPixelSize[0])
|
||||||
|
assert isclose(view.mapToView(axisSpec[2]).x(), 0.75, abs_tol=viewPixelSize[0])
|
||||||
|
|
||||||
def test_left(p, axisSpec, tickSpecs, textSpecs):
|
def test_left(p, axisSpec, tickSpecs, textSpecs):
|
||||||
assert view.mapToView(axisSpec[1]).y() == 0.875
|
viewPixelSize = view.viewPixelSize()
|
||||||
assert view.mapToView(axisSpec[2]).y() == 0.125
|
assert isclose(view.mapToView(axisSpec[1]).y(), 0.875, abs_tol=viewPixelSize[1])
|
||||||
|
assert isclose(view.mapToView(axisSpec[2]).y(), 0.125, abs_tol=viewPixelSize[1])
|
||||||
|
|
||||||
plot = pg.PlotWidget()
|
plot = pg.PlotWidget()
|
||||||
view = plot.plotItem.getViewBox()
|
view = plot.plotItem.getViewBox()
|
@ -1,11 +1,8 @@
|
|||||||
import weakref
|
import weakref
|
||||||
try:
|
|
||||||
import faulthandler
|
|
||||||
faulthandler.enable()
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
import faulthandler
|
||||||
|
faulthandler.enable()
|
||||||
|
|
||||||
pg.mkQApp()
|
pg.mkQApp()
|
||||||
|
|
||||||
def test_getViewWidget():
|
def test_getViewWidget():
|
@ -2,10 +2,10 @@
|
|||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pyqtgraph.Qt import QtCore, QtGui, QtTest
|
from pyqtgraph.Qt import 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:
|
||||||
@ -188,7 +188,6 @@ def test_ImageItem_axisorder():
|
|||||||
|
|
||||||
|
|
||||||
def test_dividebyzero():
|
def test_dividebyzero():
|
||||||
import pyqtgraph as pg
|
|
||||||
im = pg.image(pg.np.random.normal(size=(100,100)))
|
im = pg.image(pg.np.random.normal(size=(100,100)))
|
||||||
im.imageItem.setAutoDownsample(True)
|
im.imageItem.setAutoDownsample(True)
|
||||||
im.view.setRange(xRange=[-5+25, 5e+25],yRange=[-5e+25, 5e+25])
|
im.view.setRange(xRange=[-5+25, 5e+25],yRange=[-5e+25, 5e+25])
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ def test_InfiniteLine():
|
|||||||
px = pg.Point(-0.5, -1.0 / 3**0.5)
|
px = pg.Point(-0.5, -1.0 / 3**0.5)
|
||||||
assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill)
|
assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill)
|
||||||
assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill)
|
assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
def test_mouseInteraction():
|
def test_mouseInteraction():
|
||||||
# disable delay of mouse move events because events is called immediately in test
|
# disable delay of mouse move events because events is called immediately in test
|
||||||
@ -96,7 +96,4 @@ def test_mouseInteraction():
|
|||||||
assert hline2.mouseHovering == False
|
assert hline2.mouseHovering == False
|
||||||
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
|
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
|
||||||
assert hline2.value() == -1
|
assert hline2.value() == -1
|
||||||
|
plt.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_mouseInteraction()
|
|
@ -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)
|
@ -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():
|
||||||
@ -30,7 +30,3 @@ def test_PlotCurveItem():
|
|||||||
assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.")
|
assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.")
|
||||||
|
|
||||||
p.close()
|
p.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_PlotCurveItem()
|
|
@ -1,11 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
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 +39,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 +100,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 +109,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 +144,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)
|
@ -5,7 +5,6 @@ import numpy as np
|
|||||||
|
|
||||||
def test_scatterplotitem():
|
def test_scatterplotitem():
|
||||||
app = pg.mkQApp()
|
app = pg.mkQApp()
|
||||||
app.processEvents()
|
|
||||||
|
|
||||||
plot = pg.PlotWidget()
|
plot = pg.PlotWidget()
|
||||||
# set view range equal to its bounding rect.
|
# set view range equal to its bounding rect.
|
||||||
@ -99,7 +98,4 @@ def test_init_spots():
|
|||||||
assert spots[1].pen() == pg.mkPen(None)
|
assert spots[1].pen() == pg.mkPen(None)
|
||||||
assert spots[1].brush() == pg.mkBrush(None)
|
assert spots[1].brush() == pg.mkBrush(None)
|
||||||
assert spots[1].data() == 'zzz'
|
assert spots[1].data() == 'zzz'
|
||||||
|
plot.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_scatterplotitem()
|
|
@ -1,4 +1,3 @@
|
|||||||
import pytest
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
app = pg.mkQApp()
|
app = pg.mkQApp()
|
@ -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):
|
BIN
tests/images/imageitem/bool.png
Normal file
After Width: | Height: | Size: 622 B |
BIN
tests/images/imageitem/gradient_mono_byte.png
Normal file
After Width: | Height: | Size: 660 B |
BIN
tests/images/imageitem/gradient_mono_byte_levels.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/images/imageitem/gradient_mono_int.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
tests/images/imageitem/gradient_mono_int_levels.png
Normal file
After Width: | Height: | Size: 670 B |
BIN
tests/images/imageitem/gradient_rgba_byte.png
Normal file
After Width: | Height: | Size: 718 B |
BIN
tests/images/imageitem/gradient_rgba_byte_levels.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
tests/images/imageitem/gradient_rgba_float.png
Normal file
After Width: | Height: | Size: 749 B |
BIN
tests/images/imageitem/gradient_rgba_float_additive.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
tests/images/imageitem/gradient_rgba_float_alpha.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
tests/images/imageitem/init.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
tests/images/imageitem/levels1.png
Normal file
After Width: | Height: | Size: 647 B |
BIN
tests/images/imageitem/lut.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
tests/images/imageitem/monochrome.png
Normal file
After Width: | Height: | Size: 622 B |
BIN
tests/images/imageitem/resolution_with_downsampling_x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/images/imageitem/resolution_with_downsampling_y.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
tests/images/imageitem/resolution_without_downsampling.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/images/nonuniform_image/colormap-3x3.png
Normal file
After Width: | Height: | Size: 656 B |
BIN
tests/images/nonuniform_image/lut-3x3.png
Normal file
After Width: | Height: | Size: 668 B |
BIN
tests/images/plotcurveitem/connectall.png
Normal file
After Width: | Height: | Size: 885 B |
BIN
tests/images/plotcurveitem/connectarray.png
Normal file
After Width: | Height: | Size: 788 B |
BIN
tests/images/plotcurveitem/connectfinite.png
Normal file
After Width: | Height: | Size: 547 B |
BIN
tests/images/plotcurveitem/connectpairs.png
Normal file
After Width: | Height: | Size: 770 B |
BIN
tests/images/roi/baseroi/roi_getarrayregion.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
tests/images/roi/baseroi/roi_getarrayregion_anisotropic.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
tests/images/roi/baseroi/roi_getarrayregion_halfpx.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
tests/images/roi/baseroi/roi_getarrayregion_img_trans.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
tests/images/roi/baseroi/roi_getarrayregion_inverty.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
tests/images/roi/baseroi/roi_getarrayregion_resize.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
tests/images/roi/baseroi/roi_getarrayregion_rotate.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion_anisotropic.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion_halfpx.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion_img_trans.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion_inverty.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion_resize.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
tests/images/roi/ellipseroi/roi_getarrayregion_rotate.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
tests/images/roi/polylineroi/closed_clear.png
Normal file
After Width: | Height: | Size: 627 B |
BIN
tests/images/roi/polylineroi/closed_click_segment.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/images/roi/polylineroi/closed_drag_handle.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
tests/images/roi/polylineroi/closed_drag_new_handle.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
tests/images/roi/polylineroi/closed_drag_roi.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/images/roi/polylineroi/closed_hover_handle.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/images/roi/polylineroi/closed_hover_roi.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/images/roi/polylineroi/closed_hover_segment.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
tests/images/roi/polylineroi/closed_init.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/images/roi/polylineroi/closed_setpoints.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/images/roi/polylineroi/closed_setstate.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/images/roi/polylineroi/open_clear.png
Normal file
After Width: | Height: | Size: 627 B |
BIN
tests/images/roi/polylineroi/open_click_segment.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
tests/images/roi/polylineroi/open_drag_handle.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/images/roi/polylineroi/open_drag_new_handle.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
tests/images/roi/polylineroi/open_drag_roi.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/images/roi/polylineroi/open_hover_handle.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/images/roi/polylineroi/open_hover_roi.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/images/roi/polylineroi/open_hover_segment.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
tests/images/roi/polylineroi/open_init.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/images/roi/polylineroi/open_setpoints.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/images/roi/polylineroi/open_setstate.png
Normal file
After Width: | Height: | Size: 1.1 KiB |