diff --git a/.gitignore b/.gitignore index 6e7ceb83..72b81d55 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ conf/booth*.service docs/*.8 docs/*.8.html script/service-runnable -script/unit-test.py src/b_config.h.in src/b_config.h src/booth_config.h diff --git a/Makefile.am b/Makefile.am index acd4c1e2..b6466580 100644 --- a/Makefile.am +++ b/Makefile.am @@ -35,12 +35,11 @@ TARFILE = $(PACKAGE_NAME)-$(VERSION).tar.gz EXTRA_DIST = autogen.sh conf/booth.conf.example \ script/booth-keygen script/lsb script/ocf script/service-runnable.in \ - script/unit-test.py.in script/wireshark-dissector.lua \ + script/wireshark-dissector.lua \ test/arbtests.py test/assertions.py test/booth_path test/boothrunner.py \ test/boothtestenv.py.in test/clientenv.py test/clienttests.py test/live_test.sh \ test/runtests.py.in test/serverenv.py test/servertests.py test/sitetests.py \ test/utils.py \ - unit-tests \ contrib \ icons \ $(SPEC).in booth-rpmlintrc \ diff --git a/README-testing b/README-testing index c5aadfee..8a97da29 100644 --- a/README-testing +++ b/README-testing @@ -141,91 +141,4 @@ BOOTH_RUNTESTS_SINGLE_INSTANCE environment variable to make tests use only single port (9929), but parallel instances will fail. - -=== Unit tests - -These use gdb and pexpect to set boothd state to some configured value, -injecting some input and looking at the output. - - # python script/unit-test.py src/boothd unit-tests/ - -Or, if using the 'booth-test' RPM, - - # python unit-test.py src/boothd unit-tests/ - -This must (currently?) be run as a non-root user; another optional argument is -the test to start from, eg. '003'. - - -Basically, boothd is started with the config file `unit-tests/booth.conf`, and -gdb gets attached to it. - -Then, some ticket state is set, incoming messages are delivered, and outgoing -messages and the state is compared to expected values. - -`unit-tests/_defaults.txt` has default values for the initial state and -message data. - - -Each test file consists of headers and key/value pairs: - --------------------- -ticket: - state ST_STABLE - -message0: # optional comment for the log file - header.cmd OP_ACCEPTING - ticket.id "asdga" - -outgoing0: - header.cmd OP_PREPARING - last_ack_ballot 42 - -finally: - new_ballot 1234 --------------------- - - -A few details to the the above example: - -* Ticket states in RAM (`ticket`, `finally`) are written in host-endianness. - -* Message data (`messageN`, `outgoingN`) are automatically converted via `htonl` resp. `ntohl`. They are delivered/checked in the order defined by the integer `N` component. - -* Strings are done via `strcpy()` - -* `ticket` and `messageN` are assignment chunks - -* `finally` and `outgoingN` are compare chunks - -* In `outgoingN` you can check _both_ message data (keys with a `.` in them) and ticket state - -* Symbolic names are useable, GDB translates them for us - -* The test scripts in `unit-tests/` need to be named with 3 digits, an underscore, some text, and `.txt` - -* The "fake" `crm_ticket` script gets the current test via `UNIT_TEST`; test scripts can pass additional information via `UNIT_TEST_AUX`. - - - -==== Tips and Hints - -There's another special header: `gdb__N__`. These lines are sent to GDB after -injecting a message, but before waiting for an outgoing line. Values that -contain `§` are sent as multiple lines to GDB. - -This means that a stanza like - --------------------- -gdb0: - watch booth_conf->ticket[0].owner § commands § bt § c § end --------------------- - -will cause a watchpoint to be set, and when it is triggered a backtrace (`bt`) -is written to the log file. - -This makes it easy to ask for additional data or check for a call-chain when -hitting bugs that can be reproduced via such a unit-test. - - # vim: set ft=asciidoc : diff --git a/booth.spec.in b/booth.spec.in index 03192454..f72e4317 100644 --- a/booth.spec.in +++ b/booth.spec.in @@ -160,7 +160,6 @@ ln -s ../../%{_initddir}/booth-arbitrator %{buildroot}%{_sbindir}/rcbooth-arbitr #install test-parts mkdir -p %{buildroot}/%{test_path}/conf -cp -a unit-tests/ script/unit-test.py test %{buildroot}/%{test_path}/ cp -a conf/booth.conf.example %{buildroot}/%{test_path}/conf/ chmod +x %{buildroot}/%{test_path}/test/booth_path chmod +x %{buildroot}/%{test_path}/test/live_test.sh diff --git a/configure.ac b/configure.ac index c2ea2d76..eb1d1e19 100644 --- a/configure.ac +++ b/configure.ac @@ -161,7 +161,6 @@ AC_CONFIG_FILES([Makefile docs/Makefile conf/Makefile]) AC_CONFIG_FILES([conf/booth-arbitrator.service conf/booth@.service]) -AC_CONFIG_FILES([script/unit-test.py]) AC_CONFIG_FILES([script/service-runnable], [chmod +x script/service-runnable]) # =============================================== diff --git a/script/unit-test.py.in b/script/unit-test.py.in deleted file mode 100644 index ea071c78..00000000 --- a/script/unit-test.py.in +++ /dev/null @@ -1,650 +0,0 @@ -#!@PYTHON_SHEBANG@ -# vim: fileencoding=utf-8 -# see http://stackoverflow.com/questions/728891/correct-way-to-define-python-source-code-encoding -# NOTE: setting the encoding is needed as non-ASCII characters are contained - -import os, sys, time, signal, tempfile, socket, posix, time -import re, shutil, pexpect, logging, pprint -import random, copy, glob, traceback - - -# Don't make that much sense - function/line is write(). -# Would have to use traceback.extract_stack() manually. -# %(funcName)10.10s:%(lineno)3d %(levelname)8s -# The second ":" is to get correct syntax highlightning, -# eg. messages with ERROR etc. are red in vim. -default_log_format = '%(asctime)s: : %(message)s' -default_log_datefmt = '%b %d %H:%M:%S' - - -# Compatibility with dictionary methods not present in Python 3; -# https://www.python.org/dev/peps/pep-0469/#migrating-to-the-common-subset-of-python-2-and-3 -try: - dict.iteritems -except AttributeError: # Python 3 - iter_items = lambda d: iter(d.items()) -else: # Python 2 - iter_items = lambda d: d.iteritems() - - -# {{{ pexpect-logging glue -# needed for use as pexpect.logfile, to relay into existing logfiles -class expect_logging(): - prefix = "" - test = None - - def __init__(self, pre, inst): - self.prefix = pre - self.test = inst - - def flush(self, *arg): - pass - - def write(self, stg): - if self.test.dont_log_expect == 0: - # TODO: split by input/output, give program - if sys.version_info[0] >= 3: - stg = str(stg, 'UTF-8') - for line in re.split(r"[\r\n]+", stg): - if line == self.test.prompt: - continue - if line == "": - continue - logging.debug(" " + self.prefix + " " + line) -# }}} - - -# {{{ dictionary plus second hash -class dict_plus(dict): - def __init__(self): - self.aux = dict() - -# def aux(self): -# return self.aux -# }}} - - -class UT(): -# {{{ Members - binary = None - test_base = None - lockfile = None - - defaults = None - - this_port = None - this_site = "127.0.0.1" - this_site_id = None - running = False - - gdb = None - booth = None - prompt = "CUSTOM-GDB-PROMPT-%d-%d" % (os.getpid(), time.time()) - - dont_log_expect = 0 - current_nr = None - - udp_sock = None - - # http://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output - BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) -# }}} - - -# {{{ setup functions - @classmethod - def _filename(cls, desc): - return "/tmp/booth-unittest.%d.%s" % (os.getpid(), desc) - - - def __init__(self, bin, dir): - self.binary = os.path.realpath(bin) - self.test_base = os.path.realpath(dir) + "/" - self.defaults = self.read_test_input(self.test_base + "_defaults.txt", state="ticket") - self.lockfile = UT._filename("lock") - self.udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - - def read_test_input(self, file, state=None, m = dict()): - fo = open(file, "r") - state = None - line_nr = 0 - for line in fo.readlines(): - line_nr += 1 - - # comment? - if re.match(r"^\s*#", line): - continue - # empty line - if re.match(r"^\s*$", line): - continue - - # message resp. ticket - # We allow a comment to have something to write out to screen - res = re.match(r"^\s*(\w+)\s*:(?:\s*(#.*?\S))?\s*$", line) - if res: - state = res.group(1) - if state not in m: - m[state] = dict_plus() - if res.group(2): - m[state].aux["comment"] = res.group(2) - m[state].aux["line"] = line_nr - continue - - assert(state) - - res = re.match(r"^\s*(\S+)\s*(.*)\s*$", line) - if res: - m[state][ res.group(1) ] = res.group(2) - return m - - - def setup_log(self, **args): - global default_log_format - global default_log_datefmt - - this_test_log = logging.FileHandler( mode = "w", **args ) - this_test_log.setFormatter( - logging.Formatter(fmt = default_log_format, - datefmt = default_log_datefmt) ) - - this_test_log.emit( - logging.makeLogRecord( { - "msg": "## vim: set ft=messages : ##", - "lineno": 0, - "levelname": "None", - "level": None,} ) ) - - # in the specific files we want ALL information - this_test_log.setLevel(logging.DEBUG) - - logging.getLogger('').addHandler(this_test_log) - return this_test_log - - - def running_on_console(self): - return sys.stdout.isatty() - - - def colored_string(self, stg, color): - if self.running_on_console(): - return "\033[%dm%s\033[0m" % (30+color, stg) - return stg - - - # We want shorthand in descriptions, ie. "state" - # instead of "booth_conf->ticket[0].state". - def translate_shorthand(self, name, context): - if context == 'ticket': - return "booth_conf->ticket[0]." + name - if context == 'message': - return "msg->" + name - if context == 'inject': - return "ntohl(((struct boothc_ticket_msg *)buf)->" + name + ")" - assert(False) - - - def stop_processes(self): - if os.access(self.lockfile, os.F_OK): - os.unlink(self.lockfile) - # In case the boothd process is already dead, isalive() would still return True - # (because GDB still has it), but terminate() does fail. - # So we just quit GDB, and that might take the boothd with it - - # if not, we terminate it ourselves. - if self.gdb: - self.gdb.close( force=True ); - self.drain_booth_log() - if self.booth: - self.booth.close( force=self.booth.isalive() ) - - - def start_a_process(self, bin, env_add=(), **args): - name = re.sub(r".*/", "", bin) - # How to get stderr, too? - expct = pexpect.spawn(bin, - env=dict(os.environ, **dict({ - 'PATH': ':'.join((self.test_base + "/bin/", - os.getenv('PATH'))), - 'UNIT_TEST_PATH': self.test_base, - 'LC_ALL': 'C', - 'LANG': 'C'}, **dict(env_add))), - timeout=30, - maxread=32768, - **args) - expct.setecho(False) - expct.logfile_read = expect_logging("<- %s" % name, self) - expct.logfile_send = expect_logging(" -> %s" % name, self) - return expct - - - def start_processes(self, test): - self.booth = self.start_a_process(self.binary, - args = ["daemon", "-DS", - "-c", self.test_base + "/booth.conf", - "-s", "127.0.0.1", - "-l", self.lockfile], - env_add=( ('UNIT_TEST', test), - ('UNIT_TEST_FILE', os.path.realpath(test)), - # provide some space, so that strcpy(getenv()) works - ('UNIT_TEST_AUX', "".zfill(1024)), - )); - - logging.info("started booth with PID %d, lockfile %s" % (self.booth.pid, self.lockfile)) - self.booth.expect("BOOTH site \S+ \(build \S+\) daemon is starting", timeout=2) - #print self.booth.before; exit - - self.gdb = self.start_a_process("gdb", - args=["-quiet", - "-p", str(self.booth.pid), - # Don't use .gdbinit - "-nx", "-nh", - # Run until the defined point. - # This is necessary so that ticket state setting doesn't - # happen _before_ the call to pcmk_load_ticket() - # (which would overwrite our data) - "-ex", "break ticket_cron", - "-ex", "continue"]) - logging.info("started GDB with PID %d" % self.gdb.pid) - self.gdb.expect("(gdb)") - self.gdb.sendline("set pagination off\n") - self.gdb.sendline("set interactive-mode off\n") - self.gdb.sendline("set verbose off\n") ## sadly to late for the initial "symbol not found" messages - self.gdb.sendline("set prompt " + self.prompt + "\\n\n"); - self.sync(2000) - - # Only stop for this recipient, so that broadcasts are not seen multiple times - self.send_cmd("break booth_udp_send if to == &(booth_conf->site[1])") - self.send_cmd("break recvfrom") - # ticket_cron is still a breakpoint - - # Now we're set up. - self.this_site_id = self.query_value("local->site_id") - self.this_port = int(self.query_value("booth_conf->port")) - - # do a self-test - assert(self.check_value("local->site_id", self.this_site_id)) - - self.running = False -# }}} - - -# {{{ GDB communication - def sync(self, timeout=-1): - self.gdb.expect(self.prompt, timeout) - - answer = self.gdb.before - - self.dont_log_expect += 1 - # be careful not to use RE characters like +*.[] etc. - r = str(random.randint(2**19, 2**20)) - self.gdb.sendline("print " + r) - self.gdb.expect(r, timeout) - self.gdb.expect(self.prompt, timeout) - self.dont_log_expect -= 1 - return answer # send a command to GDB, returning the GDB answer as string. - - def drain_booth_log(self): - try: - self.booth.read_nonblocking(64*1024, 0) - except pexpect.EOF: - pass - except pexpect.TIMEOUT: - pass - finally: - pass - - def send_cmd(self, stg, timeout=-1): - # give booth a chance to get its messages out - self.drain_booth_log() - - self.gdb.sendline(stg) - return self.sync(timeout=timeout) - - def _query_value(self, which): - val = self.send_cmd("print " + which) - cleaned = re.search(r"^\$\d+ = (.*\S)\s*$", val, re.MULTILINE) - if not cleaned: - self.user_debug("query failed") - return cleaned.group(1) - - def query_value(self, which): - res = self._query_value(which) - logging.debug("query_value: «%s» evaluates to «%s»" % (which, res)) - return res - - def check_value(self, which, value): - val = self._query_value("(" + which + ") == (" + value + ")") - logging.debug("check_value: «%s» is «%s»: %s" % (which, value, val)) - if val == "1": - return True - # for easier (test) debugging we'll show the _real_ value, too. - want = self._query_value(value) - # Order is important, so that next query works!! - has = self._query_value(which) - # for informational purposes - self._query_value('state_to_string($$)') - logging.error("«%s»: got «%s», expected «%s». ERROR." % (which, has, want)) - return False - - # Send data to GDB, to inject them into the binary. - # Handles different data types - def set_val(self, name, value, numeric_conv=None): - logging.debug("setting value «%s» to «%s» (num_conv %s)" %(name, value, numeric_conv)) - res = None - # string value? - if re.match(r'^"', value): - res = self.send_cmd("print strcpy(" + name + ", " + value + ")") - elif re.match(r"^'", value): - # single-quoted; GDB only understands double quotes. - v1 = re.sub(r"^'", '', value) - v2 = re.sub(r"'$", '', v1) - # TODO: replace \\\\" etc. - v3 = re.sub(r'"', '\\"', v2) - res = self.send_cmd("print strcpy(" + name + ', "' + v3 + '")') - # numeric - elif numeric_conv: - res = self.send_cmd("set variable " + name + " = " + numeric_conv + "(" + value + ")") - else: - res = self.send_cmd("set variable " + name + " = " + value) - for r in [r"There is no member named", - r"Structure has no component named", - r"No symbol .* in current context", ]: - assert(not re.search(r, res, re.MULTILINE)) - logging.debug("set_val %s done" % name) -# }}} GDB communication - - - # there has to be some event waiting, so that boothd stops again. - def continue_debuggee(self, timeout=30): - res = None - if not self.running: - res = self.send_cmd("continue", timeout) - self.drain_booth_log() - return res - - -# {{{ High-level functions. -# Generally, GDB is attached to BOOTHD, and has it stopped. - def set_state(self, kv): - if not kv: - return - - self.current_nr = kv.aux.get("line") - #os.system("strace -f -tt -s 2000 -e write -p" + str(self.gdb.pid) + " &") - for n, v in iter_items(kv): - self.set_val( self.translate_shorthand(n, "ticket"), v) - logging.info("set state") - - - def user_debug(self, txt): - logging.error("Problem detected: %s", txt) - logging.info(self.gdb.buffer) - if not sys.stdin.isatty(): - logging.error("Not a terminal, stopping.") - else: - print("\n\nEntering interactive mode.\n\n") - self.gdb.sendline("set prompt GDB> \n") - self.gdb.setecho(True) - # can't use send_cmd, doesn't reply with expected prompt anymore. - self.gdb.interact() - #while True: - # sys.stdout.write("GDB> ") - # sys.stdout.flush() - # x = sys.stdin.readline() - # if not x: - # break - # self.send_cmd(x) - self.stop_processes() - sys.exit(1) - - - def wait_for_function(self, fn, timeout=20): - until = time.time() + timeout - while True: - stopped_at = self.continue_debuggee(timeout=3) - if not stopped_at: - self.user_debug("Not stopped at any breakpoint?") - if re.search(r"^Program received signal SIGABRT,", stopped_at, re.MULTILINE): - self.user_debug("assert() failed") - if re.search(r"^Program received signal SIGSEGV,", stopped_at, re.MULTILINE): - self.user_debug("Segfault") - if re.search(r"^Breakpoint \d+, (0x\w+ in )?%s " % fn, stopped_at, re.MULTILINE): - break - if time.time() > until: - self.user_debug("Didn't stop in function %s" % fn) - logging.info("Now in %s" % fn) - - # We break, change the data, and return the correct size. - def send_message(self, msg): - self.udp_sock.sendto('a', (socket.gethostbyname(self.this_site), self.this_port)) - - self.wait_for_function("recvfrom") - # drain input, but stop afterwards for changing data - self.send_cmd("finish") - # step over length assignment - self.send_cmd("next") - - # push message. - for (n, v) in iter_items(msg): - self.set_val( self.translate_shorthand(n, "message"), v, "htonl") - - # set "received" length - self.set_val("rv", "msg->header.length", "ntohl") - - # the next thing should run continue via wait_for_function - - def wait_outgoing(self, msg): - self.wait_for_function("booth_udp_send") - ok = True - for (n, v) in iter_items(msg): - if re.search(r"\.", n): - ok = self.check_value( self.translate_shorthand(n, "inject"), v) and ok - else: - ok = self.check_value( self.translate_shorthand(n, "ticket"), v) and ok - - if not ok: - sys.exit(1) - logging.info("out gone") - #stopped_at = self.sync() - - def merge_dicts(self, base, overlay): - return dict(base, **overlay) - - - def loop(self, fn, data): - matches = (re.match(r"^(outgoing|message)(\d+)$", k) for k in data) - loop_max = max(int(m.group(2)) for m in matches if m) - for counter in range(0, loop_max+1): # incl. last message - - kmsg = 'message%d' % counter - msg = data.get(kmsg) - - ktkt = 'ticket%d' % counter - tkt = data.get(ktkt) - - kout = 'outgoing%d' % counter - out = data.get(kout) - - kgdb = 'gdb%d' % counter - gdb = data.get(kgdb) - - - if not any([msg, out, tkt]): - continue - - logging.info("Part %d" % counter) - if tkt: - self.current_nr = tkt.aux.get("line") - comment = tkt.aux.get("comment", "") - logging.info("ticket change %s (%s:%d) %s" % (ktkt, fn, self.current_nr, comment)) - self.set_state(tkt) - if gdb: - for (k, v) in iter_items(gdb): - self.send_cmd(k + " " + v.replace("§", "\n")) - if msg: - self.current_nr = msg.aux.get("line") - comment = msg.aux.get("comment", "") - logging.info("sending %s (%s:%d) %s" % (kmsg, fn, self.current_nr, comment)) - self.send_message(self.merge_dicts(data["message"], msg)) - if kgdb in data and len(gdb) == 0: - self.user_debug("manual override") - if out: - self.current_nr = out.aux.get("line") - comment = out.aux.get("comment", "") - logging.info("waiting for %s (%s:%d) %s" % (kout, fn, self.current_nr, comment)) - self.wait_outgoing(out) - logging.info("loop ends") - - - def let_booth_go_a_bit(self): - self.drain_booth_log() - logging.debug("running: %d" % self.running) - - if not self.running: - self.gdb.sendline("continue") - time.sleep(1) - self.drain_booth_log() - # stop it - via GDB! - self.gdb.sendintr() - # If we sent the signal to booth, the next - # "print state_to_string()" or "continue" - # might catch the signal - and fail to do - # what we want/need. - # - # This additional signal seems to be unnecessary. - #posix.kill(self.gdb.pid, signal.SIGINT) - # In case it's really needed we should drain booth's signals queue, - # eg. by sending "print getpid()" twice, before the sync() call. - self.running = False - self.sync(2000) - - - def do_finally(self, data): - if not data: - return - - self.current_nr = data.aux.get("line") - # Allow debuggee to reach a stable state - self.let_booth_go_a_bit() - - ok = True - for (n, v) in iter_items(data): - ok = self.check_value( self.translate_shorthand(n, "ticket"), v) and ok - if not ok: - sys.exit(1) - - - def run(self, start_from="000", end_with="999"): - os.chdir(self.test_base) - # TODO: sorted, random order - tests = sorted(f for f in glob.glob("*") - if re.match(r"^\d\d\d_.*\.txt$", f)) - failed = 0 - for f in tests: - if f[0:3] < start_from: - continue - if f[0:3] > end_with: - continue - log = None - logfn = UT._filename(f) - if self.running_on_console(): - sys.stdout.write("\n") - self.current_nr = "setup" - try: - log = self.setup_log(filename = logfn) - - log.setLevel(logging.DEBUG) - logging.error(self.colored_string("Starting test '%s'" % f, self.BLUE) + ", logfile " + logfn) - self.start_processes(f) - - test = self.read_test_input(f, m=copy.deepcopy(self.defaults)) - logging.debug("data: %s" % pprint.pformat(test, width = 200)) - - self.set_state(test.get("ticket")) - self.loop(f, test) - self.do_finally(test.get("finally")) - - self.current_nr = "teardown" - logging.warn(self.colored_string("Finished test '%s' - OK" % f, self.GREEN)) - except: - failed += 1 - logging.error(self.colored_string("Broke in %s:%s %s" % (f, self.current_nr, sys.exc_info()), self.RED)) - for frame in traceback.format_tb(sys.exc_info()[2]): - logging.info(" - %s " % frame.rstrip()) - finally: - self.stop_processes() - if log: - log.close() - logging.getLogger("").removeHandler(log) - if self.running_on_console(): - sys.stdout.write("\n") - return failed -# }}} - - -#def traceit(frame, event, arg): -# if event == "line": -# lineno = frame.f_lineno -# print frame.f_code.co_filename, ":", "line", lineno -# return traceit - - -# {{{ main -if __name__ == '__main__': - # Likely assumption for the root exclusion is the amount of risk - # associated with what naturally accompanies root privileges: - # - accidental overwrite (eventually also deletion) of unrelated, - # legitimate and perhaps vital files - # - accidental termination of unrelated, legitimate and perhaps - # vital processes - # - and so forth, possibly amplified with awkward parallel test - # suite run scenarios (containers partly sharing state, etc.) - # - # Nonetheless, there are cases like self-contained CI runs where - # all these concerns are absent, so allow opt-in relaxing of this. - # Alternatively, the config generator could inject particular - # credentials for a booth proces to use, but that might come too - # late to address the above concerns reliably. - if (os.geteuid() == 0 - and "--allow-root-user" not in sys.argv - and not(os.environ.get("BOOTH_UNITTEST_ROOT_USER"))): - sys.stderr.write("Must be run non-root; aborting.\n") - sys.exit(1) - - - ut = UT(sys.argv[1], sys.argv[2] + "/") - - # "master" log object needs max level - logging.basicConfig(level = logging.DEBUG, - filename = "/dev/null", - filemode = "a", - format = default_log_format, - datefmt = default_log_datefmt) - - - # make sure no old processes are active anymore - os.system("killall boothd > /dev/null 2> /dev/null") - - overview_log = ut.setup_log( filename = UT._filename('seq') ) - overview_log.setLevel(logging.WARN) - - # http://stackoverflow.com/questions/9321741/printing-to-screen-and-writing-to-a-file-at-the-same-time - console = logging.StreamHandler() - console.setFormatter(logging.Formatter(' # %(message)s')) - console.setLevel(logging.WARN) - logging.getLogger('').addHandler(console) - - - logging.info("Starting boothd unit tests.") - - #sys.settrace(traceit) - - starting = "0" - if len(sys.argv) > 3: - starting = sys.argv[3] - ending = "999" - if len(sys.argv) > 4: - ending = sys.argv[4] - ret = ut.run(starting, ending) - sys.exit(ret) -# }}} diff --git a/unit-tests/001_init-get-heartbeat.txt b/unit-tests/001_init-get-heartbeat.txt deleted file mode 100644 index fa79a066..00000000 --- a/unit-tests/001_init-get-heartbeat.txt +++ /dev/null @@ -1,25 +0,0 @@ -# vim: ft=sh et : - - -ticket: - state ST_FOLLOWER - current_term 1 - leader 0 - -# should be accepted -message0: # valid heartbeat - header.cmd OP_HEARTBEAT - header.result RLT_SUCCESS - header.from booth_conf->site[2].site_id - ticket.leader booth_conf->site[2].site_id - ticket.term_valid_for 3 - ticket.term 20 - -# nothing goes out - - -# after a delay, check final state -finally: -# should be overwritten - current_term 20 - leader booth_conf->site+2 diff --git a/unit-tests/002_bad_packets.txt b/unit-tests/002_bad_packets.txt deleted file mode 100644 index 8a708055..00000000 --- a/unit-tests/002_bad_packets.txt +++ /dev/null @@ -1,70 +0,0 @@ -# vim: ft=sh et : -# -# This test is mostly concerned with ignoring invalid packets. -# We're expecting heartbeat packets. - - -ticket: - state ST_LEADER - leader local - current_term 500 - - -# defaults -message: - header.cmd OP_HEARTBEAT - ticket.term 500 - #header.from booth_conf->site[2].site_id - header.from local->site_id - header.result 0 - - -message0: # bad result code - header.result 243521741 - -outgoing0: - state ST_LEADER - - -message1: # bad sender - header.from 71 - -outgoing1: - state ST_LEADER - - - -message2: # bad version - header.version 512 - -outgoing2: - state ST_LEADER - - -message3: # bad magic - header.version 31 - -outgoing3: - state ST_LEADER - -message4: # bad length - header.length 16 - -outgoing4: - state ST_LEADER - - -message5: # bad ticket ID - ticket.id "gibtsnich" - -outgoing5: - state ST_LEADER - - -message100: # should work - ticket.term 510 - -# no outgoing message -finally: - state ST_FOLLOWER - current_term 510 diff --git a/unit-tests/003_pacemaker.txt b/unit-tests/003_pacemaker.txt deleted file mode 100644 index 2b039a7b..00000000 --- a/unit-tests/003_pacemaker.txt +++ /dev/null @@ -1,22 +0,0 @@ -# vim: ft=sh et : -# -# Checks whether Pacemaker gets correct command lines. - - -ticket: - state ST_FOLLOWER - current_term 40 - term_expires time(0) + 30 - - -message0: - header.cmd OP_HEARTBEAT - header.from booth_conf->site[2].site_id - header.result 0 - ticket.term 44 - ticket.leader booth_conf->site[2].site_id - #getenv("UNIT_TEST_AUX") "ballot 99 owner +" - - -finally: - current_term 44 diff --git a/unit-tests/010_retries.txt b/unit-tests/010_retries.txt deleted file mode 100644 index 9eaeabf6..00000000 --- a/unit-tests/010_retries.txt +++ /dev/null @@ -1,62 +0,0 @@ -# vim: ft=sh et : -# -# Testing whether retries are sent, and if they're stopped again. - - -ticket: - state ST_LEADER - current_term 40 - leader local - retries 10000 # needed so that heartbeats are sent _now_ - timeout 1 - # may keep ticket all the time - term_duration 3000 - # but shall start renewal now - term_expires time(0) + 1000 - - - -outgoing0: - header.cmd OP_HEARTBEAT - ticket.term 40 -outgoing1: - header.cmd OP_HEARTBEAT - ticket.term 40 -outgoing2: - header.cmd OP_HEARTBEAT - ticket.term 40 -outgoing3: - header.cmd OP_HEARTBEAT - ticket.term 40 - -# yes, you're the leader. -message4: - header.cmd OP_HEARTBEAT - header.from booth_conf->site[2].site_id - header.result 0 - ticket.term 40 - ticket.leader local->site_id - -# doesn't stop ... there is no retry limit - -outgoing5: - header.cmd OP_HEARTBEAT -outgoing6: - header.cmd OP_HEARTBEAT -outgoing7: - header.cmd OP_HEARTBEAT -outgoing8: - header.cmd OP_HEARTBEAT -outgoing9: - header.cmd OP_HEARTBEAT -outgoing10: - header.cmd OP_HEARTBEAT - - -# Now term expires -ticket11: - term_expires time(0) - 1 - -# no outgoing message, gets to be follower -finally: - state ST_FOLLOWER diff --git a/unit-tests/020_ext-verifier.txt b/unit-tests/020_ext-verifier.txt deleted file mode 100644 index b1a5e9d4..00000000 --- a/unit-tests/020_ext-verifier.txt +++ /dev/null @@ -1,53 +0,0 @@ -# vim: ft=sh et : -# -# Testing whether the external verifier (before-acquire-handler) -# is obeyed. - - -ticket: - name "tick1" - state ST_LEADER - current_term 40 - leader local - # may keep ticket all the time - term_duration 3000 - # but shall start renewal now - term_expires time(0) + 1000 - req_sent_at time(0) - 10 - - -gdb0: - call parse_extprog("test `set|grep ^BOOTH|wc -l` -ge 5", booth_conf->ticket+0) - -outgoing0: - header.cmd OP_HEARTBEAT - - -ticket1: - ext_verifier 'test "$BOOTH_TICKET" == "tick1"' - # cause re-query of the verifier - req_sent_at time(0) - 10 - -# -#gdb1: -# break ticket_broadcast_proposed_state § commands § bt § c § end - - -outgoing1: - header.cmd OP_HEARTBEAT - - -# now say that we may not have it anymore. -ticket2: - ext_verifier 'test "$BOOTH_TICKET" == "tick2FOO"' - # cause re-query of the verifier - req_sent_at time(0) - 10 - -# We just tell the others we don't have it anymore. -outgoing2: - header.cmd OP_REQ_VOTE - ticket.leader -1 - -finally: - state ST_FOLLOWER - leader NULL diff --git a/unit-tests/060_catchup_same_owner.txt b/unit-tests/060_catchup_same_owner.txt deleted file mode 100644 index 73899cda..00000000 --- a/unit-tests/060_catchup_same_owner.txt +++ /dev/null @@ -1,38 +0,0 @@ -# vim: ft=sh et : -# We've got the ticket; a peer agrees with us re. owner, but has a -# higher term number. -# We must not lose the ticket. - -ticket: - state ST_LEADER - current_term 100 - leader local - term_expires time(0) + 35 - term_duration 3000 - retries 6 - timeout 1 - hb_sent_at time(0) - 2000 - - -gdb0: - watch booth_conf->ticket[0].leader § commands § bt § c § end - -# No message0 - -outgoing0: - header.cmd OP_HEARTBEAT - header.result RLT_SUCCESS - ticket.term 100 - ticket.leader local->site_id - - -message1: # same owner - header.cmd OP_HEARTBEAT - header.result RLT_SUCCESS - header.from booth_conf->site[2].site_id - ticket.term 110 - ticket.leader local->site_id - -finally: - leader local - diff --git a/unit-tests/100_abort-after-retries.txt b/unit-tests/100_abort-after-retries.txt deleted file mode 100644 index 881d7858..00000000 --- a/unit-tests/100_abort-after-retries.txt +++ /dev/null @@ -1,40 +0,0 @@ -# vim: ft=sh et : -# -# Testing whether retries are aborted at some time. - - -ticket: - state ST_STABLE - last_ack_ballot 40 - new_ballot 50 - retries 6 - timeout 1 - owner local - expiry 3000 - # but renewing is necessary - expires time(0) + 100 - next_cron time(0) + 1 - - -outgoing0: - header.cmd OP_PREPARING -outgoing1: - header.cmd OP_PREPARING -outgoing2: - header.cmd OP_PREPARING -outgoing3: - header.cmd OP_PREPARING - -# Now give cause to abort. -ticket4: - expires time(0) - 2 - retry_number 10 - timeout 2 -outgoing4: - header.cmd CMD_CATCHUP - - -# ticket must be lost -finally: - owner 0 - state ST_INIT diff --git a/unit-tests/_defaults.txt b/unit-tests/_defaults.txt deleted file mode 100644 index 46feb969..00000000 --- a/unit-tests/_defaults.txt +++ /dev/null @@ -1,55 +0,0 @@ -# vim: ft=sh et : - - -# ticket defaults, mostly set via config file. -ticket: - - name "ticket" - - ## these would matter if testing via GDB had high latencies - #expiry 60 - #timeout 10 - acquire_after 0 - - - - # defaults for all tests - state ST_INIT - next_cron.tv_sec 0 - -# time(0)+1 - # local is site[0] per convention - - leader booth_conf->site+1 - #owner booth_conf->site+1 - #expires time(0)+1 - term_expires.tv_sec time(0)+1 - #last_ack_ballot 242 - - leader 0 - #proposer 0 - #proposed_owner 0 - #new_ballot 0 - #proposal_acknowledges 0 - #retry_number 0 - - - -# defaults for input message. -# sender is a peer, and it wants something. -message: - - ticket.id "ticket" - # invalid by default - header.cmd -1 - # invalid by default - header.result 1 - # invalid by default - header.from -1 - header.version BOOTHC_VERSION - header.magic BOOTHC_MAGIC - header.length sizeof(struct boothc_ticket_msg) - ticket.leader -1 - ticket.term 0 - ticket.term_valid_for 0 - diff --git a/unit-tests/bin/crm_ticket b/unit-tests/bin/crm_ticket deleted file mode 100755 index cf697616..00000000 --- a/unit-tests/bin/crm_ticket +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -# -# "fake" crm_ticket for unit tests. -# * Ignores set,grant,revoke actions -# * returns values it gets asked for from UNIT_TEST_AUX - - - -function word_after() -{ -# Per default $1 is printed - perl -e ' - $search=shift(); - $stg=shift(); - print $'${3:-1}' if $stg =~ /$search/;' "$1" "$2" -} - - -function Get() -{ - which=$(word_after " -G ('?)(\\w+)\\1" "$1" 2) - if [[ -z "$which" ]] ; then - exit 1 - fi - - word_after "\\b$which\\b (\\S+)" "$UNIT_TEST_AUX" - # provide a newline - echo "" - exit 0 -} - - - -# include resp. supersede with per-test functions -inc="${UNIT_TEST_FILE%.txt}.sh" -if [[ -e "$inc" ]] ; then - . "$inc" -fi - - -if [[ "$*" == *" -G "* ]] ; then - Get "$*" -elif [[ "$*" == *" -g "* ]] ; then - : # grant -elif [[ "$*" == *" -r "* ]] ; then - : # revoke -elif [[ "$*" == *" -S "* ]] ; then - : # set -else - echo "not understood" >&2 - exit 1 -fi diff --git a/unit-tests/booth.conf b/unit-tests/booth.conf deleted file mode 100644 index 5a69acb4..00000000 --- a/unit-tests/booth.conf +++ /dev/null @@ -1,18 +0,0 @@ -# "local" -site=127.0.0.1 - -# these should not exist, although it shouldn't matter much -# because no packets should be sent anyway -arbitrator="127.0.0.243" -site="127.0.0.244" - -# The ticket name, which corresponds to a set of resources which can be -# fail-overed among different sites. -ticket="ticket" - expire = 60 - timeout = 1 - acquire-after = 30 - weights = 1,2,3 - - # make it non-critical, put provide enough space to override if necessary. - before-acquire-handler = "//////////////////////////////////bin/true" diff --git a/unit-tests/init-catchup.txt b/unit-tests/init-catchup.txt deleted file mode 100644 index c681e89d..00000000 --- a/unit-tests/init-catchup.txt +++ /dev/null @@ -1,48 +0,0 @@ -# vim: ft=sh et : - - -ticket: - state ST_INIT - last_ack_ballot 1 - new_ballot 2012 - -# No message0 -# expecting catchup query - -# outgoing packet: expect this data -outgoing0: - header.cmd CMD_CATCHUP - header.result RLT_SUCCESS - - -# ignore "bad" catchup data -message1: - header.from booth_conf->site[2].site_id - header.cmd CMR_CATCHUP - header.result 243521741 - -outgoing1: - header.cmd CMD_CATCHUP - header.result RLT_SUCCESS - - -# accept good CMR_CATCHUP -message2: - header.cmd CMR_CATCHUP - header.result RLT_SUCCESS - header.from booth_conf->site[2].site_id - ticket.ballot 2062 - ticket.prev_ballot 2052 - ticket.owner -1 - -# nothing goes out - - -# after a delay, check final state -finally: -# should be overwritten - last_ack_ballot 2052 -# should not be - a OP_PREPARING would fetch the new value - new_ballot 2012 -# too low-level -# proposal_acknowledges 5