This patch adds tests for the swig generated python library bindings
that reuse the C language tests.

Fitting it into autotools was a bit of a trick, and is likely pretty
brittle, as before the test script runs, it needs to know the location
of the built libapparmor.so library, the built _LibAppArmor.so library
and the python wrapper bits (thankfully, the latter two are the same
directory). It's also unclear how to get autotools to emit the output of
the test_python.py script when building, rather than just summarizing it
as one test run.

Also note that test_python.py is doing a bit of magic to automatically
generate test case methods based on the contents of the test_multi/
directory. This has the disadvantage of breaking tools like nosetests
and other external tools that try to automatically detect testcases.

Signed-off-by: Steve Beattie <steve@nxnw.org>
---
 libraries/libapparmor/configure.ac                       |    1 
 libraries/libapparmor/swig/python/Makefile.am            |    2 
 libraries/libapparmor/swig/python/setup.py.in            |    1 
 libraries/libapparmor/swig/python/test/Makefile.am       |   21 ++
 libraries/libapparmor/swig/python/test/test_python.py.in |  147 +++++++++++++++
 5 files changed, 172 insertions(+)

Index: b/libraries/libapparmor/swig/python/Makefile.am
===================================================================
--- a/libraries/libapparmor/swig/python/Makefile.am
+++ b/libraries/libapparmor/swig/python/Makefile.am
@@ -2,6 +2,8 @@ if HAVE_PYTHON
 
 EXTRA_DIST      = libapparmor_wrap.c
 
+SUBDIRS = test
+
 libapparmor_wrap.c: $(srcdir)/../SWIG/libapparmor.i
 	$(SWIG) -python -I$(srcdir)/../../src -module LibAppArmor -o $@ $(srcdir)/../SWIG/libapparmor.i
 	mv LibAppArmor.py __init__.py
Index: b/libraries/libapparmor/swig/python/setup.py.in
===================================================================
--- a/libraries/libapparmor/swig/python/setup.py.in
+++ b/libraries/libapparmor/swig/python/setup.py.in
@@ -15,4 +15,5 @@ setup(name          = 'LibAppArmor',
                                  include_dirs=['@top_srcdir@/src'],
                                  extra_link_args = '-L@top_builddir@/src/.libs -lapparmor'.split(),
                     )],
+      scripts       = [],
       )
Index: b/libraries/libapparmor/configure.ac
===================================================================
--- a/libraries/libapparmor/configure.ac
+++ b/libraries/libapparmor/configure.ac
@@ -76,6 +76,7 @@ swig/perl/Makefile
 swig/perl/Makefile.PL
 swig/python/Makefile
 swig/python/setup.py
+swig/python/test/Makefile
 swig/ruby/Makefile
 testsuite/Makefile
 testsuite/config/Makefile
Index: b/libraries/libapparmor/swig/python/test/Makefile.am
===================================================================
--- /dev/null
+++ b/libraries/libapparmor/swig/python/test/Makefile.am
@@ -0,0 +1,21 @@
+if HAVE_PYTHON
+
+# NOTE: tests needs to exist in test/test*.py for python's setuptools
+# not to treat it as a script to install.
+
+test_python.py: test_python.py.in $(top_builddir)/config.status
+	$(AM_V_GEN)cd "$(top_builddir)" && \
+	$(SHELL) ./config.status --file="swig/python/test/$@"
+	chmod +x test_python.py
+
+CLEANFILES = test_python.py
+
+# bah, how brittle is this?
+PYTHON_DIST_BUILD_PATH = '$(builddir)/../build/$$($(PYTHON) -c "import distutils.util; import platform; print(\"lib.%s-%s\" %(distutils.util.get_platform(), platform.python_version()[:3]))")'
+
+TESTS	= test_python.py
+TESTS_ENVIRONMENT = \
+  LD_LIBRARY_PATH='$(top_builddir)/src/.libs:$(PYTHON_DIST_BUILD_PATH)' \
+  PYTHONPATH='$(PYTHON_DIST_BUILD_PATH)'
+
+endif
Index: b/libraries/libapparmor/swig/python/test/test_python.py.in
===================================================================
--- /dev/null
+++ b/libraries/libapparmor/swig/python/test/test_python.py.in
@@ -0,0 +1,147 @@
+#! @PYTHON@
+# ------------------------------------------------------------------
+#
+#   Copyright (C) 2013 Canonical Ltd.
+#   Author: Steve Beattie <steve@nxnw.org>
+#
+#   This program is free software; you can redistribute it and/or
+#   modify it under the terms of version 2 of the GNU General Public
+#   License published by the Free Software Foundation.
+#
+# ------------------------------------------------------------------
+
+import ctypes
+import os
+import unittest
+import LibAppArmor as libapparmor
+
+TESTDIR = "../../../testsuite/test_multi"
+
+# map of testsuite .out entries that aren't a simple to_lower() and
+# s/ /_/ of the field name
+
+OUTPUT_MAP = {
+    'Event type':       'event',
+    'Mask':             'requested_mask',
+    'Command':          'comm',
+    'Token':            'magic_token',
+    'ErrorCode':        'error_code',
+    'Network family':   'net_family',
+    'Socket type':      'net_sock_type',
+    'Protocol':         'net_protocol',
+    'Local addr':       'net_local_addr',
+    'Foreign addr':     'net_foreign_addr',
+    'Local port':       'net_local_port',
+    'Foreign port':     'net_foreign_port',
+    'Audit subid':      'audit_sub_id',
+}
+
+# FIXME: pull this automatically out of LibAppArmor, but swig
+# classes aren't real enough. (Perhaps we're not using swig correctly)
+EVENT_MAP = {
+    libapparmor.AA_RECORD_ALLOWED:  'AA_RECORD_ALLOWED',
+    libapparmor.AA_RECORD_AUDIT:    'AA_RECORD_AUDIT',
+    libapparmor.AA_RECORD_DENIED:   'AA_RECORD_DENIED',
+    libapparmor.AA_RECORD_ERROR:    'AA_RECORD_ERROR',
+    libapparmor.AA_RECORD_HINT:     'AA_RECORD_HINT',
+    libapparmor.AA_RECORD_INVALID:  'AA_RECORD_INVALID',
+    libapparmor.AA_RECORD_STATUS:   'AA_RECORD_STATUS',
+}
+
+# default is None if not in this table
+NO_VALUE_MAP = {
+    'fsuid': int(ctypes.c_ulonglong(-1).value),
+    'ouid':  int(ctypes.c_ulonglong(-1).value),
+}
+
+
+class AAPythonBindingsTests(unittest.TestCase):
+
+    def setUp(self):
+        # REPORT ALL THE OUTPUT
+        self.maxDiff = None
+
+    def _runtest(self, testname):
+        infile = "%s.in" % (testname)
+        outfile = "%s.out" % (testname)
+        # infile *should* only contain one line
+        with open(os.path.join(TESTDIR, infile), 'r') as f:
+            line = f.read()
+            swig_record = libapparmor.parse_record(line)
+
+        record = self.create_record_dict(swig_record)
+        record['file'] = infile
+        libapparmor.free_record(swig_record)
+
+        expected = self.parse_output_file(outfile)
+        self.assertEquals(expected, record,
+                          "expected records did not match\n" +
+                          "expected = %s\nactual = %s" % (expected, record))
+
+    def parse_output_file(self, outfile):
+        '''parse testcase .out file and return dict'''
+
+        output = dict()
+        with open(os.path.join(TESTDIR, outfile), 'r') as f:
+            lines = f.readlines()
+
+        count = 0
+        for l in lines:
+            line = l.rstrip('\n')
+            count += 1
+            if line == "START":
+                self.assertEquals(count, 1,
+                                  "Unexpected output format in %s" % (outfile))
+                continue
+            else:
+                key, value = line.split(": ", 1)
+                if key in OUTPUT_MAP:
+                    newkey = OUTPUT_MAP[key]
+                else:
+                    newkey = key.lower().replace(' ', '_')
+                # check if entry already exists?
+                output[newkey] = value
+
+        return output
+
+    def create_record_dict(self, record):
+        '''parse the swig created record and construct a dict from it'''
+
+        new_record = dict()
+        for key in [x for x in dir(record) if not (x.startswith('_') or x == 'this')]:
+            value = record.__getattr__(key)
+            if key == "event" and value in EVENT_MAP:
+                new_record[key] = EVENT_MAP[value]
+            elif key == "version":
+                # FIXME: out files should report log version?
+                # FIXME: or can we just deprecate v1 logs?
+                continue
+            elif key in NO_VALUE_MAP:
+                if NO_VALUE_MAP[key] == value:
+                    continue
+                else:
+                    new_record[key] = str(value)
+            elif record.__getattr__(key):
+                new_record[key] = str(value)
+
+        return new_record
+
+
+def find_testcases(testdir):
+    '''dig testcases out of passed directory'''
+
+    for f in os.listdir(testdir):
+        if f.endswith(".in"):
+            yield f[:-3]
+
+
+def main():
+    for f in find_testcases(TESTDIR):
+        def stub_test(self, testname=f):
+            self._runtest(testname)
+        stub_test.__doc__ = "test %s" % (f)
+        setattr(AAPythonBindingsTests, 'test_%s' % (f), stub_test)
+    return unittest.main(verbosity=2)
+
+if __name__ == "__main__":
+    main()


