git » libjio » commit 4f92a29

Add behaviour tests

author Alberto Bertogli
2009-03-09 06:02:56 UTC
committer Alberto Bertogli
2009-03-26 22:59:14 UTC
parent 0e023680595121dcf0c8140b1563ada22b4d237c

Add behaviour tests

This patch adds a suite of tests that check the library's behaviour. It's
written in Python, so it uses the Python bindings.

It has three different "test suites", one of them uses the fault injection
support incorporated in a previous patch, so it depends on libfiu being
installed and libjio having been built with FI=1. The other two test
suites can be run without libfiu.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>

.gitignore +2 -0
Makefile +1 -1
tests/README +3 -8
tests/auto/README +15 -0
tests/auto/runtests +19 -0
tests/auto/t_corruption.py +88 -0
tests/auto/t_fi.py +225 -0
tests/auto/t_normal.py +173 -0
tests/auto/tf.py +236 -0

diff --git a/.gitignore b/.gitignore
index f4e9015..457538f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ samples/full
 samples/jio1
 samples/jio2
 samples/jio3
+*.pyc
+*.pyo
diff --git a/Makefile b/Makefile
index b2bb56e..c541228 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ ifdef PROFILE
 ALL_CFLAGS += -g -pg -fprofile-arcs -ftest-coverage
 endif
 
-ifdef FIU
+ifdef FI
 ALL_CFLAGS += -DFIU_ENABLE=1
 LIBS += -lfiu
 endif
diff --git a/tests/README b/tests/README
index 9e5fd0a..8d77722 100644
--- a/tests/README
+++ b/tests/README
@@ -1,10 +1,5 @@
 
-Here you will find a small set of testing utilities for libjio that cover
-performance testing and recovery testings. They're really simple so there's
-not much documentation besides the code.
-
-Another really useful way of doing testing is using some of the well known
-filesystem benchmarking applications and modify them to use libjio. I regulary
-use dbench, fsx, tiobench and the tdb test suite, the patches are available on
-the website.
+Here you will find testing utilities for libjio that cover performance and
+behaviour testing. They can be found in the "performance" and "auto"
+directories, respectively.
 
diff --git a/tests/auto/README b/tests/auto/README
new file mode 100644
index 0000000..9652520
--- /dev/null
+++ b/tests/auto/README
@@ -0,0 +1,15 @@
+
+In this directory you'll find libjio's automated tests.
+
+They're split in three suites: normal, corruption and fi. The normal suite
+tests normal, expected behaviour. The corruption suite checks how the library
+behaves in presence of disk corruption. The fi suite uses libfiu to inject
+faults in order to simulate unexpected interruptions (like power failures) and
+checks how the library behaves in those cases.
+
+To run them, use "./runtests <suite name>". To run all tests, you can run
+"./runtests all".
+
+Note that the corruption and fi suite depends on libfiu being installed, and
+libjio having been built using FI=1.
+
diff --git a/tests/auto/runtests b/tests/auto/runtests
new file mode 100755
index 0000000..645a3bf
--- /dev/null
+++ b/tests/auto/runtests
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import sys
+import tf
+
+possible_tests = ('normal', 'corruption', 'fi')
+if len(sys.argv) < 2 or sys.argv[1] not in possible_tests + ('all',):
+	print 'Usage: runtests', '|'.join(possible_tests + ('all',))
+	sys.exit(1)
+
+if sys.argv[1] == 'all':
+	mnames = ('t_' + i for i in possible_tests)
+else:
+	mnames = ('t_' + sys.argv[1],)
+
+for mn in mnames:
+	print '--', mn
+	tf.autorun(__import__(mn))
+
diff --git a/tests/auto/t_corruption.py b/tests/auto/t_corruption.py
new file mode 100644
index 0000000..c91a132
--- /dev/null
+++ b/tests/auto/t_corruption.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+
+# Corruption tests using libfiu. libjio must have been built with libfiu
+# enabled (using something like make FIU=1) for them to work.
+
+from tf import *
+
+try:
+	import fiu
+except ImportError:
+	print
+	print "Error: unable to load fiu module. Corruption tests need"
+	print "libfiu support. Please install libfiu and recompile libjio"
+	print "with FIU=1. You can still run the other tests."
+	print
+
+
+def test_c01():
+	"checksum (1 bit change)"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_sync")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+	tc = open(transpath(n, 1)).read()
+	# flip just one bit of the first byte
+	tc = chr((ord(tc[0]) & 0xFE) | (~ ord(tc[0]) & 0x1) & 0xFF) + tc[1:]
+	open(transpath(n, 1), 'w').write(tc)
+	fsck_verify(n, corrupt = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_c02():
+	"truncate trans"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_sync")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+	tp = transpath(n, 1)
+	open(tp, 'r+').truncate(len(content(tp)) - 2)
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_c03():
+	"op len too big"
+	c = gencontent(10)
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_sync")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+
+	tf = TransFile(transpath(n, 1))
+	tf.ops[0].tlen = 99
+	tf.save()
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_c04():
+	"op len too small"
+	c = gencontent(100)
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_sync")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+
+	tf = TransFile(transpath(n, 1))
+	tf.ops[0].tlen = 10
+	tf.save()
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+
diff --git a/tests/auto/t_fi.py b/tests/auto/t_fi.py
new file mode 100644
index 0000000..e07e6f2
--- /dev/null
+++ b/tests/auto/t_fi.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python
+
+# General tests using libfiu. libjio must have been built with libfiu enabled
+# (using something like make FIU=1) for them to work.
+
+import struct
+from tf import *
+import libjio
+
+try:
+	import fiu
+except ImportError:
+	print
+	print "Error: unable to load fiu module. Fault injection tests need"
+	print "libfiu support. Please install libfiu and recompile libjio"
+	print "with FIU=1. You can still run the other tests."
+	print
+
+
+def test_f01():
+	"fail jio/get_tid/overflow"
+	c = gencontent()
+
+	def f1(f, jf):
+		jf.write(c)
+		fiu.enable("jio/get_tid/overflow")
+		try:
+			jf.write(c)
+		except IOError:
+			pass
+
+	n = run_with_tmp(f1)
+	assert content(n) == c
+	assert struct.unpack("I", content(jiodir(n) + '/lock'))[0] == 0
+	fsck_verify(n)
+	assert content(n) == c
+	cleanup(n)
+
+def test_f02():
+	"fail jio/commit/created_tf"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/created_tf")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_f03():
+	"fail jio/commit/tf_header"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_header")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_f04():
+	"fail jio/commit/tf_ophdr"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable_external("jio/commit/tf_ophdr",
+				gen_ret_after(1, 0, 1))
+		t = jf.new_trans()
+		t.add(c, 0)
+		t.add(c, len(c) + 200)
+		t.commit()
+
+	n = run_with_tmp(f1)
+
+	assert len(content(transpath(n, 1))) == DHS + DOHS + len(c) + DOHS
+	assert content(n) == ''
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_f05():
+	"fail jio/commit/tf_opdata"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable_external("jio/commit/tf_opdata",
+				gen_ret_after(1, 0, 1))
+		t = jf.new_trans()
+		t.add(c, 0)
+		t.add(c, len(c) + 200)
+		t.commit()
+
+	n = run_with_tmp(f1)
+
+	assert len(content(transpath(n, 1))) == DHS + (DOHS + len(c)) * 2
+	assert content(n) == ''
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_f06():
+	"fail jio/commit/tf_data"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_data")
+		t = jf.new_trans()
+		t.add(c, 0)
+		t.add(c, len(c) + 200)
+		t.commit()
+
+	n = run_with_tmp(f1)
+
+	assert len(content(transpath(n, 1))) == DHS + (DOHS + len(c)) * 2
+	assert content(n) == ''
+	fsck_verify(n, broken = 1)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_f07():
+	"fail jio/commit/tf_sync"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/tf_sync")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+	fsck_verify(n, reapplied = 1)
+	assert content(n) == c
+	cleanup(n)
+
+def test_f08():
+	"fail jio/commit/wrote_op"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/wrote_op")
+		t = jf.new_trans()
+		t.add(c, 0)
+		t.add(c, len(c) + 200)
+		t.commit()
+
+	n = run_with_tmp(f1)
+
+	assert content(n) == c
+	fsck_verify(n, reapplied = 1)
+	assert content(n) == c + '\0' * 200 + c
+	cleanup(n)
+
+def test_f09():
+	"fail jio/commit/wrote_all_ops"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/wrote_all_ops")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == c
+	fsck_verify(n, reapplied = 1)
+	assert content(n) == c
+	cleanup(n)
+
+def test_f10():
+	"fail jio/commit/pre_ok_free_tid"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/commit/pre_ok_free_tid")
+		jf.write(c)
+
+	n = run_with_tmp(f1)
+	assert content(n) == c
+	assert struct.unpack("I", content(jiodir(n) + '/lock'))[0] == 1
+	fsck_verify(n)
+	assert content(n) == c
+	assert not os.path.exists(jiodir(n))
+	cleanup(n)
+
+def test_f11():
+	"fail jio/commit/tf_sync in rollback"
+	c = gencontent()
+
+	def f1(f, jf):
+		jf.write('x' * (80 + len(c)))
+		t = jf.new_trans()
+		t.add(c, 80)
+		t.commit()
+		assert content(f.name) == 'x' * 80 + c
+		fiu.enable("jio/commit/tf_sync")
+		t.rollback()
+
+	n = run_with_tmp(f1)
+
+	assert content(n) == 'x' * 80 + c
+	fsck_verify(n, reapplied = 1)
+	assert content(n) == 'x' * (80 + len(c))
+	cleanup(n)
+
+def test_f12():
+	"fail jio/jsync/pre_unlink"
+	c = gencontent()
+
+	def f1(f, jf):
+		fiu.enable("jio/jsync/pre_unlink")
+		t = jf.new_trans()
+		t.add(c, 0)
+		t.commit()
+		jf.jsync()
+
+	n = run_with_tmp(f1, libjio.J_LINGER)
+
+	assert content(n) == c
+	fsck_verify(n, reapplied = 1)
+	assert content(n) == c
+	cleanup(n)
+
+
diff --git a/tests/auto/t_normal.py b/tests/auto/t_normal.py
new file mode 100644
index 0000000..520d3a4
--- /dev/null
+++ b/tests/auto/t_normal.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+
+# Normal tests.
+
+import libjio
+from tf import *
+
+
+def test_n01():
+	"open + close"
+	def f1(f, jf):
+		pass
+
+	n = run_with_tmp(f1)
+	assert content(n) == ''
+	fsck_verify(n)
+	assert content(n) == ''
+	cleanup(n)
+
+def test_n02():
+	"write + seek + read"
+	c = gencontent()
+
+	def f1(f, jf):
+		jf.write(c)
+		jf.lseek(0, 0)
+		assert jf.read(len(c) * 2) == c
+
+	n = run_with_tmp(f1)
+	assert content(n) == c
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n03():
+	"pwrite"
+	c = gencontent()
+
+	def f1(f, jf):
+		jf.pwrite(c, 80)
+
+	n = run_with_tmp(f1)
+	assert content(n) == '\0' * 80 + c
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n04():
+	"truncate"
+	def f1(f, jf):
+		jf.truncate(826)
+
+	n = run_with_tmp(f1)
+	assert content(n) == '\0' * 826
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n05():
+	"commit"
+	c = gencontent()
+
+	def f1(f, jf):
+		t = jf.new_trans()
+		t.add(c, 80)
+		t.commit()
+
+	n = run_with_tmp(f1)
+	assert content(n) == '\0' * 80 + c
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n06():
+	"empty, then rollback"
+	c = gencontent()
+
+	def f1(f, jf):
+		t = jf.new_trans()
+		t.add(c, 80)
+		t.commit()
+		t.rollback()
+
+	n = run_with_tmp(f1)
+
+	# XXX: This is weird, because the file was empty at the beginning.
+	# However, making it go back to 0 is delicate and the current code
+	# doesn't implement it. It probably should.
+	assert content(n) == '\0' * 80
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n07():
+	"extending, then rollback"
+	c1 = gencontent()
+	c2 = gencontent()
+
+	def f1(f, jf):
+		jf.write(c1)
+		t = jf.new_trans()
+		t.add(c2, len(c1) - 973)
+		t.commit()
+		t.rollback()
+
+	n = run_with_tmp(f1)
+
+	assert content(n) == c1
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n08():
+	"multiple overlapping ops"
+	c1 = gencontent(9345)
+	c2 = gencontent(len(c1))
+	c3 = gencontent(len(c1))
+	c4 = gencontent(len(c1))
+	c5 = gencontent(len(c1))
+
+	def f1(f, jf):
+		jf.write(c1)
+		t = jf.new_trans()
+		t.add(c2, len(c1) - 973)
+		t.add(c3, len(c1) - 1041)
+		t.add(c4, len(c1) - 666)
+		t.add(c5, len(c1) - 3000)
+		t.commit()
+
+	n = run_with_tmp(f1)
+	assert content(n) == c1[:-3000] + c5 + c4[- (- 666 + 3000):]
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n09():
+	"rollback multiple overlapping ops"
+	c1 = gencontent(9345)
+	c2 = gencontent(len(c1))
+	c3 = gencontent(len(c1))
+	c4 = gencontent(len(c1))
+	c5 = gencontent(len(c1))
+
+	def f1(f, jf):
+		jf.write(c1)
+		t = jf.new_trans()
+		t.add(c2, len(c1) - 973)
+		t.add(c3, len(c1) - 1041)
+		t.add(c4, len(c1) - 666)
+		t.add(c5, len(c1) - 3000)
+		t.commit()
+		t.rollback()
+
+	n = run_with_tmp(f1)
+
+	assert content(n) == c1
+	fsck_verify(n)
+	cleanup(n)
+
+def test_n10():
+	"lingering transactions"
+	c = gencontent()
+
+	def f1(f, jf):
+		t = jf.new_trans()
+		t.add(c, 0)
+		t.commit()
+		del t
+		assert content(f.name) == c
+		assert os.path.exists(transpath(f.name, 1))
+		jf.jsync()
+		assert not os.path.exists(transpath(f.name, 1))
+
+	n = run_with_tmp(f1, libjio.J_LINGER)
+
+	assert content(n) == c
+	fsck_verify(n)
+	cleanup(n)
+
+
diff --git a/tests/auto/tf.py b/tests/auto/tf.py
new file mode 100644
index 0000000..016fd1e
--- /dev/null
+++ b/tests/auto/tf.py
@@ -0,0 +1,236 @@
+
+"""
+Our customized testing framework.
+
+While not as sophisticated as the unittest module, it's targeted to our
+particular kind of tests.
+
+To that end, it has several simple but useful functions aimed to make tests
+more easier to read and write.
+"""
+
+import sys
+import os
+import time
+import random
+import struct
+import libjio
+
+
+# Useful constants, must match libjio.h
+DHS = 12	# disk header size
+DOHS = 16	# disk op header size
+
+
+def tmppath():
+	"""Returns a temporary path. We could use os.tmpnam() if it didn't
+	print a warning, or os.tmpfile() if it allowed us to get its name.
+	Since we just need a stupid name, we got our own function. Truly a
+	shame. Yes, it's not safe; I know and I don't care."""
+	tmpdir = os.environ.get('TMPDIR', '/tmp')
+	now = time.time()
+	now_s = str(int(now))
+	now_f = str((now - int(now)) * 10000)
+	now_str = "%s.%s" % (now_s[-5:], now_f[:now_f.find('.')])
+	return tmpdir + '/jiotest.%s.%s' % (now_str, os.getpid())
+
+
+def run_forked(f, *args, **kwargs):
+	"""Runs the function in a different process."""
+	pid = os.fork()
+	if pid == 0:
+		# child
+		f(*args, **kwargs)
+		sys.exit(0)
+	else:
+		# parent
+		id, status = os.waitpid(pid, 0)
+		if not os.WIFEXITED(status):
+			raise RuntimeError, (id, status)
+
+def forked(f):
+	"Decorator that makes the function run in a different process."
+	def newf(*args, **kwargs):
+		run_forked(f, *args, **kwargs)
+	return newf
+
+
+def gencontent(size = 9377):
+	"Generates random content."
+	s = ''
+	a = "%.20f" % random.random()
+	while len(s) < size:
+		s += a
+	s = s[:size]
+	return s
+
+def content(path):
+	"Returns the content of the given path."
+	f = open(path)
+	return f.read()
+
+
+def biopen(path, mode = 'w+', jflags = 0):
+	"Returns (open(path), libjio.open(path))."
+	if 'r' in mode:
+		flags = os.O_RDONLY
+		if '+' in mode:
+			flags = os.O_RDWR
+	elif 'w' in mode:
+		flags = os.O_RDWR
+		if '+' in mode:
+			flags = flags | os.O_CREAT | os.O_TRUNC
+	else:
+		raise RuntimeError
+
+	return open(path, mode), libjio.open(path, flags, 0400, jflags)
+
+def bitmp(mode = 'w+', jflags = 0):
+	"Opens a temporary file with biopen()."
+	path = tmppath()
+	return biopen(path, mode, jflags)
+
+
+def run_with_tmp(func, jflags = 0):
+	"""Runs the given function, that takes a file and a jfile as
+	parameters, using a temporary file. Returns the path of the temporary
+	file. The function runs in a new process that exits afterwards."""
+	f, jf = bitmp(jflags = jflags)
+	run_forked(func, f, jf)
+	return f.name
+
+
+def jiodir(path):
+	return os.path.dirname(path) + '/.' + os.path.basename(path) + '.jio'
+
+def transpath(path, ntrans):
+	jpath = jiodir(path)
+	return jpath + '/' + str(ntrans)
+
+def fsck(path):
+	"Calls libjio's jfsck()."
+	res = libjio.jfsck(path)
+	return res
+
+def fsck_verify(n, **kwargs):
+	"""Runs fsck(n), and verifies that the fsck result matches the given
+	values. The default is to match all elements except total to 0 (total
+	is calculated automatically from the sum of the others). Raises an
+	AssertionError if the given results were not the ones expected."""
+	expected = {
+		'invalid': 0,
+		'broken': 0,
+		'reapplied': 0,
+		'corrupt': 0,
+		'in_progress': 0,
+		'apply_error': 0,
+	}
+	expected.update(kwargs)
+	expected['total'] = sum(expected.values())
+	res = fsck(n)
+
+	for k in expected:
+		if k not in res:
+			raise AssertionError, k + ' not in res'
+		if res[k] != expected[k]:
+			raise AssertionError, k + ' does not match: ' + \
+					str(res)
+
+	libjio.jfsck_cleanup(n)
+
+def cleanup(path):
+	"""Unlinks the path and its temporary libjio directory. The libjio
+	directory must only have the 'lock' file in it."""
+	os.unlink(path)
+	jpath = jiodir(path)
+	if os.path.isdir(jpath):
+		assert 'lock' in os.listdir(jpath)
+		os.unlink(jpath + '/lock')
+		os.rmdir(jpath)
+
+
+class attrdict (dict):
+	def __getattr__(self, name):
+		return self[name]
+
+	def __setattr__(self, name, value):
+		self[name] = value
+
+	def __delattr__(self, name):
+		del self[name]
+
+class TransFile (object):
+	def __init__(self, path = ''):
+		self.id = -1
+		self.flags = 0
+		self.numops = -1
+		self.ops = []
+		self.path = path
+		if path:
+			self.load()
+
+	def load(self):
+		fd = open(self.path)
+
+		# header
+		hdrfmt = "III"
+		self.id, self.flags, self.numops = struct.unpack(hdrfmt,
+				fd.read(struct.calcsize(hdrfmt)))
+
+		# operations (header only)
+		opfmt = "IIQ"
+		self.ops = []
+		for i in range(self.numops):
+			tlen, plen, offset = struct.unpack(opfmt,
+					fd.read(struct.calcsize(opfmt)))
+			payload = fd.read(tlen)
+			assert len(payload) == tlen
+			self.ops.append(attrdict(tlen = tlen, plen = plen,
+				offset = offset, payload = payload))
+
+	def save(self):
+		# the lack of integrity checking in this function is
+		# intentional, so we can write broken transactions and see how
+		# jfsck() copes with them
+		fd = open(self.path, 'w')
+		fd.write(struct.pack("III", self.id, self.flags, self.numops))
+		for o in self.ops:
+			fd.write(struct.pack("IIQs", o.tlen, o.plen, o.offset,
+				o.payload))
+
+	def __repr__(self):
+		return '<TransFile %s: id:%d f:%s n:%d ops:%s>' % \
+			(self.path, self.id, hex(self.flags), self.numops,
+					self.ops)
+
+
+def gen_ret_after(n, notyet, itstime):
+	"""Returns a function that returns value of notyet the first n
+	invocations, and itstime afterwards."""
+	holder = [n]
+	def newf(*args, **kwargs):
+		holder[0] -= 1
+		if holder[0] >= 0:
+			return notyet
+		return itstime
+	return newf
+
+
+def autorun(module):
+	"Runs all the functions in the given module that begin with 'test'."
+	for name in sorted(dir(module)):
+		if not name.startswith('test'):
+			continue
+
+		obj = getattr(module, name)
+		if '__call__' in dir(obj):
+			name = name[len('test'):]
+			if name.startswith('_'):
+				name = name[1:]
+			desc = ''
+			if obj.__doc__:
+				desc = obj.__doc__
+			print "%-10s %-60.60s" % (name, desc)
+			obj()
+
+