Skip to content

Instantly share code, notes, and snippets.

@asomers
Created May 14, 2010 18:00
Show Gist options
  • Save asomers/401433 to your computer and use it in GitHub Desktop.
Save asomers/401433 to your computer and use it in GitHub Desktop.
#! /usr/bin/python
import time
import random
import sys
import StringIO
import operator
import optparse
import os.path
import pdb
import gtk
import gobject
# Borrowed from Kiwi, as described at
# http://unpythonic.blogspot.com/2007/03/unit-testing-pygtk.html
def refresh_gui(delay=0):
while gtk.events_pending():
gtk.main_iteration_do(block=False)
time.sleep(delay)
RANDOM_TEXT_SEQ = ''.join([chr(i) for i in [9, 10] + range(32, 127)])
"""All printable 7-bit ASCII characters"""
def random_text(numeric=False, low=0, high=10):
"""Generates random text string.
If numeric==true, then 50% of returns will be numeric"""
if numeric and random.randint(0, 1):
#return numeric result, 50/50 integer or float
span = high - low
if random.randint(0, 1):
return str(random.randint(int(low - span / 2), int(high + span/2)))
else:
return str(random.uniform(low - span / 2, high + span/2))
else:
#return text result
n = int(random.expovariate(1. / 4))
return ''.join([random.choice(RANDOM_TEXT_SEQ) for i in range(n)])
def activator(d, type_):
def decorator_(f):
if not type_ in d:
d.append((type_, f))
return f
return decorator_
def class_compare(x, y):
if issubclass(y, x): return 1
elif issubclass(x, y): return -1
else: return 0
class ActionItem(object):
"""Non-widget action that should be randomly activated by Fuzz"""
def __init__(self, func):
"""func is the function that Fuzz should call"""
self._func = func
def __call__(self, *args):
self._func(args)
class Fuzz(object):
"""Fuzz test a gui application"""
_activators = []
def __init__(self ):
self._action_items = []
self._blacklist = []
#Sort _activators according to inheritance
self._activators.sort(cmp=lambda x, y: class_compare(x[0], y[0]))
def activate(self, widget, *args):
tag = gobject.idle_add(self.idle_callback, priority=gobject.PRIORITY_LOW)
if type(widget) == ActionItem:
retval = widget()
for a in self._activators:
if isinstance(widget, a[0]):
retval = a[1](self, widget, *args)
break
gobject.source_remove(tag)
return retval
def add_actionable_item(self, action_item):
self._action_items.append(action_item)
def blacklist_widgets(self, *args):
"""Never activate these widgets"""
self._blacklist += args
def print_widget(self, widget):
try:
print widget.name, widget,
except AttributeError:
try:
print widget.get_title(), widget,
except AttributeError:
try:
print widget.get_image().get_stock(), widget,
except AttributeError:
print widget,
def idle_callback(self):
"""Deals with popups that invoke gtk.main_run().
Register this before activating a widget, and deregister afterwards. Do
not register before calling refresh_gui. That way this will only be
called when the program is blocking in gtk.main_run()"""
widget = gtk.grab_get_current()
if widget:
self.fuzz([widget])
return True
else:
return False
def is_actionable(self, widget):
add_widget = True
try:
if not widget.get_property('visible'):
add_widget = False
except TypeError:
pass
try:
if not widget.get_property('sensitive'):
add_widget = False
except TypeError:
pass
try:
if not widget.get_property('editable'):
add_widget = False
except TypeError:
pass
if not reduce(operator.or_,
[isinstance(widget, i[0]) for i in self._activators],
False):
add_widget = False
return add_widget
def fuzz(self, top_levels=None):
self.actionable_widgets = []
if top_levels == None:
top_levels = gtk.window_list_toplevels()
for t in top_levels:
self.walk(t)
self.actionable_widgets += self._action_items
#Now act on a widget
i = random.randint(0, len(self.actionable_widgets) - 1)
widget = self.actionable_widgets[i]
self.print_widget(widget)
bak_stderr = sys.stderr
sys.stderr = StringIO.StringIO()
print self.activate(widget)
#synchronously run the gui
refresh_gui(.01) #Notebooks need a little delay
if sys.stderr.getvalue() != '':
print sys.stderr.getvalue()
pdb.set_trace()
sys.stderr = bak_stderr
def walk(self, widget):
"""Recurse over a widget's children, then return. If this widget is
actionable, add it to the list"""
if widget in self._blacklist:
return
try:
if isinstance(widget, gtk.Notebook):
children = (widget.get_children()[widget.get_current_page()],)
elif isinstance(widget, gtk.MenuItem):
children = (widget.get_submenu(),)
elif isinstance(widget, gtk.TreeView):
children = widget.get_columns()
elif isinstance(widget, gtk.TreeViewColumn):
children = widget.get_cell_renderers()
elif isinstance(widget, gtk.ScrolledWindow):
children = (widget.get_hscrollbar(), widget.get_vscrollbar())
else:
children = widget.get_children()
if children:
for c in children:
self.walk(c)
except AttributeError:
pass
if self.is_actionable(widget):
self.actionable_widgets.append(widget)
@activator(_activators, gtk.Button)
def on_Button(self, widget):
widget.clicked()
return 'clicked'
@activator(_activators, gtk.CellRendererText)
def on_CellRendererText(self, widget):
text = random_text(True, -999, 999)
widget.emit('edited', str(random.randint(0, 1)), text)
return text
@activator(_activators, gtk.ComboBox)
def on_ComboBox(self, widget):
n = random.randint(0, len(widget.get_model()) - 1)
widget.set_active(n)
return n
@activator(_activators, gtk.Entry)
def on_Entry(self, widget):
#Normally Entries would be non-numeric, but all of mrfilter's entries
#are numeric
text = random_text(True, -999, 999)
widget.set_text(text)
widget.activate()
return text
@activator(_activators, gtk.MenuItem)
def on_MenuItem(self, widget):
widget.activate()
return "activated"
@activator(_activators, gtk.Notebook)
def on_Notebook(self, widget):
new = random.randint(0, widget.get_n_pages() - 1)
widget.set_current_page(new)
return new
@activator(_activators, gtk.Range)
def on_Range(self, widget):
adj = widget.get_adjustment()
lower, upper = adj.lower, adj.upper
widget.set_value(random.uniform(lower, upper))
@activator(_activators, gtk.SpinButton)
def on_SpinButton(self, widget):
use_entry = random.randint(0, 1)
if use_entry:
#use the text entry
if widget.get_numeric():
adj = widget.get_adjustment()
text = random_text(True, adj.lower, adj.upper)
else:
text = random_text()
widget.set_text(text)
widget.activate()
return text
else:
#use the spinner
up = random.randint(0, 1)
if up:
widget.spin(gtk.SPIN_STEP_FORWARD)
return "up"
else:
widget.spin(gtk.SPIN_STEP_BACKWARD)
return "down"
@activator(_activators, gtk.TreeView)
def on_TreeView(self, widget):
"""Select a row of the treeview"""
lim = len(widget.get_model())
if lim == 0:
return None
n = random.randint(0, lim - 1)
widget.get_selection().select_path(n)
return n
@activator(_activators, gtk.TreeViewColumn)
def on_TreeViewColumn(self, widget):
widget.clicked()
return 'clicked'
@activator(_activators, gtk.DrawingArea)
def on_DrawingArea(self, widget):
rect = widget.get_allocation()
x = random.randint(0, rect.width)
y = random.randint(0, rect.height)
event = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
event.window = widget.get_parent_window()
event.send_event = True
event.x = float(x)
event.y = float(y)
event.x_root = float(x + rect.x) #XXX Not sure of the correctness of this
event.y_root = float(y + rect.y) #XXX Not sure of the correctness of this
event.button = 1 #TODO: support right clicks too
widget.emit('button-press-event', event)
@activator(_activators, gtk.ToggleButton)
def on_ToggleButton(self, widget):
widget.toggled()
def fuzz_main():
fuzz = Fuzz(gtk.window_list_toplevels())
do_fuzz(fuzz)
def do_fuzz(fuzz):
for i in range(1000):
try:
fuzz.fuzz()
except Exception as E:
sys.stderr = sys.__stderr__
raise E
gtk.main = fuzz_main
def fimport(filename):
"""Import a module by its full filename"""
module_ = os.path.splitext(filename)[0]
sys.path.insert(0, os.path.dirname(module_))
mod = __import__(os.path.basename(module_), level=0)
return mod
def main(argv):
usage = "%prog [OPTIONS] file [FILE_OPTIONS]"
parser = optparse.OptionParser(usage)
parser.add_option('-s', '--steps', help="run fuzzer for STEP iterations",
type='int', default=100)
(options, args) = parser.parse_args(argv[1:])
mod = fimport(args[0])
mod.main(args[1:])
if __name__ == '__main__':
sys.exit(main(sys.argv))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment