[apparmor] [patch] add better loop support to common_test.py

Christian Boltz apparmor at cboltz.de
Tue Mar 31 13:28:33 UTC 2015


Hello,

Am Dienstag, 31. März 2015 schrieb Steve Beattie:
> Sorry for the delay in reviewing.

Being able to taunt about the pending reviews also has its advantages ;-))  
(and I'm quite sure you won't write something like "to keep the number 
of pending patches high..." again *eg*)

> On Fri, Mar 06, 2015 at 11:57:11PM +0100, Christian Boltz wrote:
...
> > Here's v3, this time with the number included in the filename ;-)
> 
> Unfortunately, this doesn't appear to work under python2:
> 
>   $ PYTHONPATH=.. python2 test-example.py
>   Traceback (most recent call last):
>     File "test-example.py", line 44, in <module>
>       setup_all_tests()
>     File "/home/steve/bzr/apparmor-master/utils/test/common_test.py",
> line 60, in setup_all_tests obj_instance = obj() # will fail for
> (non-test) classes with additional __init__ parameters File
> "/usr/lib/python2.7/unittest/case.py", line 189, in __init__
> (self.__class__, methodName))
>   ValueError: no such test method in <class 'common_test.AATest'>:
> runTest
> 
> I suspect this has to do with the differences in how objects are
> internally implemented between python 2 and python 3.

Actually not - it's a behaviour difference (or a missing bugfix?) in 
py2 unittest vs. py3 unittest. Just let me paste some code:


=== /usr/lib64/python3.4/unittest/case.py ===

class TestCase(object):
    # ...
    def __init__(self, methodName='runTest'):
        # ...
        try:
            testMethod = getattr(self, methodName)
        except AttributeError:
            if methodName != 'runTest':
                # we allow instantiation with no explicit method name
                # but not an *incorrect* or missing method name
                raise ValueError("no such test method in %s: %s" %
                      (self.__class__, methodName))


=== /usr/lib64/python2.7/unittest/case.py ===

class TestCase(object):
    # ...
    def __init__(self, methodName='runTest'):
        # ...
        try:
            testMethod = getattr(self, methodName)
        except AttributeError:
            raise ValueError("no such test method in %s: %s" %
                  (self.__class__, methodName))


The difference should be obvious ;-)


A simple solution would be to add a dummy

    def runTest(self):
        pass

to AATest. Besides incrementing the number of tests run, it doesn't seem 
to hurt - but it still feels dirty :-/


After some thinking (and reading documentation) - I noticed that 
isinstance() makes things more difficult difficult than needed.

The better way is using issubclass(), which means I don't need to 
create a temporary instance (which also means I don't need the 
try/except), and also don't hit the failure you noticed.


That said, here's v3 with setup_all_tests() changed:

[ 07-common_test_better_loop_support.diff ]

=== modified file 'utils/test/common_test.py'
--- utils/test/common_test.py   2014-11-06 20:32:49 +0000
+++ utils/test/common_test.py   2015-03-03 23:37:48 +0000
@@ -1,5 +1,6 @@
 # ----------------------------------------------------------------------
 #    Copyright (C) 2013 Kshitij Gupta <kgupta8592 at gmail.com>
+#    Copyright (C) 2015 Christian Boltz <apparmor at cboltz.de>
 #
 #    This program is free software; you can redistribute it and/or
 #    modify it under the terms of version 2 of the GNU General Public
@@ -12,6 +13,8 @@
 #
 # ----------------------------------------------------------------------
 import unittest
+import inspect
 import os
 import re
+import sys
 
@@ -34,6 +36,10 @@
     #    print("Please press the Y button on the keyboard.")
     #    self.assertEqual(apparmor.common.readkey().lower(), 'y', 'Error reading key from shell!')
 
+
+class AATest(unittest.TestCase):
+    tests = []
+
 class AAParseTest(unittest.TestCase):
     parse_function = None
 
@@ -44,6 +50,33 @@
             'parse object %s returned "%s", expected "%s"' \
             %(self.parse_function.__doc__, parsed.serialize(), rule))
 
+
+def setup_all_tests():
+    '''call setup_tests_loop() for each class in module_name'''
+    for name, obj in inspect.getmembers(sys.modules['__main__']):
+        if inspect.isclass(obj):
+            if issubclass(obj, unittest.TestCase):
+                setup_tests_loop(obj)
+
+def setup_tests_loop(test_class):
+    '''Create tests in test_class using test_class.tests and self._run_test()
+
+    test_class.tests should be tuples of (test_data, expected_results)
+    test_data and expected_results can be of any type as long as test_class._run_test()
+    know how to handle them.
+
+    A typical definition for _run_test() is:
+        def test_class._run_test(self, test_data, expected)
+        '''
+
+    for (i, (test_data, expected)) in enumerate(test_class.tests):
+        def stub_test(self, test_data=test_data, expected=expected):
+            self._run_test(test_data, expected)
+
+        stub_test.__doc__ = "test '%s'" % (test_data)
+        setattr(test_class, 'test_%d' % (i), stub_test)
+
+
 def setup_regex_tests(test_class):
     '''Create tests in test_class using test_class.tests and AAParseTest._test_parse_rule()
 
=== added file 'utils/test/test-example.py'
--- utils/test/test-example.py  1970-01-01 00:00:00 +0000
+++ utils/test/test-example.py  2015-03-03 21:05:18 +0000
@@ -0,0 +1,45 @@
+#! /usr/bin/env python
+# ------------------------------------------------------------------
+#
+#    Copyright (C) 2015 Christian Boltz <apparmor at cboltz.de>
+#
+#    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 unittest
+from common_test import AATest, setup_all_tests
+
+class TestFoo(AATest):
+    tests = [
+        (0,  0 ),
+        (42, 42),
+    ]
+
+    def _run_test(self, params, expected):
+        self.assertEqual(params, expected)
+
+class TestBar(AATest):
+    tests = [
+        ('a', 'foo'),
+        ('b', 'bar'),
+        ('c', 'baz'),
+    ]
+
+    def _run_test(self, params, expected):
+        self.assertNotEqual(params, expected)
+
+    def testAdditionalBarTest(self):
+        self.assertEqual(1, 1)
+
+class TestBaz(AATest):
+    def test_Baz_only_one_test(self):
+        self.assertEqual("baz", "baz")
+
+
+
+if __name__ == '__main__':
+    setup_all_tests()
+    unittest.main(verbosity=2)


> And while python 3 has gotten wider support and I would like to
> eventually drop support for python 2.7, I don't think we can justify
> doing so quite yet.

I agree, even if I currently don't have a py2 libapparmor around for 
testing.


Regards,

Christian Boltz
-- 
> rpmdb: PANIC: fatal region error detected; run recovery
Du wohnst nicht zufällig in Bielefeld?
[> Cornelia Böttge und Michael Raab in opensuse-de]




More information about the AppArmor mailing list