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:
|
||||
- name: Checkout
|
||||
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 }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@ -69,10 +64,10 @@ jobs:
|
||||
shell: cmd
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install ${{ matrix.qt-version }} numpy${{ matrix.numpy-version }} scipy pyopengl h5py matplotlib numba
|
||||
pip install .
|
||||
pip install pytest
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
python -m pip install ${{ matrix.qt-version }} numpy${{ matrix.numpy-version }} scipy pyopengl h5py matplotlib numba
|
||||
python -m pip install --use-feature=in-tree-build .
|
||||
python -m pip install pytest
|
||||
- name: "Install Linux VirtualDisplay"
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
@ -80,14 +75,14 @@ jobs:
|
||||
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 -y libopengl0
|
||||
pip install pytest-xvfb
|
||||
python -m pip install pytest-xvfb
|
||||
- name: 'Debug Info'
|
||||
run: |
|
||||
echo python location: `which python`
|
||||
echo python version: `python --version`
|
||||
echo pytest location: `which pytest`
|
||||
echo installed packages
|
||||
pip list
|
||||
python -m pip list
|
||||
echo pyqtgraph system info
|
||||
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||
shell: bash
|
||||
@ -106,7 +101,7 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: |
|
||||
mkdir $SCREENSHOT_DIR
|
||||
pytest pyqtgraph examples -v \
|
||||
pytest tests examples -v \
|
||||
--junitxml pytest.xml \
|
||||
shell: bash
|
||||
- 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 pyqtgraph import Qt
|
||||
|
||||
import errno
|
||||
import time
|
||||
import importlib
|
||||
import itertools
|
||||
import pytest
|
||||
import os, sys
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from argparse import Namespace
|
||||
if __name__ == "__main__" and (__package__ is None or __package__==''):
|
||||
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__))
|
||||
files = [("Example App", "test_ExampleApp.py")]
|
||||
files = [("Example App", "RunExampleApp.py")]
|
||||
for ex in [utils.examples, utils.others]:
|
||||
files = buildFileList(ex, files)
|
||||
files = sorted(set(files))
|
||||
@ -143,13 +140,11 @@ conditionalExamples = {
|
||||
]
|
||||
)
|
||||
def testExamples(frontend, f):
|
||||
# runExampleFile(f[0], f[1], sys.executable, frontend)
|
||||
|
||||
name, file = f
|
||||
global path
|
||||
fn = os.path.join(path, file)
|
||||
os.chdir(path)
|
||||
sys.stdout.write("{} ".format(name))
|
||||
sys.stdout.write(f"{name}")
|
||||
sys.stdout.flush()
|
||||
import1 = "import %s" % frontend if frontend != '' else ''
|
||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||
@ -172,24 +167,19 @@ except:
|
||||
raise
|
||||
|
||||
""".format(import1, import2)
|
||||
if sys.platform.startswith('win'):
|
||||
process = subprocess.Popen([sys.executable],
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
else:
|
||||
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 = subprocess.Popen([sys.executable],
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True)
|
||||
process.stdin.write(code)
|
||||
process.stdin.close()
|
||||
|
||||
output = ''
|
||||
fail = False
|
||||
while True:
|
||||
try:
|
||||
c = process.stdout.read(1).decode()
|
||||
c = process.stdout.read(1)
|
||||
except IOError as err:
|
||||
if err.errno == errno.EINTR:
|
||||
# Interrupted system call; just try again.
|
||||
@ -197,7 +187,6 @@ except:
|
||||
else:
|
||||
raise
|
||||
output += c
|
||||
|
||||
if output.endswith('test complete'):
|
||||
break
|
||||
if output.endswith('test failed'):
|
||||
@ -210,16 +199,25 @@ except:
|
||||
if time.time() - start > 2.0 and not killed:
|
||||
process.kill()
|
||||
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
|
||||
'exception' in res[1].decode().lower() or
|
||||
'error' in res[1].decode().lower()):
|
||||
print(res[0].decode())
|
||||
print(res[1].decode())
|
||||
pytest.fail("{}\n{}\nFailed {} Example Test Located in {} "
|
||||
.format(res[0].decode(), res[1].decode(), name, file),
|
||||
pytrace=False)
|
||||
'exception' in stderr.lower() or
|
||||
'error' in stderr.lower()):
|
||||
if (not fail
|
||||
and name == "RemoteGraphicsView"
|
||||
and "pyqtgraph.multiprocess.remoteproxy.ClosedError" in stderr):
|
||||
# This test can intermittently fail when the subprocess is killed
|
||||
return None
|
||||
print(stdout)
|
||||
print(stderr)
|
||||
pytest.fail(
|
||||
f"{stdout}\n{stderr}\nFailed {name} Example Test Located in {file}",
|
||||
pytrace=False
|
||||
)
|
||||
|
||||
if __name__ == "__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 numpy
|
||||
from collections import OrderedDict
|
||||
import tempfile
|
||||
from . import units
|
||||
from .python2_3 import asUnicode, basestring
|
||||
from .Qt import QtCore
|
||||
@ -187,11 +188,8 @@ def measureIndent(s):
|
||||
while n < len(s) and s[n] == ' ':
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import tempfile
|
||||
cf = """
|
||||
key: 'value'
|
||||
key2: ##comment
|
||||
@ -201,16 +199,13 @@ key2: ##comment
|
||||
key22: [1,2,3]
|
||||
key23: 234 #comment
|
||||
"""
|
||||
fn = tempfile.mktemp()
|
||||
with open(fn, 'w') as tf:
|
||||
tf.write(cf)
|
||||
print("=== Test:===")
|
||||
num = 1
|
||||
for line in cf.split('\n'):
|
||||
print("%02d %s" % (num, line))
|
||||
num += 1
|
||||
print(cf)
|
||||
print("============")
|
||||
data = readConfigFile(fn)
|
||||
with tempfile.NamedTemporaryFile(encoding="utf-8") as tf:
|
||||
tf.write(cf.encode("utf-8"))
|
||||
print("=== Test:===")
|
||||
for num, line in enumerate(cf.split('\n'), start=1):
|
||||
print("%02d %s" % (num, line))
|
||||
print(cf)
|
||||
print("============")
|
||||
data = readConfigFile(tf.name)
|
||||
print(data)
|
||||
os.remove(fn)
|
||||
|
||||
|
@ -113,11 +113,10 @@ def getExc(indent=4, prefix='| ', skip=1):
|
||||
def printExc(msg='', indent=4, prefix='|'):
|
||||
"""Print an error message followed by an indented exception backtrace
|
||||
(This function is intended to be called within except: blocks)"""
|
||||
exc = getExc(indent, prefix + ' ', skip=2)
|
||||
print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg))
|
||||
print(" "*indent + prefix + '='*30 + '>>')
|
||||
print(exc)
|
||||
print(" "*indent + prefix + '='*30 + '<<')
|
||||
exc = getExc(indent=0, prefix="", skip=2)
|
||||
# print(" "*indent + prefix + '='*30 + '>>')
|
||||
warnings.warn("\n".join([msg, exc]), RuntimeWarning, stacklevel=2)
|
||||
# print(" "*indent + prefix + '='*30 + '<<')
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)).
|
||||
"""
|
||||
img_ptr = img.bits()
|
||||
def qimage_to_ndarray(qimg):
|
||||
img_ptr = qimg.bits()
|
||||
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
if hasattr(img_ptr, 'setsize'): # PyQt sip.voidptr
|
||||
# sizeInBytes() was introduced in Qt 5.10
|
||||
# however PyQt5 5.12 will fail with:
|
||||
# "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
|
||||
try:
|
||||
# 64-bits size
|
||||
nbytes = img.sizeInBytes()
|
||||
nbytes = qimg.sizeInBytes()
|
||||
except (TypeError, AttributeError):
|
||||
# 32-bits size
|
||||
nbytes = img.byteCount()
|
||||
nbytes = qimg.byteCount()
|
||||
img_ptr.setsize(nbytes)
|
||||
|
||||
arr = np.frombuffer(img_ptr, dtype=np.ubyte)
|
||||
arr = arr.reshape(img.height(), img.width(), 4)
|
||||
depth = qimg.depth()
|
||||
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:
|
||||
arr = arr.copy()
|
||||
|
@ -795,17 +795,28 @@ class ROI(GraphicsObject):
|
||||
self.mouseDragHandler.mouseDragEvent(ev)
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
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() > 0:
|
||||
ev.accept()
|
||||
self.sigClicked.emit(self, ev)
|
||||
else:
|
||||
ev.ignore()
|
||||
with warnings.catch_warnings():
|
||||
# warning present on pyqt5 5.12 + python 3.8
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=(
|
||||
".*Implicit conversion to integers using __int__ is "
|
||||
"deprecated, and may be removed in a future version of "
|
||||
"Python."
|
||||
),
|
||||
category=DeprecationWarning
|
||||
)
|
||||
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):
|
||||
self.isMoving = True
|
||||
@ -1400,18 +1411,29 @@ class Handle(UIGraphicsItem):
|
||||
self.update()
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
## 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()
|
||||
with warnings.catch_warnings():
|
||||
# warning present on pyqt5 5.12 + python 3.8
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=(
|
||||
".*Implicit conversion to integers using __int__ is "
|
||||
"deprecated, and may be removed in a future version of "
|
||||
"Python."
|
||||
),
|
||||
category=DeprecationWarning
|
||||
)
|
||||
## 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):
|
||||
menu = QtGui.QMenu()
|
||||
|
@ -1330,8 +1330,6 @@ if __name__ == '__main__':
|
||||
#### File I/O tests
|
||||
|
||||
print("\n================ File I/O Tests ===================\n")
|
||||
import tempfile
|
||||
tf = tempfile.mktemp()
|
||||
tf = 'test.ma'
|
||||
# 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 -*-
|
||||
from ..Qt import QtGui, QtCore
|
||||
import warnings
|
||||
|
||||
__all__ = ['VerticalLabel']
|
||||
#class VerticalLabel(QtGui.QLabel):
|
||||
@ -46,8 +47,9 @@ class VerticalLabel(QtGui.QLabel):
|
||||
rgn = self.contentsRect()
|
||||
align = self.alignment()
|
||||
#align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter
|
||||
|
||||
self.hint = p.drawText(rgn, align, self.text())
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
self.hint = p.drawText(rgn, align, self.text())
|
||||
p.end()
|
||||
|
||||
if self.orientation == 'vertical':
|
||||
|
@ -4,9 +4,10 @@ xvfb_height = 1080
|
||||
# use this due to some issues with ndarray reshape errors on CI systems
|
||||
xvfb_colordepth = 24
|
||||
xvfb_args=-ac +extension GLX +render
|
||||
faulthandler_timeout = 30
|
||||
faulthandler_timeout = 60
|
||||
|
||||
filterwarnings =
|
||||
error
|
||||
# re-enable standard library warnings
|
||||
once::DeprecationWarning
|
||||
once::PendingDeprecationWarning
|
||||
@ -19,3 +20,5 @@ filterwarnings =
|
||||
ignore:Visible window deleted. To prevent this, store a reference to the window object.
|
||||
# xvfb warnings on non-linux systems
|
||||
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
|
||||
pg.mkQApp()
|
||||
|
@ -1,11 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pytest
|
||||
import pyqtgraph as pg
|
||||
from collections import OrderedDict
|
||||
import pyqtgraph.dockarea as da
|
||||
|
||||
pg.mkQApp()
|
||||
|
||||
import pyqtgraph.dockarea as da
|
||||
|
||||
def test_dockarea():
|
||||
a = da.DockArea()
|
||||
@ -176,14 +174,14 @@ def test_dockarea():
|
||||
# a superfluous vertical splitter in state2 has been removed
|
||||
state4 = a4.saveState()
|
||||
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):
|
||||
# return state dict with sizes removed
|
||||
ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
|
||||
state = (state[0], ch, {})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_dockarea()
|
||||
return state
|
@ -4,7 +4,6 @@ CSV export test
|
||||
from __future__ import division, print_function, absolute_import
|
||||
import pyqtgraph as pg
|
||||
import csv
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
app = pg.mkQApp()
|
||||
@ -15,44 +14,33 @@ def approxeq(a, b):
|
||||
|
||||
|
||||
def test_CSVExporter():
|
||||
tempfilename = tempfile.NamedTemporaryFile(suffix='.csv').name
|
||||
print("using %s as a temporary file" % tempfilename)
|
||||
|
||||
plt = pg.plot()
|
||||
y1 = [1,3,2,3,1,6,9,8,4,2]
|
||||
plt.plot(y=y1, name='myPlot')
|
||||
|
||||
|
||||
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
|
||||
x2 = pg.np.linspace(0, 1.0, len(y2))
|
||||
plt.plot(x=x2, y=y2)
|
||||
|
||||
|
||||
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
|
||||
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
|
||||
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:
|
||||
r = csv.reader(csv_file)
|
||||
lines = [line for line in r]
|
||||
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||
with tempfile.NamedTemporaryFile(mode="w+t", suffix='.csv', encoding="utf-8", delete=False) as tf:
|
||||
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)
|
||||
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
||||
|
||||
i = 0
|
||||
for vals in lines:
|
||||
|
||||
for i, vals in enumerate(lines):
|
||||
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(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(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])
|
||||
i += 1
|
||||
|
||||
os.unlink(tempfilename)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_CSVExporter()
|
@ -5,7 +5,6 @@ from pyqtgraph.exporters import HDF5Exporter
|
||||
import numpy as np
|
||||
from numpy.testing import assert_equal
|
||||
import h5py
|
||||
import os
|
||||
|
||||
|
||||
@pytest.fixture
|
@ -1,18 +1,10 @@
|
||||
"""
|
||||
SVG export test
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
import pyqtgraph as pg
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
||||
|
||||
def test_plotscene():
|
||||
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name
|
||||
print("using %s as a temporary file" % tempfilename)
|
||||
def test_plotscene(tmpdir):
|
||||
pg.setConfigOption('foreground', (0,0,0))
|
||||
w = pg.GraphicsLayoutWidget()
|
||||
w.show()
|
||||
@ -25,15 +17,13 @@ def test_plotscene():
|
||||
app.processEvents()
|
||||
|
||||
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
|
||||
os.unlink(tempfilename)
|
||||
w.close()
|
||||
|
||||
def test_simple():
|
||||
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name
|
||||
print("using %s as a temporary file" % tempfilename)
|
||||
|
||||
def test_simple(tmpdir):
|
||||
view = pg.GraphicsView()
|
||||
view.show()
|
||||
|
||||
@ -78,5 +68,5 @@ def test_simple():
|
||||
grp2.addItem(rect3)
|
||||
|
||||
ex = pg.exporters.SVGExporter(scene)
|
||||
ex.export(fileName=tempfilename)
|
||||
os.unlink(tempfilename)
|
||||
tf = tmpdir.join("expot.svg")
|
||||
ex.export(fileName=tf)
|
@ -15,7 +15,7 @@ def test_PlotItem_shared_axis_items(orientation):
|
||||
|
||||
layout = pg.GraphicsLayoutWidget()
|
||||
|
||||
pi1 = layout.addPlot(axisItems={orientation: ax1})
|
||||
_ = layout.addPlot(axisItems={orientation: ax1})
|
||||
|
||||
pi2 = layout.addPlot()
|
||||
# left or bottom replaces, right or top adds new
|
@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pyqtgraph as pg
|
||||
import pytest
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
@ -1,16 +1,19 @@
|
||||
import pyqtgraph as pg
|
||||
from math import isclose
|
||||
|
||||
app = pg.mkQApp()
|
||||
|
||||
|
||||
def test_AxisItem_stopAxisAtTick(monkeypatch):
|
||||
def test_bottom(p, axisSpec, tickSpecs, textSpecs):
|
||||
assert view.mapToView(axisSpec[1]).x() == 0.25
|
||||
assert view.mapToView(axisSpec[2]).x() == 0.75
|
||||
viewPixelSize = view.viewPixelSize()
|
||||
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):
|
||||
assert view.mapToView(axisSpec[1]).y() == 0.875
|
||||
assert view.mapToView(axisSpec[2]).y() == 0.125
|
||||
viewPixelSize = view.viewPixelSize()
|
||||
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()
|
||||
view = plot.plotItem.getViewBox()
|
@ -1,11 +1,8 @@
|
||||
import weakref
|
||||
try:
|
||||
import faulthandler
|
||||
faulthandler.enable()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import pyqtgraph as pg
|
||||
import faulthandler
|
||||
faulthandler.enable()
|
||||
|
||||
pg.mkQApp()
|
||||
|
||||
def test_getViewWidget():
|
@ -2,10 +2,10 @@
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from pyqtgraph.Qt import QtCore, QtGui, QtTest
|
||||
from pyqtgraph.Qt import QtGui, QtTest
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.tests import assertImageApproved, TransposedImageItem
|
||||
from tests.image_testing import assertImageApproved, TransposedImageItem
|
||||
try:
|
||||
import cupy
|
||||
except ImportError:
|
||||
@ -188,7 +188,6 @@ def test_ImageItem_axisorder():
|
||||
|
||||
|
||||
def test_dividebyzero():
|
||||
import pyqtgraph as pg
|
||||
im = pg.image(pg.np.random.normal(size=(100,100)))
|
||||
im.imageItem.setAutoDownsample(True)
|
||||
im.view.setRange(xRange=[-5+25, 5e+25],yRange=[-5e+25, 5e+25])
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtGui, QtCore, QtTest
|
||||
from pyqtgraph.tests import mouseDrag, mouseMove
|
||||
from tests.ui_testing import mouseDrag, mouseMove
|
||||
pg.mkQApp()
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ def test_InfiniteLine():
|
||||
px = pg.Point(-0.5, -1.0 / 3**0.5)
|
||||
assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill)
|
||||
assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill)
|
||||
|
||||
plt.close()
|
||||
|
||||
def test_mouseInteraction():
|
||||
# disable delay of mouse move events because events is called immediately in test
|
||||
@ -96,7 +96,4 @@ def test_mouseInteraction():
|
||||
assert hline2.mouseHovering == False
|
||||
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
|
||||
assert hline2.value() == -1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_mouseInteraction()
|
||||
plt.close()
|
@ -2,7 +2,7 @@ import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtTest
|
||||
from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage
|
||||
from pyqtgraph.tests import assertImageApproved
|
||||
from tests.image_testing import assertImageApproved
|
||||
from pyqtgraph.colormap import ColorMap
|
||||
import pyqtgraph.functions as fn
|
||||
import pytest
|
||||
@ -93,7 +93,7 @@ def test_NonUniformImage_colormap():
|
||||
|
||||
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)
|
||||
|
||||
viewbox.addItem(image)
|
@ -1,6 +1,6 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.tests import assertImageApproved
|
||||
from tests.image_testing import assertImageApproved
|
||||
|
||||
|
||||
def test_PlotCurveItem():
|
||||
@ -30,7 +30,3 @@ def test_PlotCurveItem():
|
||||
assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.")
|
||||
|
||||
p.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_PlotCurveItem()
|
@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import numpy as np
|
||||
import pytest
|
||||
import pyqtgraph as pg
|
||||
import platform
|
||||
from pyqtgraph.Qt import QtCore, QtGui, QtTest
|
||||
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow
|
||||
import pytest
|
||||
from tests.image_testing import assertImageApproved
|
||||
from tests.ui_testing import mouseMove, mouseDrag, mouseClick, resizeWindow
|
||||
|
||||
app = pg.mkQApp()
|
||||
pg.setConfigOption("mouseRateLimit", 0)
|
||||
@ -39,17 +39,20 @@ def test_getArrayRegion_axisorder():
|
||||
|
||||
|
||||
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()
|
||||
|
||||
#win = pg.GraphicsLayoutWidget()
|
||||
win = pg.GraphicsView()
|
||||
win.show()
|
||||
resizeWindow(win, 200, 400)
|
||||
# Don't use Qt's layouts for testing--these generate unpredictable results.
|
||||
#vb1 = win.addViewBox()
|
||||
#win.nextRow()
|
||||
#vb2 = win.addViewBox()
|
||||
|
||||
# Don't use Qt's layouts for testing--these generate unpredictable results.
|
||||
# Instead, place the viewboxes manually
|
||||
vb1 = pg.ViewBox()
|
||||
win.scene().addItem(vb1)
|
||||
@ -97,7 +100,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
|
||||
vb2.enableAutoRange(True, True)
|
||||
|
||||
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):
|
||||
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))
|
||||
img2.setImage(rgn[0, ..., 0])
|
||||
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.setPos([3, 0])
|
||||
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
|
||||
img2.setImage(rgn[0, ..., 0])
|
||||
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:
|
||||
roi.setSize([60, 60])
|
||||
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
|
||||
img2.setImage(rgn[0, ..., 0])
|
||||
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.setTransform(QtGui.QTransform().scale(1, -1).rotate(20), True)
|
||||
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
|
||||
img2.setImage(rgn[0, ..., 0])
|
||||
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()
|
||||
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
|
||||
img2.setImage(rgn[0, ..., 0])
|
||||
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)
|
||||
|
||||
roi.setState(initState)
|
||||
@ -146,7 +144,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
|
||||
rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
|
||||
img2.setImage(rgn[0, ..., 0])
|
||||
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
|
||||
roi.scene().removeItem(roi)
|
@ -5,7 +5,6 @@ import numpy as np
|
||||
|
||||
def test_scatterplotitem():
|
||||
app = pg.mkQApp()
|
||||
app.processEvents()
|
||||
|
||||
plot = pg.PlotWidget()
|
||||
# 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].brush() == pg.mkBrush(None)
|
||||
assert spots[1].data() == 'zzz'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_scatterplotitem()
|
||||
plot.close()
|
@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import pyqtgraph as pg
|
||||
|
||||
app = pg.mkQApp()
|
@ -3,68 +3,32 @@
|
||||
"""
|
||||
Procedure for unit-testing with images:
|
||||
|
||||
1. Run unit tests at least once; this initializes a git clone of
|
||||
pyqtgraph/test-data in ~/.pyqtgraph.
|
||||
|
||||
2. Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set:
|
||||
Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set:
|
||||
|
||||
$ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py
|
||||
|
||||
Any failing tests will display the test results, standard image, and the
|
||||
differences between the two. If the test result is bad, then press (f)ail.
|
||||
If the test result is good, then press (p)ass and the new image will be
|
||||
saved to the test-data directory.
|
||||
|
||||
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.
|
||||
Any failing tests will display the test results, standard image, and the
|
||||
differences between the two. If the test result is bad, then press (f)ail.
|
||||
If the test result is good, then press (p)ass and the new image will be
|
||||
saved to the test-data directory.
|
||||
|
||||
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 os
|
||||
import sys
|
||||
import inspect
|
||||
import base64
|
||||
import subprocess as sp
|
||||
import warnings
|
||||
import numpy as np
|
||||
|
||||
if sys.version[0] >= '3':
|
||||
import http.client as httplib
|
||||
import urllib.parse as urllib
|
||||
else:
|
||||
import httplib
|
||||
import urllib
|
||||
from ..Qt import QtGui, QtCore, QtTest, QT_LIB
|
||||
from .. import functions as fn
|
||||
from .. import GraphicsLayoutWidget
|
||||
from .. import ImageItem, TextItem
|
||||
from pathlib import Path
|
||||
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph import functions as fn
|
||||
from pyqtgraph import GraphicsLayoutWidget
|
||||
from pyqtgraph import ImageItem, TextItem
|
||||
|
||||
|
||||
tester = None
|
||||
@ -101,6 +65,21 @@ def getTester():
|
||||
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):
|
||||
"""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
|
||||
image as the standard.
|
||||
|
||||
This function will automatically clone the test-data repository into
|
||||
~/.pyqtgraph/test-data. However, it is up to the user to ensure this repository
|
||||
is kept up to date and to commit/push new images after they are saved.
|
||||
|
||||
Run the test with the environment variable PYQTGRAPH_AUDIT=1 to bring up
|
||||
the auditing GUI.
|
||||
|
||||
@ -131,43 +106,28 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||
comparison (see ``assertImageMatch()``).
|
||||
"""
|
||||
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()
|
||||
|
||||
graphstate = scenegraphState(w, standardFile)
|
||||
qimg = QtGui.QImage(w.size(), QtGui.QImage.Format.Format_ARGB32)
|
||||
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]]
|
||||
graphstate = scenegraphState(image, standardFile)
|
||||
image = getImageFromWidget(image)
|
||||
|
||||
if message is None:
|
||||
code = inspect.currentframe().f_back.f_code
|
||||
message = "%s::%s" % (code.co_filename, code.co_name)
|
||||
|
||||
# Make sure we have a test data repo available, possibly invoking git
|
||||
dataPath = getTestDataRepo()
|
||||
# Make sure we have a test data repo available
|
||||
dataPath = getTestDataDirectory()
|
||||
|
||||
# Read the standard image if it exists
|
||||
stdFileName = os.path.join(dataPath, standardFile + '.png')
|
||||
if not os.path.isfile(stdFileName):
|
||||
stdImage = None
|
||||
else:
|
||||
pxm = QtGui.QPixmap()
|
||||
pxm.load(stdFileName)
|
||||
stdImage = fn.imageToArray(pxm.toImage(), copy=True, transpose=False)
|
||||
qimg = QtGui.QImage(stdFileName)
|
||||
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
|
||||
stdImage = fn.qimage_to_ndarray(qimg).copy()
|
||||
del qimg
|
||||
|
||||
# If the test image does not match, then we go to audit if requested.
|
||||
try:
|
||||
@ -191,18 +151,13 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||
image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype)
|
||||
|
||||
assertImageMatch(image, stdImage, **kwargs)
|
||||
|
||||
|
||||
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
|
||||
print(graphstate)
|
||||
|
||||
|
||||
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
|
||||
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
|
||||
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':
|
||||
sys.excepthook(*sys.exc_info())
|
||||
getTester().test(image, stdImage, message)
|
||||
@ -210,20 +165,18 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||
print('Saving new standard image to "%s"' % stdFileName)
|
||||
if not os.path.isdir(stdPath):
|
||||
os.makedirs(stdPath)
|
||||
img = fn.makeQImage(image, alpha=True, transpose=False)
|
||||
img.save(stdFileName)
|
||||
qimg = fn.ndarray_to_qimage(image, QtGui.QImage.Format.Format_RGBA8888)
|
||||
qimg.save(stdFileName)
|
||||
del qimg
|
||||
else:
|
||||
if stdImage is None:
|
||||
raise Exception("Test standard %s does not exist. Set "
|
||||
"PYQTGRAPH_AUDIT=1 to add this image." % stdFileName)
|
||||
else:
|
||||
if os.getenv('TRAVIS') is not None:
|
||||
saveFailedTest(image, stdImage, standardFile, upload=True)
|
||||
elif os.getenv('CI') is not None:
|
||||
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
|
||||
saveFailedTest(image, stdImage, standardFile)
|
||||
print(graphstate)
|
||||
raise
|
||||
if os.getenv('CI') is not None:
|
||||
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
|
||||
saveFailedTest(image, stdImage, standardFile)
|
||||
print(graphstate)
|
||||
raise
|
||||
|
||||
|
||||
def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
||||
@ -249,8 +202,8 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
||||
pxThreshold : float
|
||||
Minimum value difference at which two pixels are considered different
|
||||
pxCount : int or None
|
||||
Maximum number of pixels that may differ. Default is 0 for Qt4 and
|
||||
1% of image size for Qt5.
|
||||
Maximum number of pixels that may differ. Default is 0, on Windows some
|
||||
tests have a value of 2.
|
||||
maxPxDiff : float or None
|
||||
Maximum allowed difference between pixels
|
||||
avgPxDiff : float or None
|
||||
@ -264,12 +217,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
||||
assert im1.dtype == im2.dtype
|
||||
|
||||
if pxCount == -1:
|
||||
if QT_LIB in {'PyQt5', 'PySide2', 'PySide6', 'PyQt6'}:
|
||||
# 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
|
||||
pxCount = 0
|
||||
|
||||
diff = im1.astype(float) - im2.astype(float)
|
||||
if imgDiff is not None:
|
||||
@ -292,9 +240,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
||||
assert corr >= minCorr
|
||||
|
||||
|
||||
def saveFailedTest(data, expect, filename, upload=False):
|
||||
"""Upload failed test images to web server to allow CI test debugging.
|
||||
"""
|
||||
def saveFailedTest(data, expect, filename):
|
||||
# concatenate data, expect, and diff into a single image
|
||||
ds = data.shape
|
||||
es = expect.shape
|
||||
@ -310,7 +256,7 @@ def saveFailedTest(data, expect, filename, upload=False):
|
||||
diff = makeDiffImage(data, expect)
|
||||
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)
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
@ -318,38 +264,15 @@ def saveFailedTest(data, expect, filename, upload=False):
|
||||
png_file.write(png)
|
||||
print("\nImage comparison failed. Test result: %s %s Expected result: "
|
||||
"%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):
|
||||
"""Given an array like (H, W, 4), return a PNG-encoded byte string.
|
||||
"""
|
||||
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')
|
||||
png = bytes(io.data().data())
|
||||
return png
|
||||
return bytes(io.data().data())
|
||||
|
||||
|
||||
def makeDiffImage(im1, im2):
|
||||
@ -467,155 +390,18 @@ class ImageTester(QtGui.QWidget):
|
||||
|
||||
|
||||
def getTestDataRepo():
|
||||
"""Return the path to a git repository with the required commit checked
|
||||
out.
|
||||
|
||||
If the repository does not exist, then it is cloned from
|
||||
https://github.com/pyqtgraph/test-data. If the repository already exists
|
||||
then the required commit is checked out.
|
||||
"""
|
||||
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
|
||||
warnings.warn(
|
||||
"Test data data repo has been merged with the main repo"
|
||||
"use getTestDataDirectory() instead, this method will be removed"
|
||||
"in a future version of pyqtgraph",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
return getTestDataDirectory()
|
||||
|
||||
|
||||
def gitCmdBase(path):
|
||||
return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path]
|
||||
|
||||
|
||||
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 getTestDataDirectory():
|
||||
dataPath = Path(__file__).absolute().parent / "images"
|
||||
return dataPath.as_posix()
|
||||
|
||||
|
||||
def scenegraphState(view, name):
|
||||
@ -632,7 +418,7 @@ def scenegraphState(view, name):
|
||||
|
||||
def itemState(root):
|
||||
state = str(root) + '\n'
|
||||
from .. import ViewBox
|
||||
from pyqtgraph import ViewBox
|
||||
state += 'bounding rect: ' + str(root.boundingRect()) + '\n'
|
||||
if isinstance(root, ViewBox):
|
||||
state += "view range: " + str(root.viewRange()) + '\n'
|
||||
@ -647,7 +433,7 @@ def transformStr(t):
|
||||
|
||||
|
||||
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):
|
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 |