git » libfiu » commit bd1a774

bindings/python: Add a fiu_ctrl.py module

author Alberto Bertogli
2014-05-23 01:01:29 UTC
committer Alberto Bertogli
2014-05-25 23:49:40 UTC
parent 76326ceb468758fac59ad86e5d6eac0def6d190b

bindings/python: Add a fiu_ctrl.py module

This patch adds a new Python module, fiu_ctrl.py, which implements similar
functionality to the fiu-run and fiu-ctrl shell utilities, but it can be used
from within Python to control tests.

.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
+