Ticket #6343: trac_6343-sage_object-test-nt.patch

File trac_6343-sage_object-test-nt.patch, 14.2 KB (added by nthiery, 10 years ago)
  • sage/misc/all.py

    # HG changeset patch
    # User Nicolas M. Thiery <nthiery@users.sf.net>
    # Date 1247264613 -7200
    # Node ID 7f4f37c7e122e890bbe53d17042ccd2113238ee2
    # Parent  319109b596f9a068302be54d76d0e3efd8d9010d
    #6343: Adds TestSuite(object).run() generic testing framework
    
    This patch implements TestSuite(object).run()  which runs
    systematic checks on the object. Here is a typical call:
    
         sage: TestSuite(ZZ).run(verbose = True)
         running ._test_an_element() ... done
         running ._test_element_pickling() ... done
         running ._test_not_implemented_methods() ... done
         running ._test_pickling() ... done
    
    In practice, TestSuite(o).run() runs all the methods named _test_* of the object o.
    
    The _test_* methods are typically implemented by abstract super classes
    and in particular via categories, in order to enforce standard
    behavior and API (_test_pickling, _test_an_element), or provide
    mathematical sanity checks (_test_associativity).
    
    For consistent error reporting, the _test_* methods in turn must use
    the new gadget sage.misc.sage_unittest.InstanceTester to actually
    run the tests.
    
    diff --git a/sage/misc/all.py b/sage/misc/all.py
    a b from randstate import seed, set_random_s 
    164164
    165165from prandom import *
    166166
     167from sage_unittest import TestSuite
     168
    167169from explain_pickle import explain_pickle, unpickle_newobj, unpickle_global, unpickle_build, unpickle_instantiate, unpickle_persistent, unpickle_extension, unpickle_appends
    168170
    169171##########################################################################
  • new file sage/misc/sage_unittest.py

    diff --git a/sage/misc/sage_unittest.py b/sage/misc/sage_unittest.py
    new file mode 100644
    - +  
     1r"""
     2Unit testing for Sage objects
     3"""
     4#*****************************************************************************
     5#  Copyright (C) 2009 Nicolas M. Thiery <nthiery at users.sf.net>
     6#
     7#  Distributed under the terms of the GNU General Public License (GPL)
     8#                  http://www.gnu.org/licenses/
     9#******************************************************************************
     10
     11##############################################################################
     12
     13import unittest
     14
     15class TestSuite(object):
     16    """
     17    Test suites for Sage objects.
     18
     19    EXAMPLES::
     20
     21        sage: TestSuite(ZZ).run()
     22
     23    No output means that all tests passed. Which tests?
     24    In practice this calls all the methods ``._test_*`` of this
     25    object, in alphabetic order::
     26
     27        sage: TestSuite(ZZ).run(verbose = True)
     28        running ._test_not_implemented_methods() ... done
     29        running ._test_pickling() ... done
     30
     31    Those methods methods are typically implemented by abstract
     32    super classes, in particular via categories, in order to
     33    enforce standard behavior and API, or provide mathematical
     34    sanity checks. For example if ``self`` is in the category of
     35    finite semigroups, this checks that the multiplication is
     36    associative (at least on some elements)::
     37
     38        sage: S = FiniteSemigroups().example(alphabet = ('a', 'b')) # todo: not implemented (comes with the category patches)
     39        sage: TestSuite(S).run(verbose = True)                      # todo: not implemented (comes with the category patches)
     40        running ._test_an_element() ... done
     41        running ._test_associativity() ... done
     42        running ._test_element_pickling() ... done
     43        running ._test_enumerated_set_contains() ... done
     44        running ._test_enumerated_set_iter_cardinality() ... done
     45        running ._test_enumerated_set_iter_list() ... done
     46        running ._test_not_implemented_methods() ... done
     47        running ._test_pickling() ... done
     48        running ._test_some_elements() ... done
     49
     50    The different test methods can be called independently::
     51
     52        sage: S._test_associativity()                   # todo: not implemented (comes with the category patches)
     53
     54    When meaningful, one can further customize on which elements
     55    the tests are run. Here, we use it to *prove* that the
     56    multiplication is indeed associative, by runing the test on
     57    all the elements::
     58
     59        sage: S._test_associativity(elements = S)       # todo: not implemented (comes with the category patches)
     60
     61    Adding a new test boils down to adding a new method in the
     62    class of the object or any superclass (e.g. in a
     63    category). This method should use the utility :meth:`._tester`
     64    to handle standard options and report test failures. See the
     65    code of :meth:`._test_an_element` for an example.
     66
     67    Eventually, every implementation of a :class:`SageObject`
     68    should run a :class:`TestSuite` on one of its instances in its doctest.
     69    (replacing the current ``loads(dumps(x))` tests).
     70
     71    TODO:
     72     - allow for customized behavior in case of failing assertion
     73       (warning, error, statistic accounting)
     74       This involves reimplementing the method sfail / failIf
     75       / ... of unittest.TestCase in InstanceTester
     76     - Improve integration with doctests (statistics on failing/passing tests)
     77     - Integration with unittest:
     78       Make TestSuite inherit from unittest.TestSuite?
     79       Make .run(...) accept a result object
     80     - Add some standard option ``proof = True``, asking for the
     81       test method to choose appropriately the elements so as to
     82       prove the desired property. The test method may assume that
     83       a parent implements properly all the super categories. For
     84       example, the test_commutative method of the category
     85       ``CommutativeSemigroups()`` may just check that the
     86       provided generators commute, implicitly assuming that
     87       generators indeed generate the semigroup (as required by
     88       ``Semigroups()``).
     89    """
     90
     91    def __init__(self, instance):
     92        """
     93        TESTS::
     94
     95            sage: TestSuite(ZZ)
     96            Test suite for Integer Ring
     97        """
     98        self._instance = instance
     99
     100    def __repr__(self):
     101        """
     102        TESTS::
     103
     104            sage: TestSuite(ZZ)
     105            Test suite for Integer Ring
     106        """
     107        return "Test suite for %s"%self._instance
     108       
     109
     110    def run(self, category = None, **options):
     111        """
     112        Run all the tests from this test suite:
     113
     114        EXAMPLES::
     115
     116            sage: TestSuite(ZZ).run()
     117
     118        We now use the ``verbose`` option::       
     119
     120            sage: TestSuite(ZZ).run(verbose = True)
     121            running ._test_not_implemented_methods() ... done
     122            running ._test_pickling() ... done
     123
     124        """
     125        tester = self._instance._tester(**options)
     126        for method_name in dir(self._instance):
     127            # Note: testunit usually looks for methods called test* , but we don't want to catch self.test() here!
     128            if method_name[0:6] == "_test_":
     129                # TODO: improve pretty printing
     130                # could use the doc string of the test method
     131                tester.info("running .%s() ..."%method_name, newline = False)
     132                getattr(self._instance, method_name)(tester = tester)
     133                tester.info("done")
     134
     135class InstanceTester(unittest.TestCase):
     136    def __init__(self, instance, elements = None, verbose = False, **options):
     137        """
     138        A gadget attached to an instance providing it with testing utilities.
     139
     140        EXAMPLES::
     141       
     142            sage: from sage.misc.sage_unittest import InstanceTester
     143            sage: InstanceTester(instance = ZZ, verbose = True, elements = [1,2,3])
     144            Testing utilities for Integer Ring
     145
     146        This is used by ``SageObject._tester``, which see::
     147
     148            sage: ZZ._tester()
     149            Testing utilities for Integer Ring
     150
     151        """
     152        self._instance = instance
     153        self._verbose = verbose
     154        self._elements = elements
     155   
     156    def runTest(self):
     157        """
     158        Trivial implementation of :meth:`unittest.TestCase.runTest` to
     159        please the super class :class:`TestCase`. That's the price to
     160        pay for abusively inheriting from it.
     161
     162            sage: from sage.misc.sage_unittest import InstanceTester
     163            sage: tester = InstanceTester(ZZ, verbose = True)
     164            sage: tester.runTest()
     165           
     166        """
     167        pass
     168
     169    def info(self, message, newline = True):
     170        """
     171        Displays user information
     172
     173        EXAMPLES::
     174
     175            sage: from sage.misc.sage_unittest import InstanceTester
     176            sage: tester = InstanceTester(ZZ, verbose = True)
     177
     178            sage: tester.info("hello"); tester.info("world")
     179            hello
     180            world
     181
     182            sage: tester = InstanceTester(ZZ, verbose = False)
     183            sage: tester.info("hello"); tester.info("world")
     184
     185            sage: tester = InstanceTester(ZZ, verbose = True)
     186            sage: tester.info("hello", newline = False); tester.info("world")
     187            hello world
     188
     189           
     190        """
     191        if self._verbose:
     192            if newline:
     193                print message
     194            else:
     195                print message,           
     196
     197    def __repr__(self):
     198        """
     199        EXAMPLES::
     200
     201            sage: from sage.misc.sage_unittest import InstanceTester
     202            sage: InstanceTester(ZZ, verbose = True)
     203            Testing utilities for Integer Ring
     204
     205        """
     206        return "Testing utilities for %s"%self._instance
     207
     208
     209    def some_elements(self):
     210        """
     211        Returns a list (or iterable) of elements of ``self`` on which
     212        the tests should be run. This is only meaningful for container
     213        objects like parents.
     214
     215        By default, this calls :meth:`.some_elements`::
     216
     217            sage: from sage.misc.sage_unittest import InstanceTester
     218            sage: class MyParent(Parent):
     219            ...       def some_elements(self):
     220            ...           return [1,2,3,4,5]
     221            ...
     222            sage: tester = InstanceTester(MyParent())
     223            sage: list(tester.some_elements())
     224            [1, 2, 3, 4, 5]
     225
     226            sage: tester = InstanceTester(ZZ, elements = [1,3,5])
     227            sage: list(tester.some_elements())
     228            [1, 3, 5]
     229
     230        """
     231        if self._elements is None:
     232            return self._instance.some_elements()
     233        else:
     234            return self._elements
  • sage/structure/sage_object.pyx

    diff --git a/sage/structure/sage_object.pyx b/sage/structure/sage_object.pyx
    a b cdef class SageObject: 
    210210
    211211##     def _set_category(self, C):
    212212##         self.__category = C
    213    
     213
     214    #############################################################################
     215    # Test framework
     216    #############################################################################
     217
     218
     219    def _test_not_implemented_methods(self, **options):
     220        """
     221        Checks that all required methods for this object are implemented
     222
     223        TESTS::
     224           
     225            sage: class Abstract(SageObject):
     226            ...       @abstract_method
     227            ...       def bla(self):
     228            ...           "returns bla"
     229            ...
     230            sage: class Concrete(Abstract):
     231            ...       def bla(self):
     232            ...           return 1
     233            ...
     234            sage: class IncompleteConcrete(Abstract):
     235            ...       pass
     236            sage: Concrete()._test_not_implemented_methods()
     237            sage: IncompleteConcrete()._test_not_implemented_methods()
     238            Traceback (most recent call last):
     239            ...
     240            AssertionError: Not implemented method: bla
     241
     242        """
     243        from sage.misc.abstract_method import AbstractMethod
     244        tester = self._tester(**options)
     245        for name in dir(self):
     246            # This tries to reduce the occasions to trigger the
     247            # calculation of a lazy attribute
     248            # This won't work for classes using the getattr trick
     249            # to pseudo inherit from category
     250            if hasattr(self.__class__, name):
     251                f = getattr(self.__class__, name)
     252                if not isinstance(f, AbstractMethod):
     253                    continue
     254            try:
     255                # self.name may have been set explicitly in self.__dict__
     256                getattr(self, name)
     257            except AttributeError:
     258                pass
     259            except NotImplementedError:
     260                tester.fail("Not implemented method: %s"%name)
     261
     262    def _test_pickling(self, **options):
     263        """
     264        Checks that this object can be pickled and unpickled properly.
     265
     266        SEE ALSO: :func:`dumps` :func:`loads`
     267
     268        TODO: for a stronger test, this could send the object to a
     269        remote Sage session, and get it back.
     270        """
     271        tester = self._tester(**options)
     272        from sage.misc.all import loads, dumps
     273        tester.assertEqual(loads(dumps(self)), self)
     274
     275    def _tester(self, tester = None, **options):
     276        """
     277        Returns a gadget attached to ``self`` providing testing utilities
     278
     279        This is used by :class:`sage.misc.sage_unittest.TestSuite` and the _test_* methods.
     280
     281        EXAMPLES::
     282
     283            sage: tester = ZZ._tester()
     284
     285            sage: tester.assert_(1 == 1)
     286            sage: tester.assert_(1 == 0)
     287            Traceback (most recent call last):
     288            ...
     289            AssertionError
     290            sage: tester.assert_(1 == 0, "this is expected to fail")
     291            Traceback (most recent call last):
     292            ...
     293            AssertionError: this is expected to fail
     294
     295            sage: tester.assertEquals(1, 1)
     296            sage: tester.assertEquals(1, 0)
     297            Traceback (most recent call last):
     298            ...
     299            AssertionError: 1 != 0
     300
     301        The available assertion testing facilities are the same as in
     302        :class:`unittest.TestCase`, which see (actually, by a slight
     303        abuse, tester is currently an instance of this class).
     304
     305        TESTS:
     306
     307            sage: ZZ._tester(tester = tester) is tester
     308            True
     309        """
     310        if tester is None:
     311            from sage.misc.sage_unittest import InstanceTester
     312            return InstanceTester(self, **options)
     313        else:
     314            assert len(options) == 0
     315            assert tester._instance is self
     316            return tester
    214317
    215318    #############################################################################
    216319    # Coercions to interface objects