Repository: ManiacalLabs/BiblioPixel
Pull request #388 (2017-08-21 23:30:02)

Added Automatic Demo for Remote Control
Also fixed `seconds` bug in Runner where `fps` was required.

This is a "SparkCon Special" push that will be 3.3.1


Patch:
diff --git a/bibliopixel/animation/animation.py b/bibliopixel/animation/animation.py
index 3552be6..5e3d6ce 100644
--- a/bibliopixel/animation/animation.py
+++ b/bibliopixel/animation/animation.py
@@ -12,6 +12,7 @@ class STATE(IntEnum):
     complete = 2
     canceled = 3
     max_steps = 4
+    timeout = 5
 
 
 class BaseAnimation(object):
@@ -66,6 +67,8 @@ class BaseAnimation(object):
     def compute_state(self):
         if self.threading.stop_event.isSet():
             self.state = STATE.canceled
+        elif self.runner.seconds and (time.time() - self.runner.run_start_time) > self.runner.seconds:
+            self.state = STATE.timeout
         elif self.runner.max_steps and not (self.cur_step < self.runner.max_steps):
             self.state = STATE.max_steps
         elif (not self.runner.until_complete and self.state == STATE.complete):
@@ -117,6 +120,7 @@ class BaseAnimation(object):
     @contextlib.contextmanager
     def run_context(self):
         self.state = STATE.running
+        self.runner.run_start_time = time.time()
         self.threading.stop_event.clear()
         self._step = 0
         self.cur_step = 0
diff --git a/bibliopixel/animation/runner.py b/bibliopixel/animation/runner.py
index c078a18..beb0af3 100644
--- a/bibliopixel/animation/runner.py
+++ b/bibliopixel/animation/runner.py
@@ -33,10 +33,9 @@ class Runner(object):
         self.fps = fps
         self.sleep_time = 1 / fps if fps else sleep_time
         self.until_complete = until_complete
-        if seconds is None:
-            self.max_steps = max_steps
-        else:
-            self.max_steps = int(seconds / self.sleep_time)
+        self.seconds = seconds
+        self.run_start_time = 0
+        self.max_steps = max_steps
         self.max_cycles = max_cycles
         self.seconds = seconds
         self.threaded = threaded
diff --git a/bibliopixel/animation/sequence.py b/bibliopixel/animation/sequence.py
index 60ddd81..25d2cec 100644
--- a/bibliopixel/animation/sequence.py
+++ b/bibliopixel/animation/sequence.py
@@ -1,14 +1,30 @@
 from . import collection
+import random as rand
 
 
 class Sequence(collection.Collection):
+    def __init__(self, layout, random=False, **kwds):
+        super().__init__(layout, **kwds)
+        self.random = random
+        self.indices = list(range(0, len(self.animations)))
+        self.sub_index = 0
+
+    def pre_run(self):
+        if self.random:
+            rand.shuffle(self.indices)
+        self.sub_index = 0
+
     def step(self, amt=1):
-        self.index += 1
-        if self.index >= len(self.animations):
+        self.sub_index += 1
+
+        if self.sub_index >= len(self.animations):
             if self.runner.until_complete:
                 self.completed = True
             else:
-                self.index = 0
+                rand.shuffle(self.indices)
+                self.sub_index = 0
+
+        self.index = self.indices[self.sub_index]
 
         if not self.completed and self.animations:
             self.current_animation.run_all_frames()
diff --git a/bibliopixel/project/types/colors.py b/bibliopixel/project/types/colors.py
new file mode 100644
index 0000000..825c2b5
--- /dev/null
+++ b/bibliopixel/project/types/colors.py
@@ -0,0 +1,22 @@
+import functools
+from . import color
+
+USAGE = """Colors is a list of colors.  Each color can be:
+
+* A list of three numbers: [0, 0, 0] or [255, 0, 255].
+* A single number which represents a brightness/gray level: 0, 255, 127
+* A string:  "red", "yellow", "gold" naming a color from ...colors.COLORS.
+
+All numbers must be in the range [0, 256) - 0 <= x < 256
+"""
+
+
+@functools.singledispatch
+def make(c):
+    raise ValueError("Don't understand type %s" % type(c), USAGE)
+
+
+@make.register(tuple)
+@make.register(list)
+def _(c):
+    return [color.make(i) for i in c]
diff --git a/bibliopixel/project/types/defaults.py b/bibliopixel/project/types/defaults.py
index faf3388..8aa574a 100644
--- a/bibliopixel/project/types/defaults.py
+++ b/bibliopixel/project/types/defaults.py
@@ -1,8 +1,9 @@
-from . import channel_order, color, duration, gamma, ledtype, rotation
+from . import channel_order, color, colors, duration, gamma, ledtype, rotation
 
 FIELD_TYPES = {
     'c_order': channel_order,
     'color': color,
+    'colors': colors,
     'duration': duration,
     'gamma': gamma,
     'ledtype': ledtype,
diff --git a/bibliopixel/remote/control.py b/bibliopixel/remote/control.py
index 83302ab..f9721de 100644
--- a/bibliopixel/remote/control.py
+++ b/bibliopixel/remote/control.py
@@ -34,10 +34,20 @@ class RemoteControl(collection.Collection):
                  external_access=False, port=5000,
                  title='BiblioPixel Remote', bgcolor='black',
                  font_color='white', default=None,
-                 triggers=[]):
+                 triggers=[], auto_demo=None):
 
         self.internal_delay = 0  # never wait
 
+        if auto_demo:
+            auto_demo_run = auto_demo.pop('run', 10)  # default to 10 seconds per
+            auto_demo['typename'] = 'sequence'
+            auto_demo['data'] = dict(DEFAULT_ANIM_CONFIG, **auto_demo.get('data', {}))
+            if 'name' not in auto_demo:
+                auto_demo['name'] = 'DEMO_ANIM'
+            auto_demo['data']['display'] = auto_demo['data'].get('display', auto_demo['name'])
+            auto_demo['name'] = normalize_name(auto_demo['name'])
+            auto_demo['animations'] = []
+
         # normalize name and display first
         for i, anim in enumerate(animations):
             adesc = anim['animation']
@@ -48,8 +58,18 @@ class RemoteControl(collection.Collection):
             adesc['data']['display'] = adesc['data'].get('display', adesc['name'])
             adesc['name'] = normalize_name(adesc['name'])
             anim['run'] = anim.get('run', {})
+            if auto_demo:
+                demo_sub = copy.deepcopy(anim)
+                demo_sub['run'].update(auto_demo_run)
+                demo_sub['animation'].pop('name')
+                demo_sub['animation'].pop('data')
+                demo_sub['run']['threaded'] = False  # no threading allowed for internal sequences
+                auto_demo['animations'].append(demo_sub)
             anim['run']['threaded'] = True  # threaded required
 
+        if auto_demo:
+            animations.insert(0, {'animation': auto_demo, 'run': {'threaded': True}})
+
         super().__init__(layout, copy.deepcopy(animations), True)
 
         self.name_map = {}


Test patch:
diff --git a/test/bibliopixel/layout/matrix_results.py b/test/bibliopixel/layout/matrix_results.py
new file mode 100644
index 0000000..3e27bfa
--- /dev/null
+++ b/test/bibliopixel/layout/matrix_results.py
@@ -0,0 +1,357 @@
+# This was file was automatically generated on 15:53:46 08/20/17 CEST
+
+BRESENHAM0 = (
+    '*       ',
+    ' *      ',
+    '  *     ',
+    '   *    ',
+    '    *   ',
+    '     *  ',
+    '      * ',
+    '       *',
+)
+
+BRESENHAM1 = (
+    '*       ',
+    ' *      ',
+    '  *     ',
+    '   *    ',
+    '    *   ',
+    '     *  ',
+    '      * ',
+    '       *',
+)
+
+BRESENHAM2 = (
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '   *            ',
+    '    *           ',
+    '     *          ',
+    '      *         ',
+    '       *        ',
+    '        *       ',
+    '         *      ',
+    '         *      ',
+    '          *     ',
+    '           *    ',
+    '            *   ',
+)
+
+BRESENHAM3 = (
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '   *            ',
+    '    *           ',
+    '     *          ',
+    '      *         ',
+    '       *        ',
+    '        *       ',
+    '         *      ',
+    '         *      ',
+    '          *     ',
+    '           *    ',
+    '            *   ',
+)
+
+DRAW_CIRCLE1 = (
+    '                ',
+    '                ',
+    '      *****     ',
+    '     *     *    ',
+    '    *       *   ',
+    '   *         *  ',
+    '  *           * ',
+    '  *           * ',
+    '  *           * ',
+    '  *           * ',
+    '  *           * ',
+    '   *         *  ',
+    '    *       *   ',
+    '     *     *    ',
+    '      *****     ',
+    '                ',
+)
+
+DRAW_CIRCLE2 = (
+    '   *    ',
+    '  *     ',
+    ' *      ',
+    '*       ',
+    '        ',
+    '        ',
+    '        ',
+    '        ',
+)
+
+DRAW_CIRCLE3 = (
+    '*   ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+    '    ',
+)
+
+DRAW_RECT = (
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '   ***          ',
+    '   ***          ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+)
+
+DRAW_TEXT = (
+    '      *                         ',
+    '      *                         ',
+    ' **   * **   ***                ',
+    '   *  **  * *   *               ',
+    ' ***  *   * *                   ',
+    '*  *  **  * *   *               ',
+    ' **** * **   ***                ',
+    '                                ',
+    '                                ',
+    '                                ',
+)
+
+DRAW_TRIANGLE = (
+    '**              ',
+    '* ***           ',
+    ' *   **         ',
+    ' *     ***      ',
+    '  *       **    ',
+    '  *       *     ',
+    '  *       *     ',
+    '   *     *      ',
+    '   *    *       ',
+    '    *  *        ',
+    '    *  *        ',
+    '     **         ',
+    '     *          ',
+    '                ',
+    '                ',
+    '                ',
+)
+
+FILL_CIRCLE1 = (
+    '                ',
+    '                ',
+    '      *****     ',
+    '     *******    ',
+    '    *********   ',
+    '   ***********  ',
+    '  ************* ',
+    '  ************* ',
+    '  ************* ',
+    '  ************* ',
+    '  ************* ',
+    '   ***********  ',
+    '    *********   ',
+    '     *******    ',
+    '      *****     ',
+    '                ',
+)
+
+FILL_CIRCLE2 = (
+    '********',
+    '********',
+    '********',
+    '********',
+    '********',
+    '********',
+    '********',
+    '********',
+)
+
+FILL_CIRCLE3 = (
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+    '****',
+)
+
+FILL_RECT = (
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '   ******       ',
+    '   ******       ',
+    '   ******       ',
+    '   ******       ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+)
+
+FILL_SCREEN = (
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+    '****************',
+)
+
+HORIZONTAL_LINE = (
+    '****************',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+)
+
+VERTICAL_LINE = (
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+    '*               ',
+)
+
+VERTICAL_LINE2 = (
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+    ' *              ',
+)
+
+WU0 = (
+    '*       ',
+    ' *      ',
+    '  *     ',
+    '   *    ',
+    '    *   ',
+    '     *  ',
+    '      * ',
+    '       *',
+)
+
+WU1 = (
+    '*       ',
+    ' *      ',
+    '  *     ',
+    '   *    ',
+    '    *   ',
+    '     *  ',
+    '      * ',
+    '       *',
+)
+
+WU2 = (
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '   *            ',
+    '   **           ',
+    '    **          ',
+    '     **         ',
+    '      **        ',
+    '       **       ',
+    '        **      ',
+    '         **     ',
+    '          **    ',
+    '           **   ',
+    '            **  ',
+)
+
+WU3 = (
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '                ',
+    '   *            ',
+    '   **           ',
+    '    **          ',
+    '     **         ',
+    '      **        ',
+    '       **       ',
+    '        **      ',
+    '         **     ',
+    '          **    ',
+    '           **   ',
+    '            **  ',
+)
diff --git a/test/bibliopixel/layout/matrix_test.py b/test/bibliopixel/layout/matrix_test.py
index de05439..4a7b330 100644
--- a/test/bibliopixel/layout/matrix_test.py
+++ b/test/bibliopixel/layout/matrix_test.py
@@ -1,175 +1,158 @@
-import unittest
+import contextlib, itertools, os, time, unittest
 
 from bibliopixel.project import data_maker
 from bibliopixel.layout import Matrix
 from bibliopixel.drivers.driver_base import DriverBase
+from . import matrix_results
 
 
 WHITE = (255, 255, 255)
 
+DUMP_FILENAME = os.environ.get('BP_MATRIX_DUMP')
+
+if DUMP_FILENAME:
+    with open(DUMP_FILENAME, 'w') as fp:
+        fp.write('# This was file was automatically generated on ')
+        fp.write(time.strftime('%X %x %Z'))
+        fp.write('\n')
+
 
 class BaseMatrixTest(unittest.TestCase):
 
-    def make_matrix(self, width, height, **kwds):
-        driver = DriverBase(num=width * height)
-        return Matrix(
-            driver, width=width, height=height, maker=self.maker, **kwds)
+    def text_at(self, x, y):
+        """Return text for a given pixel"""
+        return '*' if any(self.matrix.get(x, y)) else ' '
+
+    def line_at(self, y):
+        return ''.join(self.text_at(x, y) for x in range(self.matrix.width))
+
+    def to_strings(self):
+        return tuple(self.line_at(y) for y in range(self.matrix.height))
+
+    def name_of_test(self):
+        name = self.id().split('.')[-1]
+        if name.startswith('test_'):
+            name = name[len('test_'):]
+        return name.upper()
+
+    def dump(self):
+        # Dump the result to a file, if enabled.
+        pass
 
-    def assert_changed(self, matrix, expected):
-        changed = [i for i, c in enumerate(matrix._colors) if c[0]]
-        if changed != expected:
-            print(changed)
-        self.assertEqual(changed, expected)
+    @contextlib.contextmanager
+    def matrix_test(self, width=16, height=16):
+        d = DriverBase(num=width * height)
+        self.matrix = Matrix(d, width=width, height=height, maker=self.maker)
 
-    def assert_unchanged(self, matrix, expected):
-        unchanged = [i for i, c in enumerate(matrix._colors) if not c[0]]
-        self.assertEqual(unchanged, expected)
+        yield  # Perform your operation here.
+
+        self.dump()
+        expected = getattr(matrix_results, self.name_of_test())
+        actual = self.to_strings()
+        if expected != actual:
+            print('Expected:', *(repr(s) for s in expected), sep='\n')
+            print('Actual:', *(repr(s) for s in actual), sep='\n')
+            self.assertTrue(False)
+
+    def test_horizontal_line(self):
+        with self.matrix_test():
+            self.matrix.drawLine(0, 0, 15, 0, WHITE)
+
+    def test_vertical_line(self):
+        with self.matrix_test():
+            self.matrix.drawLine(0, 0, 0, 15, WHITE)
+
+    def test_vertical_line2(self):
+        with self.matrix_test():
+            self.matrix.drawLine(1, 0, 1, 15, WHITE)
 
     def test_draw_circle1(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.drawCircle(8, 8, 6, WHITE)
-        expected = [
-            38, 39, 40, 41, 42, 52, 58, 68,
-            76, 82, 92, 98, 110, 113, 125, 130,
-            142, 145, 157, 162, 174, 178, 188,
-            196, 204, 212, 218, 230, 231, 232, 233, 234]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.drawCircle(8, 8, 6, WHITE)
 
     def test_draw_circle2(self):
-        matrix = self.make_matrix(width=8, height=8)
-        matrix.drawCircle(4, 4, 15, WHITE)
-        expected = [3, 13, 17, 31]  # Why isn't this empty!?
-        self.assert_changed(matrix, expected)
+        with self.matrix_test(8, 8):
+            self.matrix.drawCircle(4, 4, 15, WHITE)
 
     def test_draw_circle3(self):
-        matrix = self.make_matrix(width=4, height=12)
-        matrix.drawCircle(4, 6, 20, WHITE)
-        expected = [0]
-        self.assert_changed(matrix, expected)  # Huh?
+        with self.matrix_test(4, 12):
+            self.matrix.drawCircle(4, 6, 20, WHITE)
 
     def test_fill_circle1(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.fillCircle(8, 8, 6, WHITE)
-        expected = [
-            38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, 58, 68, 69, 70, 71,
-            72, 73, 74, 75, 76, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92,
-            98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 113,
-            114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 130,
-            131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 145,
-            146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 162,
-            163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 178,
-            179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 196, 197, 198,
-            199, 200, 201, 202, 203, 204, 212, 213, 214, 215, 216, 217, 218,
-            230, 231, 232, 233, 234]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.fillCircle(8, 8, 6, WHITE)
 
     def test_fill_circle2(self):
-        matrix = self.make_matrix(width=8, height=8)
-        matrix.fillCircle(4, 4, 15, WHITE)
-        expected = []
-        self.assert_unchanged(matrix, expected)
+        with self.matrix_test(8, 8):
+            self.matrix.fillCircle(4, 4, 15, WHITE)
 
     def test_fill_circle3(self):
-        matrix = self.make_matrix(width=4, height=12)
-        matrix.fillCircle(4, 6, 20, WHITE)
-        expected = []
-        self.assert_unchanged(matrix, expected)
+        with self.matrix_test(4, 12):
+            self.matrix.fillCircle(4, 6, 20, WHITE)
 
-    def test_bresenham1(self):
-        matrix = self.make_matrix(width=8, height=8)
-        matrix.bresenham_line(0, 0, 8, 8, WHITE)
-        expected = [0, 14, 18, 28, 36, 42, 54, 56]
-        self.assert_changed(matrix, expected)
+    def test_bresenham0(self):
+        with self.matrix_test(8, 8):
+            self.matrix.bresenham_line(0, 0, 8, 8, WHITE)
 
-        matrix = self.make_matrix(width=8, height=8)
-        matrix.bresenham_line(8, 8, 0, 0, WHITE)
-        self.assert_changed(matrix, expected)
+    def test_bresenham1(self):
+        with self.matrix_test(8, 8):
+            self.matrix.bresenham_line(8, 8, 0, 0, WHITE)
 
     def test_bresenham2(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.bresenham_line(3, 5, 15, 18, WHITE)
-        expected = [92, 100, 122, 134, 152, 168, 182, 201, 213, 235, 243]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.bresenham_line(3, 5, 15, 18, WHITE)
 
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.bresenham_line(15, 18, 3, 5, WHITE)
-        self.assert_changed(matrix, expected)
+    def test_bresenham3(self):
+        with self.matrix_test():
+            self.matrix.bresenham_line(15, 18, 3, 5, WHITE)
 
-    def test_wu1(self):
-        matrix = self.make_matrix(width=8, height=8)
-        matrix.wu_line(0, 0, 8, 8, WHITE)
-        expected = [0, 14, 18, 28, 36, 42, 54, 56]
-        self.assert_changed(matrix, expected)
+    def test_wu0(self):
+        with self.matrix_test(8, 8):
+            self.matrix.wu_line(0, 0, 8, 8, WHITE)
 
-        matrix = self.make_matrix(width=8, height=8)
-        matrix.wu_line(8, 8, 0, 0, WHITE)
-        self.assert_changed(matrix, expected)
+    def test_wu1(self):
+        with self.matrix_test(8, 8):
+            self.matrix.wu_line(8, 8, 0, 0, WHITE)
 
     def test_wu2(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.wu_line(3, 5, 15, 18, WHITE)
-        expected = [92, 99, 100, 122, 123, 133, 134, 152, 153,
-                    167, 168, 182, 183, 201, 202, 212, 213, 235, 236,
-                    242, 243]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.wu_line(3, 5, 15, 18, WHITE)
 
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.wu_line(15, 18, 3, 5, WHITE)
-        self.assert_changed(matrix, expected)
+    def test_wu3(self):
+        with self.matrix_test():
+            self.matrix.wu_line(15, 18, 3, 5, WHITE)
 
     def test_draw_rect(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.drawRect(3, 5, 3, 2, WHITE)
-        expected = [90, 91, 92, 99, 100, 101]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.drawRect(3, 5, 3, 2, WHITE)
 
     def test_fill_rect(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.fillRect(3, 5, 6, 4, WHITE)
-        expected = [87, 88, 89, 90, 91, 92, 99, 100, 101, 102, 103, 104, 119,
-                    120, 121, 122, 123, 124, 131, 132, 133, 134, 135, 136]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.fillRect(3, 5, 6, 4, WHITE)
 
     def test_fill_screen(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.fillScreen(WHITE)
-        expected = []
-        self.assert_unchanged(matrix, expected)
+        with self.matrix_test():
+            self.matrix.fillScreen(WHITE)
 
     def DISABLED_test_draw_round_rect(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.drawRoundRect(3, 5, 6, 7, 7, WHITE)
-        expected = []
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.drawRoundRect(3, 5, 6, 7, 7, WHITE)
 
     def DISABLED_test_fill_round_rect(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.fillRoundRect(3, 5, 6, 7, 7, WHITE)
-        expected = []
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.fillRoundRect(3, 5, 6, 7, 7, WHITE)
 
     def test_draw_triangle(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.drawTriangle(0, 0, 11, 4, 5, 12, WHITE)
-        expected = [0, 1, 27, 28, 29, 31, 33, 37, 38, 54, 55, 56, 62, 66, 74,
-                    75, 85, 93, 98, 106, 118, 124, 131, 136, 152, 155, 164, 167,
-                    185, 186, 197]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.drawTriangle(0, 0, 11, 4, 5, 12, WHITE)
 
     def DISABLED_test_fill_triangle(self):
-        matrix = self.make_matrix(width=16, height=16)
-        matrix.fillTriangle(0, 0, 11, 4, 5, 12, WHITE)
-        expected = []
-        self.assert_changed(matrix, expected)
+        with self.matrix_test():
+            self.matrix.fillTriangle(0, 0, 11, 4, 5, 12, WHITE)
 
     def test_draw_text(self):
-        matrix = self.make_matrix(width=32, height=10)
-        matrix.drawText('abc', color=WHITE)
-        expected = [6, 57, 65, 66, 70, 72, 73, 77, 78, 79, 111, 115, 117, 120,
-                    121, 124, 129, 130, 131, 134, 138, 140, 175, 179, 181, 184,
-                    185, 188, 191, 193, 194, 195, 196, 198, 200, 201, 205, 206,
-                    207]
-        self.assert_changed(matrix, expected)
+        with self.matrix_test(32, 10):
+            self.matrix.drawText('abc', color=WHITE)
 
 
 class MatrixTest(BaseMatrixTest):
@@ -184,4 +167,28 @@ class SharedMatrixIntegerTest(BaseMatrixTest):
     maker = data_maker.Maker(shared_memory=True, floating=False)
 
 
+class DumpTest(BaseMatrixTest):
+    maker = data_maker.Maker()
+    indent = ''
+
+    def dump(self):
+        if not DUMP_FILENAME:
+            return
+
+        with open(DUMP_FILENAME, 'a') as fp:
+            def writeln(*parts):
+                if parts:
+                    fp.write(self.indent)
+                    fp.writelines(itertools.chain(*parts))
+                fp.write('\n')
+
+            writeln()
+            writeln(self.name_of_test(), ' = (')
+
+            for row in self.to_strings():
+                writeln("    '", row, "',")
+
+            writeln(')')
+
+
 del BaseMatrixTest  # http://stackoverflow.com/a/22836015/43839
diff --git a/test/bibliopixel/project/types/colors_test.py b/test/bibliopixel/project/types/colors_test.py
new file mode 100644
index 0000000..9aba350
--- /dev/null
+++ b/test/bibliopixel/project/types/colors_test.py
@@ -0,0 +1,14 @@
+from . base import TypesBaseTest
+from bibliopixel import colors
+
+
+class ColorTypesTest(TypesBaseTest):
+    def test_empty(self):
+        self.make('colors', [], [])
+
+    def test_single(self):
+        self.make('colors', ['red'], [(255, 0, 0)])
+
+    def test_many(self):
+        self.make('colors', ['red', [0, 255, 0], 127],
+                  [(255, 0, 0), (0, 255, 0), (127, 127, 127)])