Instantly share code, notes, and snippets.
Created
May 14, 2010 18:00
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save asomers/401433 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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