Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xsetup.py36
-rw-r--r--tests/skip1
-rwxr-xr-xtests/viewer.py314
3 files changed, 340 insertions, 11 deletions
diff --git a/setup.py b/setup.py
index df9b92d..9362dc7 100755
--- a/setup.py
+++ b/setup.py
@@ -10,14 +10,19 @@ class TestCommand(Command):
user_options = [('coverage',
None,
'enable code coverage reporting'),
- ('prefix=',
+ ('prefix=',
'p',
- 'set sugar installation prefix, for dependency loading'),]
+ 'set sugar installation prefix, for dependency loading'),
+ ('single=',
+ 't',
+ 'run a single test'),
+ ]
description = 'runs tests from the test directory'
def initialize_options(self):
self._dir = os.getcwd()
self.coverage = False
+ self.single = False
self.prefix = None
def finalize_options(self):
@@ -33,6 +38,7 @@ class TestCommand(Command):
'python%d.%d'%sys.version_info[:2],
'site-packages')
sys.path.insert(1, prefix)
+ os.environ.setdefault('SUGAR_PREFIX', self.prefix)
if self.coverage:
import coverage
coverage.erase()
@@ -40,15 +46,23 @@ class TestCommand(Command):
loader = TestLoader()
suite = TestSuite()
- skipped = file('tests/skip', 'r').read().split('\n')
- for t in glob.glob(pjoin(self._dir, 'tests', '*.py')):
- if not t.endswith('__init__.py') and basename(t) not in skipped:
- modname = '.'.join(['tests', splitext(basename(t))[0]])
- mod = __import__(modname, {'sys':sys}, fromlist=[modname])
- print "loading %s" % modname
- suite.addTest(loader.loadTestsFromModule(mod))
- else:
- print "skipping %s" % t
+ if not self.single:
+ skipped = file('tests/skip', 'r').read().split('\n')
+ for t in glob.glob(pjoin(self._dir, 'tests', '*.py')):
+ if not t.endswith('__init__.py') and basename(t) not in skipped:
+ modname = '.'.join(['tests', splitext(basename(t))[0]])
+ mod = __import__(modname, {'sys':sys}, fromlist=[modname])
+ print "loading %s" % modname
+ suite.addTest(loader.loadTestsFromModule(mod))
+ else:
+ print "skipping %s" % t
+ else:
+ modname = '.'.join(['tests', self.single])
+ mod = __import__(modname, {'sys':sys}, fromlist=[modname])
+ print "loading %s" % modname
+ suite.addTest(loader.loadTestsFromModule(mod))
+
+
t = TextTestRunner(verbosity = 1)
t.run(suite)
diff --git a/tests/skip b/tests/skip
index 2061a09..882d14d 100644
--- a/tests/skip
+++ b/tests/skip
@@ -1,2 +1,3 @@
run-tests.py
overlaytests.py
+viewer.py
diff --git a/tests/viewer.py b/tests/viewer.py
new file mode 100755
index 0000000..6c564bf
--- /dev/null
+++ b/tests/viewer.py
@@ -0,0 +1,314 @@
+#!/usr/bin/env python
+import sys
+import os
+SUGAR_PREFIX = os.getenv('SUGAR_PREFIX')
+sys.path.append(SUGAR_PREFIX+'/lib/python2.6/site-packages')
+
+import gtk, gtk.gdk
+import cairo
+from math import pi as PI
+PI2 = PI/2
+
+import rsvg
+
+from sugar.bundle import activitybundle
+ACTIVITY_PATH = '%s/share/sugar/activities/'%SUGAR_PREFIX
+#ACTIVITY_PATH = os.getenv('SUGAR_BUNDLE_PATH')
+
+# FIXME ideally, apps scale correctly and we should use proportional positions
+X_WIDTH = 800
+X_HEIGHT = 600
+ACTION_WIDTH = 100
+ACTION_HEIGHT = 70
+
+# block look
+BLOCK_PADDING = 5
+BLOCK_WIDTH = 100
+BLOCK_CORNERS = 10
+BLOCK_INNER_PAD = 10
+
+SNAP_WIDTH = BLOCK_WIDTH - BLOCK_PADDING - BLOCK_INNER_PAD*2
+SNAP_HEIGHT = SNAP_WIDTH*X_HEIGHT/X_WIDTH
+SNAP_SCALE = float(SNAP_WIDTH)/X_WIDTH
+
+# load data structure
+states = dict(
+ INIT=dict(
+ next='state1',
+ app='Browse',
+ actions=[dict(name='message', pos=[20,5])],
+ events=[{'name':'click', 'pos':(200,200)}],
+ ),
+ state1=dict(
+ next='state2',
+ app='Browse',
+ actions=[dict(name='mock')],
+ events=[{'name':'click', 'pos':(200,100)},{'pos':(200,200), 'name':'keyboard'}],
+ ),
+ state2=dict(
+ next='state3',
+ app='Write',
+ actions=[dict(name='flash', pos=[2,5]), dict(name='text', pos=[400,500])],
+ events=[{'name':'keyboard'}],
+ ),
+ state3=dict(
+ next='state4',
+ app='Write',
+ actions=[dict(name='message', pos=[2,5]), dict(name='hilight', pos=[600,100]), dict(name='message', pos=[300,500])],
+ events=[{'name':'click'}],
+ ),
+ state4=dict(
+ app='Browse',
+ next=None,
+ actions=[dict(name='message', pos=[200,5])],
+ events=[{'name':'scratch&smell'}],
+ ),
+)
+
+def _paint_state(ctx, states):
+ """
+ Paints a tutorius fsm state in a cairo context.
+ Final context state will be shifted by the size of the graphics.
+ """
+ block_width = BLOCK_WIDTH - BLOCK_PADDING
+ block_max_height = alloc.height
+
+ cur_state = 'INIT'
+ cur_app = states[cur_state]['app']
+ app_start = ctx.get_matrix()
+ while cur_state:
+ state = states[cur_state]
+ new_app = state['app']
+ if new_app != cur_app:
+ ctx.save()
+ ctx.set_matrix(app_start)
+ _render_app_hints(ctx, cur_app)
+ ctx.restore()
+ app_start = ctx.get_matrix()
+ ctx.translate(BLOCK_PADDING, 0)
+ cur_app = new_app
+
+ local_height = (block_max_height - BLOCK_PADDING)/len(state['actions']) - BLOCK_PADDING
+ ctx.save()
+ for action in state['actions']:
+ origin = tuple(ctx.get_matrix())[-2:]
+ if click_pos and \
+ click_pos[0]-BLOCK_WIDTH<origin[0] and \
+ drag_pos[0]>origin[0]:
+ selection.append(action)
+ render_action(ctx, block_width, local_height, action)
+ ctx.translate(0, local_height+BLOCK_PADDING)
+
+ ctx.restore()
+ ctx.translate(BLOCK_WIDTH, 0)
+ local_height = (block_max_height - BLOCK_PADDING)/len(state['events']) - BLOCK_PADDING
+ ctx.save()
+ for event in state['events']:
+ origin = tuple(ctx.get_matrix())[-2:]
+ if click_pos and \
+ click_pos[0]-BLOCK_WIDTH<origin[0] and \
+ drag_pos[0]>origin[0]:
+ selection.append(event)
+ render_event(ctx, block_width, local_height, event)
+ ctx.translate(0, local_height+BLOCK_PADDING)
+
+ ctx.restore()
+ ctx.translate(BLOCK_WIDTH, 0)
+
+ # FIXME use special notation for happy path, not chaining
+ cur_state = state['next']
+ yield True
+
+ ctx.set_matrix(app_start)
+ _render_app_hints(ctx, cur_app)
+
+ yield False
+
+alloc = None
+click_pos = None
+drag_pos = None
+selection = []
+
+def _render_snapshot(ctx, elem):
+ ctx.set_source_rgba(1.0, 1.0, 1.0, 0.5)
+ ctx.rectangle(0, 0, SNAP_WIDTH, SNAP_HEIGHT)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ pos = elem.get('pos')
+ if pos:
+ # FIXME this size approximation is fine, but I believe we could
+ # do better.
+ ctx.scale(SNAP_SCALE, SNAP_SCALE)
+ ctx.rectangle(pos[0], pos[1], ACTION_WIDTH, ACTION_HEIGHT)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+def _render_app_hints(ctx, appname):
+ ctx.set_source_rgb(0.0, 0.0, 0.0)
+ ctx.set_dash((1,1,0,0), 1)
+ ctx.move_to(0, 0)
+ ctx.line_to(0, alloc.height)
+ ctx.stroke()
+ ctx.set_dash(tuple(), 1)
+
+ icon_path = activitybundle.ActivityBundle(
+ os.path.join(ACTIVITY_PATH, appname+'.activity')).get_icon()
+ icon = rsvg.Handle(icon_path)
+ ctx.save()
+ ctx.translate(-15, 0)
+ ctx.scale(0.5, 0.5)
+ icon_surf = icon.render_cairo(ctx)
+ ctx.restore()
+
+
+def render_action(ctx, width, height, action):
+ ctx.save()
+ inner_width = width-(BLOCK_CORNERS<<1)
+ inner_height = height-(BLOCK_CORNERS<<1)
+
+ paint_border = ctx.rel_line_to
+ filling = cairo.LinearGradient(0, 0, 0, inner_height)
+ if action not in selection:
+ filling.add_color_stop_rgb(0.0, 0.7, 0.7, 1.0)
+ filling.add_color_stop_rgb(1.0, 0.1, 0.1, 0.8)
+ else:
+ filling.add_color_stop_rgb(0.0, 0.4, 0.4, 0.8)
+ filling.add_color_stop_rgb(1.0, 0.0, 0.0, 0.5)
+ tracing = cairo.LinearGradient(0, 0, 0, inner_height)
+ tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0)
+ tracing.add_color_stop_rgb(1.0, 0.2, 0.2, 0.2)
+
+ ctx.move_to(BLOCK_CORNERS, 0)
+ paint_border(inner_width, 0)
+ ctx.arc(inner_width+BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI2, 0.0)
+ ctx.arc(inner_width+BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, 0.0, PI2)
+ ctx.arc(BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, PI2, PI)
+ ctx.arc(BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI, -PI2)
+
+ ctx.set_source(tracing)
+ ctx.stroke_preserve()
+ ctx.set_source(filling)
+ ctx.fill()
+
+ ctx.save()
+ ctx.set_source_rgb(0, 0, 0)
+ ctx.move_to(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
+ ctx.show_text(action['name'])
+ ctx.stroke()
+ ctx.restore()
+
+ ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2)
+ _render_snapshot(ctx, action)
+
+ ctx.restore()
+
+def render_event(ctx, width, height, event):
+ ctx.save()
+ inner_width = width-(BLOCK_CORNERS<<1)
+ inner_height = height-(BLOCK_CORNERS<<1)
+
+ filling = cairo.LinearGradient(0, 0, 0, inner_height)
+ if event not in selection:
+ filling.add_color_stop_rgb(0.0, 1.0, 0.8, 0.6)
+ filling.add_color_stop_rgb(1.0, 1.0, 0.6, 0.2)
+ else:
+ filling.add_color_stop_rgb(0.0, 0.8, 0.6, 0.4)
+ filling.add_color_stop_rgb(1.0, 0.6, 0.4, 0.1)
+ tracing = cairo.LinearGradient(0, 0, 0, inner_height)
+ tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0)
+ tracing.add_color_stop_rgb(1.0, 0.3, 0.3, 0.3)
+
+ ctx.move_to(BLOCK_CORNERS, 0)
+ ctx.rel_line_to(inner_width, 0)
+ ctx.rel_line_to(BLOCK_CORNERS, BLOCK_CORNERS)
+ ctx.rel_line_to(0, inner_height)
+ ctx.rel_line_to(-BLOCK_CORNERS, BLOCK_CORNERS)
+ ctx.rel_line_to(-inner_width, 0)
+ ctx.rel_line_to(-BLOCK_CORNERS, -BLOCK_CORNERS)
+ ctx.rel_line_to(0, -inner_height)
+ ctx.close_path()
+
+ ctx.set_source(tracing)
+ ctx.stroke_preserve()
+ ctx.set_source(filling)
+ ctx.fill()
+
+ ctx.set_source_rgb(0, 0, 0)
+ ctx.move_to(BLOCK_INNER_PAD, BLOCK_INNER_PAD)
+ ctx.show_text(event['name'])
+ ctx.stroke()
+
+ ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2)
+ _render_snapshot(ctx, event)
+
+ ctx.restore()
+
+def on_viewer_expose(widget, evt):
+ ctx = widget.window.cairo_create()
+ global alloc
+ alloc = widget.get_allocation()
+ ctx.set_source_pixmap(widget.window,
+ widget.allocation.x,
+ widget.allocation.y)
+
+ #draw no more than our expose event intersects our child
+ region = gtk.gdk.region_rectangle(widget.allocation)
+ r = gtk.gdk.region_rectangle(evt.area)
+ region.intersect(r)
+ ctx.region (region)
+ ctx.clip()
+ ctx.paint()
+
+ ctx.translate(BLOCK_PADDING, BLOCK_PADDING)
+
+ painter = _paint_state(ctx, states)
+ while painter.next(): pass
+
+ if click_pos and drag_pos:
+ ctx.set_matrix(cairo.Matrix())
+ ctx.rectangle(click_pos[0], click_pos[1], \
+ drag_pos[0]-click_pos[0], drag_pos[1]-click_pos[1])
+ ctx.set_source_rgba(0, 0, 1, 0.5)
+ ctx.fill_preserve()
+ ctx.stroke()
+
+ return False
+
+def _on_click(widget, evt):
+ # the rendering pipeline will work out the click validation process
+ global selection, click_pos, drag_pos
+ drag_pos = None
+ drag_pos = click_pos = evt.get_coords()
+ widget.queue_draw()
+
+ widget.drag_source_set(gtk.gdk.BUTTON1_MASK, (('view', gtk.TARGET_SAME_APP, 42),), gtk.gdk.ACTION_DEFAULT)
+ widget.drag_source_set_icon_pixbuf(gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 100, 200).get_from_drawable(widget.window, widget.window.get_colormap(), int(click_pos[0]), 0, 0,0, 100,200))
+
+ selection = []
+
+def _on_drag(widget, evt):
+ global selection, click_pos, drag_pos
+ drag_pos = evt.get_coords()
+ widget.queue_draw()
+
+
+#if __name__=='__main__':
+if True:
+ win = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ win.set_size_request(1000, 200)
+ win.connect("destroy", gtk.mainquit)
+
+ canvas = gtk.DrawingArea()
+ win.add(canvas)
+ canvas.set_app_paintable(True)
+ canvas.connect_after("expose-event", on_viewer_expose)
+ canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_MOTION_MASK)
+ canvas.connect('button-press-event', _on_click)
+ canvas.connect('motion-notify-event', _on_drag)
+
+ win.show_all()
+ gtk.main()
+
+
+