diff --git a/.travis.yml b/.travis.yml index a8e162dea..1704efd98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,16 +3,23 @@ python: - '2.7_with_system_site_packages' - 2.6 before_install: - # For Python 2.7, install PyQt4 + - sudo apt-get update + # For Python 2.7, install PyQt4 and cairo - if [[ $TRAVIS_PYTHON_VERSION == '2.7_with_system_site_packages' ]]; then source .travis_before_install; fi - # PyQt can't be installed on 2.6; run the tests without Qt + # PyQt and cairo can't be installed on 2.6; run the tests without Qt - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then source .travis_before_install_noqt; fi - sudo apt-get install swig + # Simlinks for PIL compilation + - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libfreetype.so /usr/lib/ + - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libjpeg.so /usr/lib/ + - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libpng.so /usr/lib/ + - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libz.so /usr/lib/ install: # nose is already installed - pip install cython - pip install --upgrade numpy - pip install -r dev_requirements.txt + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r requirements-2.6.txt; fi - pip install coveralls - python setup.py develop script: diff --git a/.travis_before_install b/.travis_before_install index 74980b61c..77ce13d54 100644 --- a/.travis_before_install +++ b/.travis_before_install @@ -9,4 +9,12 @@ python -c 'import PyQt4' python -c 'import PyQt4.QtCore' python -c 'import PyQt4.QtGui' +wget -nv http://cairographics.org/releases/py2cairo-1.10.0.tar.bz2 +tar -xf py2cairo-1.10.0.tar.bz2 +pushd py2cairo-1.10.0 +./waf configure +./waf build +sudo ./waf install +popd + export ETS_TOOLKIT=qt4 \ No newline at end of file diff --git a/CHANGES.txt b/CHANGES.txt index a75c9b4d8..c3071f728 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,7 +8,7 @@ Enhancements ------------ * Added a base class for drag and drop tools, example and test support. - + * PR #160: Basic testing for kiva backends. Enable 4.4.0 (May 1, 2014) diff --git a/dev_requirements.txt b/dev_requirements.txt index 8cdd9ebbe..e3475f6ea 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -3,6 +3,10 @@ coverage PIL pyparsing +PyPDF2 +pyglet +pygarrayimage +reportlab -e git+http://github.com/enthought/traits.git#egg=traits -e git+http://github.com/enthought/traitsui.git#egg=traitsui -e git+http://github.com/nucleic/kiwi.git#egg=kiwisolver diff --git a/kiva/graphics_state.py b/kiva/graphics_state.py index a5d3ec0f3..0e7eded14 100644 --- a/kiva/graphics_state.py +++ b/kiva/graphics_state.py @@ -98,8 +98,12 @@ def __init__(self): line_cap = CAP_ROUND line_join = JOIN_MITER line_dash = (0, array([0])) # This will draw a solid line - self.line_state = LineState(line_color, line_width, line_cap, - line_join, line_dash) + + # FIXME: This is a very wierd class. The following code is here to + # make the basecore2d and the PS, SVG context managers happy + super(GraphicsState, self).__init__( + line_color, line_width, line_cap, line_join, line_dash) + self.line_state = self # All other default values. self.ctm = affine.affine_identity() diff --git a/kiva/pdf.py b/kiva/pdf.py index 1645898bd..967a73d71 100644 --- a/kiva/pdf.py +++ b/kiva/pdf.py @@ -723,6 +723,11 @@ def get_text_extent(self, textstring): # Painting paths (drawing and filling contours) # ---------------------------------------------------------------- + def clear(self): + """ + """ + warnings.warn("clear() is ignored for the pdf backend") + def stroke_path(self): """ """ diff --git a/kiva/qpainter.py b/kiva/qpainter.py index f9ff964de..8ec812d97 100644 --- a/kiva/qpainter.py +++ b/kiva/qpainter.py @@ -736,7 +736,7 @@ def fill_path(self): def eof_fill_path(self): """ """ - self.path.setFillRule(QtCore.Qt.OddEvenFill) + self.path.path.setFillRule(QtCore.Qt.OddEvenFill) self.gc.fillPath(self.path.path, self.gc.brush()) self.begin_path() diff --git a/kiva/tests/__init__.py b/kiva/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kiva/tests/drawing_tester.py b/kiva/tests/drawing_tester.py new file mode 100644 index 000000000..b31a90e0f --- /dev/null +++ b/kiva/tests/drawing_tester.py @@ -0,0 +1,179 @@ +import contextlib +import os +import shutil +import tempfile + +import numpy +from PIL import Image + +from kiva.fonttools import Font +from kiva.constants import MODERN + + +class DrawingTester(object): + """ Basic drawing tests for graphics contexts. + + """ + + def setUp(self): + self.directory = tempfile.mkdtemp() + self.filename = os.path.join(self.directory, 'rendered') + self.gc = self.create_graphics_context(300, 300) + self.gc.clear() + self.gc.set_stroke_color((1.0, 0.0, 0.0)) + self.gc.set_fill_color((1.0, 0.0, 0.0)) + self.gc.set_line_width(5) + + def tearDown(self): + del self.gc + shutil.rmtree(self.directory) + + def test_line(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.move_to(107, 204) + self.gc.line_to(107, 104) + self.gc.stroke_path() + + def test_rectangle(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.move_to(107, 104) + self.gc.line_to(107, 184) + self.gc.line_to(187, 184) + self.gc.line_to(187, 104) + self.gc.line_to(107, 104) + self.gc.stroke_path() + + def test_rect(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.rect(0, 0, 200, 200) + self.gc.stroke_path() + + def test_circle(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.arc(150, 150, 100, 0.0, 2 * numpy.pi) + self.gc.stroke_path() + + def test_quarter_circle(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.arc(150, 150, 100, 0.0, numpy.pi / 2) + self.gc.stroke_path() + + def test_text(self): + with self.draw_and_check(): + font = Font(family=MODERN) + font.size = 24 + self.gc.set_font(font) + self.gc.set_text_position(23, 67) + self.gc.show_text("hello kiva") + + def test_circle_fill(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.arc(150, 150, 100, 0.0, 2 * numpy.pi) + self.gc.fill_path() + + def test_star_fill(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.move_to(100, 100) + self.gc.line_to(150, 200) + self.gc.line_to(200, 100) + self.gc.line_to(100, 150) + self.gc.line_to(200, 150) + self.gc.line_to(100, 100) + self.gc.fill_path() + + def test_star_eof_fill(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.move_to(100, 100) + self.gc.line_to(150, 200) + self.gc.line_to(200, 100) + self.gc.line_to(100, 150) + self.gc.line_to(200, 150) + self.gc.line_to(100, 100) + self.gc.eof_fill_path() + + def test_circle_clip(self): + with self.draw_and_check(): + self.gc.clip_to_rect(150, 150, 100, 100) + self.gc.begin_path() + self.gc.arc(150, 150, 100, 0.0, 2 * numpy.pi) + self.gc.fill_path() + + def test_text_clip(self): + with self.draw_and_check(): + self.gc.clip_to_rect(23, 77, 100, 23) + font = Font(family=MODERN) + font.size = 24 + self.gc.set_font(font) + self.gc.set_text_position(23, 67) + self.gc.show_text("hello kiva") + + def test_star_clip(self): + with self.draw_and_check(): + self.gc.begin_path() + self.gc.move_to(100, 100) + self.gc.line_to(150, 200) + self.gc.line_to(200, 100) + self.gc.line_to(100, 150) + self.gc.line_to(200, 150) + self.gc.line_to(100, 100) + self.gc.close_path() + self.gc.clip() + + self.gc.begin_path() + self.gc.arc(150, 150, 100, 0.0, 2 * numpy.pi) + self.gc.fill_path() + + #### Required methods #################################################### + + @contextlib.contextmanager + def draw_and_check(self): + """ A context manager to check the result. + + """ + raise NotImplementedError() + + def create_graphics_context(self, width, length): + """ Create the desired graphics context + + """ + raise NotImplementedError() + + +class DrawingImageTester(DrawingTester): + """ Basic drawing tests for graphics contexts of gui toolkits. + + """ + + @contextlib.contextmanager + def draw_and_check(self): + yield + filename = "{0}.png".format(self.filename) + self.gc.save(filename) + self.assertImageSavedWithContent(filename) + + def assertImageSavedWithContent(self, filename): + """ Load the image and check that there is some content in it. + + """ + image = numpy.array(Image.open(filename)) + # default is expected to be a totally white image + + self.assertEqual(image.shape[:2], (300, 300)) + if image.shape[2] == 3: + check = numpy.sum(image == [255, 0, 0], axis=2) == 3 + elif image.shape[2] == 4: + check = numpy.sum(image == [255, 0, 0, 255], axis=2) == 4 + else: + self.fail( + 'Pixel size is not 3 or 4, but {0}'.format(image.shape[2])) + if check.any(): + return + self.fail('The image looks empty, no red pixels where drawn') diff --git a/kiva/tests/test_agg_drawing.py b/kiva/tests/test_agg_drawing.py new file mode 100644 index 000000000..154f523f9 --- /dev/null +++ b/kiva/tests/test_agg_drawing.py @@ -0,0 +1,13 @@ +from kiva.tests.drawing_tester import DrawingImageTester +from kiva.image import GraphicsContext +from traits.testing.unittest_tools import unittest + + +class TestAggDrawing(DrawingImageTester, unittest.TestCase): + + def create_graphics_context(self, width, height): + return GraphicsContext((width, height)) + + +if __name__ == "__main__": + unittest.main() diff --git a/kiva/tests/test_cairo_drawing.py b/kiva/tests/test_cairo_drawing.py new file mode 100644 index 000000000..fd82845b5 --- /dev/null +++ b/kiva/tests/test_cairo_drawing.py @@ -0,0 +1,21 @@ +from kiva.tests.drawing_tester import DrawingImageTester +from traits.testing.unittest_tools import unittest + +try: + import cairo # noqa +except ImportError: + CAIRO_NOT_AVAILABLE = True +else: + CAIRO_NOT_AVAILABLE = False + + +@unittest.skipIf(CAIRO_NOT_AVAILABLE, "Cannot import cairo") +class TestCairoDrawing(DrawingImageTester, unittest.TestCase): + + def create_graphics_context(self, width, height): + from kiva.cairo import GraphicsContext + return GraphicsContext((width, height)) + + +if __name__ == "__main__": + unittest.main() diff --git a/kiva/tests/test_gl_drawing.py b/kiva/tests/test_gl_drawing.py new file mode 100644 index 000000000..783e112ac --- /dev/null +++ b/kiva/tests/test_gl_drawing.py @@ -0,0 +1,57 @@ +import contextlib + +try: + import pyglet +except ImportError: + PYGLET_NOT_AVAILABLE = True +else: + PYGLET_NOT_AVAILABLE = False + + +from kiva.tests.drawing_tester import DrawingImageTester +from traits.testing.unittest_tools import unittest + + +@unittest.skipIf(PYGLET_NOT_AVAILABLE, "Cannot import pyglet") +class TestGLDrawing(DrawingImageTester, unittest.TestCase): + + def tearDown(self): + if hasattr(self, 'window') and self.window is not None: + self.window.close() + del self.window + DrawingImageTester.tearDown(self) + + def create_graphics_context(self, width, height): + from kiva.gl import GraphicsContext + self.window = pyglet.window.Window(width=width, height=height) + gc = GraphicsContext((width, height)) + gc.gl_init() + return gc + + @unittest.skip("gl graphics context does not support star_clip (#164)") + def test_star_clip(self): + # FIXME: overriding test since it segfaults + DrawingImageTester.test_star_clip(self) + + @unittest.expectedFailure + def test_text_clip(self): + # gl graphics context does not clip text properly (#165). + DrawingImageTester.test_text_clip(self) + + @contextlib.contextmanager + def draw_and_check(self): + from pyglet.image.codecs.png import PNGImageEncoder + + self.window.clear() + self.window.switch_to() + self.window.dispatch_events() + yield + self.window.dispatch_events() + filename = "{0}.png".format(self.filename) + buffer = pyglet.image.get_buffer_manager() + buffer.get_color_buffer().save(filename, encoder=PNGImageEncoder()) + self.assertImageSavedWithContent(filename) + + +if __name__ == "__main__": + unittest.main() diff --git a/kiva/tests/test_pdf_drawing.py b/kiva/tests/test_pdf_drawing.py new file mode 100644 index 000000000..8a94c5664 --- /dev/null +++ b/kiva/tests/test_pdf_drawing.py @@ -0,0 +1,57 @@ +import contextlib + +from kiva.tests.drawing_tester import DrawingTester +from traits.testing.unittest_tools import unittest + +try: + import PyPDF2 # Tests require the PyPDF2 library. +except ImportError: + PYPDF2_NOT_AVAILABLE = True +else: + PYPDF2_NOT_AVAILABLE = False + +try: + import reportlab # noqa +except ImportError: + REPORTLAB_NOT_AVAILABLE = True +else: + REPORTLAB_NOT_AVAILABLE = False + + +@unittest.skipIf(PYPDF2_NOT_AVAILABLE, "PDF tests require PyPDF2") +@unittest.skipIf(REPORTLAB_NOT_AVAILABLE, "Cannot import reportlab") +class TestPDFDrawing(DrawingTester, unittest.TestCase): + + def create_graphics_context(self, width, height): + from reportlab.pdfgen.canvas import Canvas + from kiva.pdf import GraphicsContext + filename = "{0}.pdf".format(self.filename) + canvas = Canvas(filename, (width, height)) + return GraphicsContext(canvas) + + @contextlib.contextmanager + def draw_and_check(self): + yield + # Save the pdf file. + filename = "{0}.pdf".format(self.filename) + self.gc.save() + reader = PyPDF2.PdfFileReader(filename) + self.assertEqual(reader.getNumPages(), 1) + + # Find the graphics in the page + page = reader.getPage(0) + content = page.getContents() + + # Just a simple check that the path has been closed or the text has + # been drawn. + line = content.getData().splitlines()[-2] + if not any(( + line.endswith('f'), + line.endswith('S'), + line.endswith('f*'), + line.endswith('ET') and 'hello kiva' in line)): + self.fail('Path was not closed') + + +if __name__ == "__main__": + unittest.main() diff --git a/kiva/tests/test_ps_drawing.py b/kiva/tests/test_ps_drawing.py new file mode 100644 index 000000000..8445c067c --- /dev/null +++ b/kiva/tests/test_ps_drawing.py @@ -0,0 +1,33 @@ +import contextlib + +from kiva.tests.drawing_tester import DrawingTester +from kiva.ps import PSGC +from traits.testing.unittest_tools import unittest + + +class TestPSDrawing(DrawingTester, unittest.TestCase): + + def create_graphics_context(self, width, height): + return PSGC((width, height)) + + @contextlib.contextmanager + def draw_and_check(self): + yield + filename = "{0}.eps".format(self.filename) + self.gc.save(filename) + with open(filename, 'r') as handle: + lines = handle.readlines() + + # Just a simple check that the path has been closed or the text has + # been drawn. + line = lines[-1].strip() + if not any(( + line.endswith('fill'), + line.endswith('stroke'), + line.endswith('cliprestore'), + '(hello kiva) show\n' in lines)): + self.fail('Path was not closed') + + +if __name__ == "__main__": + unittest.main() diff --git a/kiva/tests/test_qpainter_drawing.py b/kiva/tests/test_qpainter_drawing.py new file mode 100644 index 000000000..19149ff11 --- /dev/null +++ b/kiva/tests/test_qpainter_drawing.py @@ -0,0 +1,30 @@ +try: + from pyface.qt import QtGui +except ImportError: + QT_NOT_AVAILABLE = True +else: + QT_NOT_AVAILABLE = False + +from kiva.tests.drawing_tester import DrawingImageTester +from traits.testing.unittest_tools import unittest + + +@unittest.skipIf(QT_NOT_AVAILABLE, "Cannot import qt") +class TestQPainterDrawing(DrawingImageTester, unittest.TestCase): + + def setUp(self): + application = QtGui.QApplication.instance() + if application is None: + self.application = QtGui.QApplication([]) + else: + self.application = application + + DrawingImageTester.setUp(self) + + def create_graphics_context(self, width, height): + from kiva.qpainter import GraphicsContext + return GraphicsContext((width, height)) + + +if __name__ == "__main__": + unittest.main() diff --git a/kiva/tests/test_svg_drawing.py b/kiva/tests/test_svg_drawing.py new file mode 100644 index 000000000..5eba7bc04 --- /dev/null +++ b/kiva/tests/test_svg_drawing.py @@ -0,0 +1,26 @@ +import contextlib +from xml.etree import ElementTree + +from kiva.tests.drawing_tester import DrawingTester +from kiva.svg import GraphicsContext +from traits.testing.unittest_tools import unittest + + +class TestSVGDrawing(DrawingTester, unittest.TestCase): + + def create_graphics_context(self, width, height): + return GraphicsContext((width, height)) + + @contextlib.contextmanager + def draw_and_check(self): + yield + filename = "{0}.svg".format(self.filename) + self.gc.save(filename) + tree = ElementTree.parse(filename) + elements = [element for element in tree.getiterator()] + if not len(elements) in [4, 7]: + self.fail('The expected number of elements was not found') + + +if __name__ == "__main__": + unittest.main() diff --git a/requirements-2.6.txt b/requirements-2.6.txt new file mode 100644 index 000000000..9a23970d3 --- /dev/null +++ b/requirements-2.6.txt @@ -0,0 +1 @@ +unittest2