author | Alberto Bertogli
<albertito@blitiri.com.ar> 2009-03-09 06:02:56 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2009-03-26 22:59:14 UTC |
parent | 0e023680595121dcf0c8140b1563ada22b4d237c |
.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() + +