author | Alberto Bertogli
<albertito@blitiri.com.ar> 2014-05-23 01:01:29 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2014-05-25 23:49:40 UTC |
parent | 76326ceb468758fac59ad86e5d6eac0def6d190b |
.gitignore | +2 | -0 |
bindings/python/fiu_ctrl.in.py | +200 | -0 |
bindings/python/setup.py | +26 | -2 |
tests/Makefile | +19 | -1 |
tests/test-fiu_ctrl.py | +68 | -0 |
diff --git a/.gitignore b/.gitignore index 5b38e06..55e90a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ bindings/python/build bindings/python/*.pyc bindings/python/*.pyo +bindings/python/fiu_ctrl.py libfiu/*.o libfiu/libfiu.a libfiu/libfiu.pc @@ -28,6 +29,7 @@ preload/run/build-needlibdl tests/*.o tests/build-flags tests/test-? +tests/libs/ tests/generated/build-flags tests/generated/tests/*.[oc] .*.swp diff --git a/bindings/python/fiu_ctrl.in.py b/bindings/python/fiu_ctrl.in.py new file mode 100644 index 0000000..8228e08 --- /dev/null +++ b/bindings/python/fiu_ctrl.in.py @@ -0,0 +1,200 @@ +""" +libfiu python module for remote control + +This module provides an easy way to run a command with libfiu enabled, and +controlling the failure points dynamically. + +It provides similar functionality to the fiu-ctrl and fiu-run shell tools, but +is useful for programmed tests. + +Note it assumes the preloading libraries are installed in @@PLIBPATH@@. +""" + +import os +import tempfile +import subprocess +import shutil +import time + + +# Default path to look for preloader libraries. +PLIBPATH = "@@PLIBPATH@@" + + +class CommandError (RuntimeError): + """There was an error running the command.""" + pass + + +class Flags: + """Contains the valid flag constants. + + ONETIME: This point of failure is disabled immediately after failing once. + """ + ONETIME = "onetime" + + +class _ControlBase (object): + """Base class for remote control objects.""" + + def run_raw_cmd(self, cmd, args): + """Runs a new raw command. To be implemented by subclasses""" + raise NotImplementedError + + def _basic_args(self, name, failnum, failinfo, flags): + """Converts the common arguments to an args list for run_raw_cmd().""" + args = ["name=%s" % name] + if failnum: + args.append("failnum=%s" % failnum) + if failinfo: + args.append("failinfo=%s" % failinfo) + if flags: + args.extend(flags) + + return args + + def enable(self, name, failnum = 1, failinfo = None, flags = ()): + """Enables the given point of failure.""" + args = self._basic_args(name, failnum, failinfo, flags) + self.run_raw_cmd("enable", args) + + def enable_random(self, name, probability, failnum = 1, + failinfo = None, flags = ()): + "Enables the given point of failure, with the given probability." + args = self._basic_args(name, failnum, failinfo, flags) + args.append("probability=%f" % probability) + self.run_raw_cmd("enable_random", args) + + def enable_stack_by_name(self, name, func_name, + failnum = 1, failinfo = None, flags = (), + pos_in_stack = -1): + """Enables the given point of failure, but only if 'func_name' is in + the stack. + + 'func_name' is be the name of the C function to look for. + """ + args = self._basic_args(name, failnum, failinfo, flags) + args.append("func_name=%s" % func_name) + if pos_in_stack >= 0: + args.append("pos_in_stack=%d" % pos_in_stack) + self.run_raw_cmd("enable_stack_by_name", args) + + def disable(self, name): + """Disables the given point of failure.""" + self.run_raw_cmd("disable", ["name=%s" % name]) + + +def _open_with_timeout(path, mode, timeout = 3): + """Open a file, waiting if it doesn't exist yet.""" + deadline = time.time() + timeout + while not os.path.exists(path): + time.sleep(0.01) + if time.time() >= deadline: + raise RuntimeError("Timeout waiting for file %r" % path) + + return open(path, mode) + + +class PipeControl (_ControlBase): + """Control pipe used to control a libfiu-instrumented process.""" + def __init__(self, path_prefix): + """Constructor. + + Args: + path: Path to the control pipe. + """ + self.path_in = path_prefix + ".in" + self.path_out = path_prefix + ".out" + + def _open_pipes(self): + # Open the files, but wait if they are not there, as the child process + # may not have created them yet. + fd_in = _open_with_timeout(self.path_in, "a") + fd_out = _open_with_timeout(self.path_out, "r") + return fd_in, fd_out + + def run_raw_cmd(self, cmd, args): + """Send a raw command over the pipe.""" + # Note we open the pipe each time for simplicity, and also to simplify + # external intervention that can be used for debugging. + fd_in, fd_out = self._open_pipes() + + s = "%s %s\n" % (cmd, ','.join(args)) + fd_in.write(s) + fd_in.flush() + + r = int(fd_out.readline()) + if r != 0: + raise CommandError + + +class EnvironmentControl (_ControlBase): + """Pre-execution environment control.""" + def __init__(self): + self.env = "" + + def run_raw_cmd(self, cmd, args): + """Add a raw command to the environment.""" + self.env += "%s %s\n" % (cmd, ','.join(args)) + + +class Subprocess (_ControlBase): + """Wrapper for subprocess.Popen, but without immediate execution. + + This class provides a wrapper for subprocess.Popen, which can be used to + run other processes under libfiu. + + However, the processes don't start straight away, allowing the user to + pre-configure some failure points. + + The process can then be started with the start() method. + + After the process has been started, the failure points can be controlled + remotely via the same functions. + + Processes can be started only once. + Note that using shell=True is not recommended, as it makes the pid of the + controlled process to be unknown. + """ + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + # Note this removes fiu_enable_posix from kwargs if it's there, that + # way kwargs remains "clean" for passing to Popen. + self.fiu_enable_posix = kwargs.pop('fiu_enable_posix', False) + + self._proc = None + self.tmpdir = None + + # Initially, this is an EnvironmentControl so we can do preparation; + # once we start the command, we will change this to be PipeControl. + self.ctrl = EnvironmentControl() + + def run_raw_cmd(self, cmd, args): + self.ctrl.run_raw_cmd(cmd, args) + + def start(self): + self.tmpdir = tempfile.mkdtemp(prefix = 'fiu_ctrl-') + + env = os.environ + env['LD_PRELOAD'] = env.get('LD_PRELOAD', '') + if self.fiu_enable_posix: + env['LD_PRELOAD'] += ' ' + PLIBPATH + '/fiu_posix_preload.so' + env['LD_PRELOAD'] += ' ' + PLIBPATH + '/fiu_run_preload.so ' + env['FIU_CTRL_FIFO'] = self.tmpdir + '/ctrl-fifo' + env['FIU_ENABLE'] = self.ctrl.env + + self._proc = subprocess.Popen(*self.args, **self.kwargs) + + fifo_path = "%s-%d" % (env['FIU_CTRL_FIFO'], self._proc.pid) + self.ctrl = PipeControl(fifo_path) + + return self._proc + + def __del__(self): + # Remove the temporary directory. + # The "'fiu_ctrl-' in self.tmpdir" check is just a safeguard. + if self.tmpdir and 'fiu_ctrl-' in self.tmpdir: + shutil.rmtree(self.tmpdir) + diff --git a/bindings/python/setup.py b/bindings/python/setup.py index fed2a78..6bb9c0b 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -1,12 +1,35 @@ +import os import sys from distutils.core import setup, Extension +from distutils.command.build_py import build_py if sys.version_info[0] == 2: ver_define = ('PYTHON2', '1') elif sys.version_info[0] == 3: ver_define = ('PYTHON3', '1') + +# We need to generate the fiu_ctrl.py file from fiu_ctrl.in.py, replacing some +# environment variables in its contents. +class generate_and_build_py (build_py): + def run(self): + if not self.dry_run: + self.generate_fiu_ctrl() + build_py.run(self) + + def generate_fiu_ctrl(self): + prefix = os.environ.get('PREFIX', '/usr/local/') + plibpath = os.environ.get('PLIBPATH', prefix + '/lib/') + + contents = open('fiu_ctrl.in.py', 'r').read() + contents = contents.replace('@@PLIBPATH@@', plibpath) + + out = open('fiu_ctrl.py', 'w') + out.write(contents) + out.close() + + fiu_ll = Extension("fiu_ll", sources = ['fiu_ll.c'], define_macros = [ver_define], @@ -23,7 +46,8 @@ setup( author = "Alberto Bertogli", author_email = "albertito@blitiri.com.ar", url = "http://blitiri.com.ar/p/libfiu", - py_modules = ['fiu'], - ext_modules = [fiu_ll] + py_modules = ['fiu', 'fiu_ctrl'], + ext_modules = [fiu_ll], + cmdclass = {'build_py': generate_and_build_py}, ) diff --git a/tests/Makefile b/tests/Makefile index 955e181..7d57070 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -15,10 +15,12 @@ ifneq ($(V), 1) NICE_CC = @echo " CC $@"; $(CC) NICE_RUN = @echo " RUN $<"; LD_LIBRARY_PATH=../libfiu/ NICE_PY = @echo " PY $<"; ./wrap-python 2 + NICE_LN = @echo " LN $@"; ln -f else NICE_CC = $(CC) NICE_RUN = LD_LIBRARY_PATH=../libfiu/ NICE_PY = ./wrap-python 2 + NICE_LN = ln -f endif default: tests @@ -27,6 +29,20 @@ all: tests tests: c-tests py-tests gen-tests utils-tests + +# Link the libraries to a single place, some of the tests need this. +libs: + mkdir -p libs/ + +libs/fiu_posix_preload.so: ../preload/posix/fiu_posix_preload.so libs + $(NICE_LN) $< libs/ + +libs/fiu_run_preload.so: ../preload/run/fiu_run_preload.so libs + $(NICE_LN) $< libs/ + +lnlibs: libs/fiu_posix_preload.so libs/fiu_run_preload.so + + # # C tests # @@ -65,9 +81,10 @@ PY_TESTS := $(wildcard test-*.py) py-tests: $(patsubst %.py,py-run-%,$(PY_TESTS)) -py-run-%: %.py +py-run-%: %.py lnlibs $(NICE_PY) ./$< + # # Sub-directory tests # @@ -87,6 +104,7 @@ utils-tests: # also remove them when cleaning just in case. clean: rm -f $(C_OBJS) $(C_BINS) + rm -rf libs/ rm -f *.bb *.bbg *.da *.gcov *.gcda *.gcno gmon.out build-flags $(MAKE) -C generated clean diff --git a/tests/test-fiu_ctrl.py b/tests/test-fiu_ctrl.py new file mode 100644 index 0000000..104ca9d --- /dev/null +++ b/tests/test-fiu_ctrl.py @@ -0,0 +1,68 @@ +""" +Tests for the fiu_ctrl.py module. + +Note the command line utility is covered by the utils/ tests, not from here, +this is just for the Python module. +""" + +import subprocess +import fiu_ctrl +import errno +import time + +fiu_ctrl.PLIBPATH = "./libs/" + +def run_cat(**kwargs): + return fiu_ctrl.Subprocess(["/bin/cat"], + stdin = subprocess.PIPE, stdout = subprocess.PIPE, + stderr = subprocess.PIPE, **kwargs) + +# Run without any failure point being enabled. +cmd = run_cat() +p = cmd.start() +out, err = p.communicate('test\n') +assert out == 'test\n', out +assert err == '', err + +# Enable before starting. +cmd = run_cat(fiu_enable_posix = True) +cmd.enable('posix/io/rw/*', failinfo = errno.ENOSPC) +p = cmd.start() +out, err = p.communicate('test\n') +assert out == '', out +assert 'space' in err, err + +# Enable after starting. +cmd = run_cat(fiu_enable_posix = True) +p = cmd.start() +cmd.enable('posix/io/rw/*', failinfo = errno.ENOSPC) +out, err = p.communicate('test\n') +assert out == '', out +assert 'space' in err, err + +# Enable-disable. +cmd = run_cat(fiu_enable_posix = True) +p = cmd.start() +cmd.enable('posix/io/rw/*', failinfo = errno.ENOSPC) +cmd.disable('posix/io/rw/*') +out, err = p.communicate('test\n') +assert out == 'test\n', (out, err) + +# Enable random. +result = { True: 0, False: 0 } +for i in range(50): + cmd = run_cat(fiu_enable_posix = True) + p = cmd.start() + cmd.enable_random('posix/io/rw/*', failinfo = errno.ENOSPC, + probability = 0.5) + out, err = p.communicate('test\n') + if 'space' in err: + result[False] += 1 + elif out == 'test\n': + result[True] += 1 + else: + assert False, (out, err) + +assert 10 < result[True] < 40, result +assert 10 < result[False] < 40, result +