This almost completes the switch to pytest. There are two missing things:
- the details of test results (--with-xunit) are not read correctly by Jenkins. I have a theory I'm investigating here.
- the beakerlib integration is still not ready


I'll not be available for the rest of the week so I'm sending this early, in case someone wants to take a look.

--
PetrĀ³
From e48a4a4a38a107a463a1c0c14c35254abbace36b Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 9 Oct 2014 17:02:25 +0200
Subject: [PATCH] dogtag plugin: Don't use doctest syntax for non-doctest
 examples

---
 ipaserver/plugins/dogtag.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 0e141a45c290b84d65b15b8c2c638577a3a39363..4576c9113b1501f9ab32aef16f8be761e92a9806 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -163,16 +163,16 @@
 1. Reading a serial number from CMS requires conversion from hexadecimal
    by converting it into a Python int or long object, use the int constructor:
 
-   >>> serial_number = int(serial_number, 16)
+        serial_number = int(serial_number, 16)
 
 2. Big integers passed to XMLRPC must be decimal unicode strings
 
-   >>> unicode(serial_number)
+       unicode(serial_number)
 
 3. Big integers received from XMLRPC must be converted back to int or long
    objects from the decimal string representation.
 
-   >>> serial_number = int(serial_number)
+       serial_number = int(serial_number)
 
 Xpath pattern matching on node names:
 -------------------------------------
@@ -202,7 +202,7 @@
 solve the chapter problem above is by using a predicate which says if the node
 name begins with 'chapter' it's a match. Here is how you can do that.
 
-    >>> doc.xpath("//book/*[starts-with(name(), 'chapter')]/section[2]")
+        doc.xpath("//book/*[starts-with(name(), 'chapter')]/section[2]")
 
 The built-in starts-with() returns true if its first argument starts with its
 second argument. Thus the example above says if the node name of the second
@@ -219,10 +219,10 @@
 EXSLT regular expression match() function on the node name. Here is how this is
 done:
 
-    >>> regexpNS = "http://exslt.org/regular-expressions";
-    >>> find = etree.XPath("//book/*[re:match(name(), '^chapter(_\d+)$')]/section[2]",
-    ...                    namespaces={'re':regexpNS}
-    >>> find(doc)
+        regexpNS = "http://exslt.org/regular-expressions";
+        find = etree.XPath("//book/*[re:match(name(), '^chapter(_\d+)$')]/section[2]",
+                           namespaces={'re':regexpNS}
+        find(doc)
 
 What is happening here is that etree.XPath() has returned us an evaluator
 function which we bind to the name 'find'. We've passed it a set of namespaces
-- 
2.1.0

From c115cf7fb1a9c9f4b0f3c02982c02f4a07a6f404 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 9 Oct 2014 17:03:02 +0200
Subject: [PATCH] Configure pytest to run doctests

---
 ipatests/pytest.ini |  2 --
 pytest.ini          | 13 +++++++++++++
 2 files changed, 13 insertions(+), 2 deletions(-)
 delete mode 100644 ipatests/pytest.ini
 create mode 100644 pytest.ini

diff --git a/ipatests/pytest.ini b/ipatests/pytest.ini
deleted file mode 100644
index d2355d9616a2aea000d14fa27c9b35d0ad5fb353..0000000000000000000000000000000000000000
--- a/ipatests/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-python_classes = test_ Test
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..fc5ab12cc45beca44a47cdfb9b2e8339c978bc02
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,13 @@
+[pytest]
+python_classes = test_ Test
+addopts = --doctest-modules
+            # Ignore files for doc tests.
+            # TODO: ideally, these should all use __name__=='__main__' guards
+          --ignore=setup.py
+          --ignore=setup-client.py
+          --ignore=checks/check-ra.py
+          --ignore=daemons/ipa-otpd/test.py
+          --ignore=doc/examples/python-api.py
+          --ignore=install/share/copy-schema-to-ca.py
+          --ignore=install/share/wsgi.py
+          --ignore=ipapython/py_default_encoding/setup.py
-- 
2.1.0

From 1ff7fcc4e8a075c3aad0774ec5ba77e433177c10 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 10 Oct 2014 09:39:00 +0200
Subject: [PATCH] Declarative tests: Move cleanup to setup_class/teardown_class

---
 ipatests/test_xmlrpc/xmlrpc_test.py | 29 +++++++++++++++++------------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/ipatests/test_xmlrpc/xmlrpc_test.py b/ipatests/test_xmlrpc/xmlrpc_test.py
index 1f44f7794bdd390153317b08b396ac3dfc09e150..306d66f715b504c51bf3189aee7d406ceddb8c36 100644
--- a/ipatests/test_xmlrpc/xmlrpc_test.py
+++ b/ipatests/test_xmlrpc/xmlrpc_test.py
@@ -252,6 +252,18 @@ class Declarative(XMLRPC_test):
     cleanup_commands = tuple()
     tests = tuple()
 
+    @classmethod
+    def setup_class(cls):
+        super(Declarative, cls).setup_class()
+        for command in cls.cleanup_commands:
+            cls.cleanup(command)
+
+    @classmethod
+    def teardown_class(cls):
+        for command in cls.cleanup_commands:
+            cls.cleanup(command)
+        super(Declarative, cls).teardown_class()
+
     def cleanup_generate(self, stage):
         for (i, command) in enumerate(self.cleanup_commands):
             func = lambda: self.cleanup(command)
@@ -260,15 +272,18 @@ def cleanup_generate(self, stage):
             )
             yield (func,)
 
-    def cleanup(self, command):
+    @classmethod
+    def cleanup(cls, command):
         (cmd, args, options) = command
+        print 'Cleanup:', cmd, args, options
         if cmd not in api.Command:
             raise nose.SkipTest(
                 'cleanup command %r not in api.Command' % cmd
             )
         try:
             api.Command[cmd](*args, **options)
-        except (errors.NotFound, errors.EmptyModlist):
+        except (errors.NotFound, errors.EmptyModlist) as e:
+            print e
             pass
 
     def test_generator(self):
@@ -277,12 +292,6 @@ def test_generator(self):
 
         nose reports each one as a seperate test.
         """
-
-        # Iterate through pre-cleanup:
-        for tup in self.cleanup_generate('pre'):
-            yield tup
-
-        # Iterate through the tests:
         name = self.__class__.__name__
         for (i, test) in enumerate(self.tests):
             if callable(test):
@@ -299,10 +308,6 @@ def test_generator(self):
                 func.description = nice
             yield (func,)
 
-        # Iterate through post-cleanup:
-        for tup in self.cleanup_generate('post'):
-            yield tup
-
     def check(self, nice, desc, command, expected, extra_check=None):
         (cmd, args, options) = command
         options.setdefault('version', self.default_version)
-- 
2.1.0

From c1556dcdc098469f77de4e125c9d6d2c0e7f9c57 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 10 Oct 2014 09:47:25 +0200
Subject: [PATCH] Declarative tests: Switch to pytest

Provide a local pytest plugin to generate tests.

The Declarative tests can now only be run with pytest
---
 ipatests/test_xmlrpc/conftest.py    | 47 +++++++++++++++++++++++++++++++++++++
 ipatests/test_xmlrpc/xmlrpc_test.py | 26 ++++++--------------
 2 files changed, 54 insertions(+), 19 deletions(-)
 create mode 100644 ipatests/test_xmlrpc/conftest.py

diff --git a/ipatests/test_xmlrpc/conftest.py b/ipatests/test_xmlrpc/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f88bb850d4e0fd43e0a8122ac042c86bc1e8a10
--- /dev/null
+++ b/ipatests/test_xmlrpc/conftest.py
@@ -0,0 +1,47 @@
+# Authors:
+#   Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2011  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Pytest plugin for Declarative tests"""
+
+
+def pytest_generate_tests(metafunc):
+    """Generates Declarative tests"""
+    if 'declarative_test_definition' in metafunc.fixturenames:
+        name = metafunc.cls.__name__
+        tests = []
+        descriptions = []
+        for i, test in enumerate(metafunc.cls.tests):
+            if callable(test):
+                description = '%d: %s' % (
+                    i,
+                    test.__name__,  # test is not a dict. pylint: disable=E1103
+                )
+            else:
+                description = '%d: %s: %s' % (i,
+                                              test['command'][0],
+                                              test.get('desc', ''))
+                test = dict(test)
+                test['nice'] = description
+            tests.append(test)
+            descriptions.append(description)
+        metafunc.parametrize(
+            ['index', 'declarative_test_definition'],
+            enumerate(tests),
+            ids=descriptions,
+        )
diff --git a/ipatests/test_xmlrpc/xmlrpc_test.py b/ipatests/test_xmlrpc/xmlrpc_test.py
index 306d66f715b504c51bf3189aee7d406ceddb8c36..59b023df936c2d042a319c177d8047a6d460956e 100644
--- a/ipatests/test_xmlrpc/xmlrpc_test.py
+++ b/ipatests/test_xmlrpc/xmlrpc_test.py
@@ -286,27 +286,15 @@ def cleanup(cls, command):
             print e
             pass
 
-    def test_generator(self):
-        """
-        Iterate through tests.
+    def test_command(self, index, declarative_test_definition):
+        """Run an individual test
 
-        nose reports each one as a seperate test.
+        The arguments are provided by the pytest plugin.
         """
-        name = self.__class__.__name__
-        for (i, test) in enumerate(self.tests):
-            if callable(test):
-                func = lambda: test(self)
-                nice = '%s[%d]: call %s: %s' % (
-                    name, i, test.__name__, test.__doc__
-                )
-                func.description = nice
-            else:
-                nice = '%s[%d]: %s: %s' % (
-                    name, i, test['command'][0], test.get('desc', '')
-                )
-                func = lambda: self.check(nice, **test)
-                func.description = nice
-            yield (func,)
+        if callable(declarative_test_definition):
+            declarative_test_definition(self)
+        else:
+            self.check(**declarative_test_definition)
 
     def check(self, nice, desc, command, expected, extra_check=None):
         (cmd, args, options) = command
-- 
2.1.0

From 4a6070632e764c4b00905c8223a9aa8c800c6865 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 10 Oct 2014 14:56:29 +0200
Subject: [PATCH] Integration: Port the ordering plugin to pytest

Ordered integration tests may now be run with pytest.
---
 freeipa.spec.in                            |  1 +
 ipatests/order_plugin.py                   |  2 +
 ipatests/test_integration/conftest.py      | 69 ++++++++++++++++++++++++++++++
 ipatests/test_integration/test_ordering.py | 54 +++++++++++++++++++++++
 4 files changed, 126 insertions(+)
 create mode 100644 ipatests/test_integration/conftest.py
 create mode 100644 ipatests/test_integration/test_ordering.py

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 008494d8bb37516981e0abd845bbc0001c6cf5a9..200fa07b826153a3e2730e8ba8a733d3c065a4c7 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -290,6 +290,7 @@ Requires: %{name}-python = %{version}-%{release}
 Requires: tar
 Requires: xz
 Requires: python-nose
+Requires: pytest
 Requires: python-paste
 Requires: python-coverage
 Requires: python-polib
diff --git a/ipatests/order_plugin.py b/ipatests/order_plugin.py
index 7b114a56783a6b3f2e34498567a61c2668ab5c08..6f6e7dc4f462b700f68113484bd6e93021d187e8 100644
--- a/ipatests/order_plugin.py
+++ b/ipatests/order_plugin.py
@@ -26,6 +26,7 @@
 from nose.plugins import Plugin
 import nose.loader
 import nose.util
+import pytest
 
 
 def ordered(cls):
@@ -41,6 +42,7 @@ def ordered(cls):
     cls._order_plugin__ordered = True
     assert not isinstance(cls, unittest.TestCase), (
         "A unittest.TestCase may not be ordered.")
+    cls = pytest.mark.source_order(cls)
     return cls
 
 
diff --git a/ipatests/test_integration/conftest.py b/ipatests/test_integration/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..6eac6b831effe322efae049fc3564467d6e233c9
--- /dev/null
+++ b/ipatests/test_integration/conftest.py
@@ -0,0 +1,69 @@
+# Authors:
+#   Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Pytest plugin for IPA
+
+Adds support for the @pytest.mark.source_order decorator which,
+when applied to a class, runs the test methods in source order.
+
+See test_ordering for an example.
+"""
+
+import pytest
+
+
+def decorate_items(items):
+    node_indexes = {}
+    for index, item in enumerate(items):
+        try:
+            func = item.function
+        except AttributeError:
+            yield (index, ), item
+            continue
+
+        key = (index, )
+        for node in reversed(item.listchain()):
+            # Find the corresponding class
+            if isinstance(node, pytest.Class):
+                cls = node.cls
+            else:
+                continue
+            if getattr(cls, '_order_plugin__ordered', False):
+                node_index = node_indexes.setdefault(node, index)
+                # Find first occurence of the method in class hierarchy
+                for i, parent_class in enumerate(reversed(cls.mro())):
+                    if getattr(parent_class, '_order_plugin__ordered', False):
+                        method = getattr(parent_class, func.__name__, None)
+                        if method:
+                            # Sort methods as tuples  (position of the class
+                            # in the inheritance chain, position of the method
+                            # within that class)
+                            key = (node_index, 0,
+                                   i, method.func_code.co_firstlineno, node)
+                            break
+                else:
+                    # Weird case fallback
+                    # Method name not in any of the classes in MRO, run it last
+                    key = node_index, 1, func.func_code.co_firstlineno, node
+                break
+        yield key, item
+
+
+def pytest_collection_modifyitems(session, config, items):
+    items[:] = [item for i, item in sorted(decorate_items(items))]
diff --git a/ipatests/test_integration/test_ordering.py b/ipatests/test_integration/test_ordering.py
new file mode 100644
index 0000000000000000000000000000000000000000..53aadc4935b49c758410fe95138528b5603b3176
--- /dev/null
+++ b/ipatests/test_integration/test_ordering.py
@@ -0,0 +1,54 @@
+# Authors:
+#   Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the ordering of tests
+
+IPA integration tests, marked with `@ordered`, require tests to be run
+in a specific order:
+- Base classes first
+- Within a class, test methods are ordered according to source line
+"""
+
+from ipatests.order_plugin import ordered
+
+
+@ordered
+class TestBase(object):
+    @classmethod
+    def setup_class(cls):
+        cls.value = 'unchanged'
+
+    def test_d_first(self):
+        type(self).value = 'changed once'
+
+
+class TestChild(TestBase):
+    def test_b_third(self):
+        assert type(self).value == 'changed twice'
+        type(self).value = 'changed thrice'
+
+    def test_a_fourth(self):
+        assert type(self).value == 'changed thrice'
+
+
+def test_c_second(self):
+    assert type(self).value == 'changed once'
+    type(self).value = 'changed twice'
+TestBase.test_c_second = test_c_second
+del test_c_second
-- 
2.1.0

From c379f7f4f7e86eb875263b57cc75e0da1c6cad64 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 10 Oct 2014 16:04:05 +0200
Subject: [PATCH] test_webui: Don't use __init__ for test classes

---
 ipatests/test_webui/test_cert.py  |  4 ++--
 ipatests/test_webui/test_dns.py   |  4 ++--
 ipatests/test_webui/test_host.py  |  4 ++--
 ipatests/test_webui/test_trust.py |  5 ++---
 ipatests/test_webui/ui_driver.py  | 27 ++++++++-------------------
 5 files changed, 16 insertions(+), 28 deletions(-)

diff --git a/ipatests/test_webui/test_cert.py b/ipatests/test_webui/test_cert.py
index 979a51e84437e7c8dae8de5412095c1e719e7db4..15d1f31f8be1d633a8a878d2ac52c5ca4a2f9eca 100644
--- a/ipatests/test_webui/test_cert.py
+++ b/ipatests/test_webui/test_cert.py
@@ -29,8 +29,8 @@
 
 class test_cert(UI_driver):
 
-    def __init__(self, *args, **kwargs):
-        super(test_cert, self).__init__(args, kwargs)
+    def setup(self):
+        super(test_cert, self).setup()
 
         if not self.has_ca():
             self.skip('CA not configured')
diff --git a/ipatests/test_webui/test_dns.py b/ipatests/test_webui/test_dns.py
index baa5bfbb157b30ad9e1445ca3dfc3af1a9573de2..1fd9dd2f88de036ad14b535742d888c05c70fb4b 100644
--- a/ipatests/test_webui/test_dns.py
+++ b/ipatests/test_webui/test_dns.py
@@ -87,8 +87,8 @@
 
 class test_dns(UI_driver):
 
-    def __init__(self, *args, **kwargs):
-        super(test_dns, self).__init__(args, kwargs)
+    def setup(self):
+        super(test_dns, self).setup()
 
         if not self.has_dns():
             self.skip('DNS not configured')
diff --git a/ipatests/test_webui/test_host.py b/ipatests/test_webui/test_host.py
index 4b1d4201225e9630993c7116018d5aae5c61ec14..446744ef1da5c6d6d0e934500fa237c753578120 100644
--- a/ipatests/test_webui/test_host.py
+++ b/ipatests/test_webui/test_host.py
@@ -34,8 +34,8 @@
 
 class host_tasks(UI_driver):
 
-    def __init__(self, *args, **kwargs):
-        super(host_tasks, self).__init__(args, kwargs)
+    def setup(self):
+        super(host_tasks, self).setup()
         self.prep_data()
         self.prep_data2()
 
diff --git a/ipatests/test_webui/test_trust.py b/ipatests/test_webui/test_trust.py
index 95e0fedda54722e7d9a7e82e238c10d0a127e4ad..ad07884b494de44312d4e47cca55eee2b8bfee8a 100644
--- a/ipatests/test_webui/test_trust.py
+++ b/ipatests/test_webui/test_trust.py
@@ -95,9 +95,8 @@ def get_range_name(self):
 
 class test_trust(trust_tasks):
 
-    def __init__(self, *args, **kwargs):
-        super(test_trust, self).__init__(args, kwargs)
-
+    def setup(self):
+        super(test_trust, self).setup()
         if not self.has_trusts():
             self.skip('Trusts not configured')
 
diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index e706a68e0e55024a835e5be3071cccfeee312a1f..b50c045a4186b7aaa93ed10320c0b7e2334cda51 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -114,15 +114,18 @@ class UI_driver(object):
 
     @classmethod
     def setup_class(cls):
+        pass
         if NO_SELENIUM:
             raise nose.SkipTest('Selenium not installed')
 
-    def __init__(self, driver=None, config=None):
+    def setup(self):
         self.request_timeout = 30
-        self.driver = driver
-        self.config = config
-        if not config:
-            self.load_config()
+        self.load_config()
+        self.driver = self.get_driver()
+        self.driver.maximize_window()
+
+    def teardown(self):
+        self.driver.quit()
 
     def load_config(self):
         """
@@ -161,20 +164,6 @@ def load_config(self):
         if 'type' not in c:
             c['type'] = DEFAULT_TYPE
 
-    def setup(self):
-        """
-        Test setup
-        """
-        if not self.driver:
-            self.driver = self.get_driver()
-            self.driver.maximize_window()
-
-    def teardown(self):
-        """
-        Test clean up
-        """
-        self.driver.quit()
-
     def get_driver(self):
         """
         Get WebDriver according to configuration
-- 
2.1.0

From d71f0500f9126ec9a81368f3bf52c5ccf0cef3c0 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 10 Oct 2014 17:15:52 +0200
Subject: [PATCH] Switch make-test to pytest

The unused capability to run on multiple Python versions is removed,
and needed arguments are now listed in pytest.ini,
leaving just a simple call to the actual test runner.
---
 make-test | 63 +++------------------------------------------------------------
 1 file changed, 3 insertions(+), 60 deletions(-)

diff --git a/make-test b/make-test
index 2a48ab18181fe313b413b8c64f300533215380bc..df4e1ad92721fc7c0d2d20f4dae87d4b47cd8ccd 100755
--- a/make-test
+++ b/make-test
@@ -1,61 +1,4 @@
-#!/usr/bin/python2
+#! /bin/bash
 
-"""
-Run IPA unit tests under multiple versions of Python (if present).
-"""
-
-import sys
-import optparse
-import os
-from os import path
-from subprocess import call
-
-versions = ('2.4', '2.5', '2.6', '2.7')
-python = '/usr/bin/python'
-nose = '/usr/bin/nosetests'
-ran = []
-fail = []
-
-cmd = [
-    nose,
-    '-v',
-    '--with-doctest',
-    '--doctest-tests',
-    '--exclude=plugins',
-]
-cmd += sys.argv[1:]
-
-
-# This must be set so ipalib.api gets initialized property for tests:
-os.environ['IPA_UNIT_TEST_MODE'] = 'cli_test'
-
-# Add in-tree client binaries to PATH
-os.environ['PATH'] = './ipa-client:' + os.environ['PATH']
-
-if not path.isfile(nose):
-    print 'ERROR: need %r' % nose
-    sys.exit(100)
-for v in versions:
-    pver = python + v
-    if not path.isfile(pver):
-        continue
-    command = [pver] + cmd
-    print ' '.join(cmd)
-    if 0 != call(cmd):
-        fail.append(pver)
-    ran.append(pver)
-
-
-print '=' * 70
-for pver in ran:
-    if pver in fail:
-        print 'FAILED under %r' % pver
-    else:
-        print 'passed under %r' % pver
-print ''
-if fail:
-    print '** FAIL **'
-    sys.exit(1)
-else:
-    print '** pass **'
-    sys.exit(0)
+set -ex
+IPA_UNIT_TEST_MODE=cli_test py.test "$@"
-- 
2.1.0

From 09c7c56583ebabe2e866baace9cc618414ef5685 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Mon, 13 Oct 2014 13:27:18 +0200
Subject: [PATCH] Add local pytest plugin for --with-xunit and --logging-level

The --with-xunit option ihas the same behavior as in nosetests:
it's an alias for pytest's --junitxml=nosetests.py

The --logging-level option enables direct IPA logging to stdout.
---
 ipatests/conftest.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 73 insertions(+)
 create mode 100644 ipatests/conftest.py

diff --git a/ipatests/conftest.py b/ipatests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..042e941f7ac9acf3441f654fc64a91d3f0bae9a7
--- /dev/null
+++ b/ipatests/conftest.py
@@ -0,0 +1,73 @@
+# Authors:
+#   Petr Viktorin <pvikt...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""pytest plugin for IPA tests"""
+
+import os
+import sys
+import logging
+
+from ipapython.ipa_log_manager import log_mgr
+
+
+def pytest_addoption(parser):
+    group = parser.getgroup("IPA nosetests compatibility shim")
+
+    group.addoption('--with-xunit', action="store_const",
+           dest="xmlpath", metavar="path",  default=None,
+           const=os.environ.get('IPATEST_XUNIT_PATH', './nosetests.xml'),
+           help="create junit-xml style report file at $IPATEST_XUNIT_PATH,"
+                "or nosetests.xml by default")
+
+    group.addoption('--logging-level', action="store",
+           dest="logging_level", metavar="level", default='CRITICAL',
+           help="level for logging to stderr. "
+                "Bypasses pytest logging redirection."
+                "May be used to show progress of long-running tests.")
+
+
+def pytest_configure(config):
+    if config.getoption('logging_level'):
+        # Forward IPA logging to a normal Python logger. Nose's logcapture plugin
+        # can't work with IPA-managed loggers
+        class LogHandler(logging.Handler):
+            name = 'forwarding log handler'
+            logger = logging.getLogger('IPA')
+
+            def emit(self, record):
+                capture = config.pluginmanager.getplugin('capturemanager')
+                orig_stdout, orig_stderr = sys.stdout, sys.stderr
+                if capture:
+                    capture._capturing.suspend_capturing()
+                sys.stderr.write(self.format(record))
+                sys.stderr.write('\n')
+                if capture:
+                    capture._capturing.resume_capturing()
+                sys.stdout, sys.stderr = orig_stdout, orig_stderr
+
+        log_mgr.configure(
+            {
+                'default_level': config.getoption('logging_level'),
+                'handlers': [{'log_handler': LogHandler(),
+                            'format': '[%(name)s] %(message)s',
+                            'level': 'debug'},
+                            {'level': 'debug',
+                            'name': 'real_stderr',
+                            'stream': sys.stderr}]},
+            configure_state='tests')
-- 
2.1.0

From 7bc1bb32d5036886d25b5a3f4076d35c34542c7b Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Mon, 13 Oct 2014 15:02:38 +0200
Subject: [PATCH] Include pytest.ini in built packages

The pytest.ini file needs to be in or above the directory py.test is called in.
When in IPA project root, this invocation will find ./ipatests/pytest.ini:
    py.test ipatests/
but these will not (they're equivalent):
    py.test .
    py.test
So pytest.ini must be in the project root.

However, setupttols can't include files outside package directories,
so we also need this file to be under ipatests/

Solve the problem by symlinking ./pytest.ini to ipatests/pytest.ini.
---
 ipatests/pytest.ini  | 19 +++++++++++++++++++
 ipatests/setup.py.in |  1 +
 pytest.ini           | 14 +-------------
 3 files changed, 21 insertions(+), 13 deletions(-)
 create mode 100644 ipatests/pytest.ini
 mode change 100644 => 120000 pytest.ini

diff --git a/ipatests/pytest.ini b/ipatests/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..0c568b6221d84a8d52b24c4cfe5e06deaa065e74
--- /dev/null
+++ b/ipatests/pytest.ini
@@ -0,0 +1,19 @@
+# pytest configuration
+
+# This file lives in ipatests/pytest.ini, so it can be included by setup.py,
+# and it's simplinked from the project's root directory, so py.test finds it
+# when called with no arguments.
+
+[pytest]
+python_classes = test_ Test
+addopts = --doctest-modules
+            # Ignore files for doc tests.
+            # TODO: ideally, these should all use __name__=='__main__' guards
+          --ignore=setup.py
+          --ignore=setup-client.py
+          --ignore=checks/check-ra.py
+          --ignore=daemons/ipa-otpd/test.py
+          --ignore=doc/examples/python-api.py
+          --ignore=install/share/copy-schema-to-ca.py
+          --ignore=install/share/wsgi.py
+          --ignore=ipapython/py_default_encoding/setup.py
diff --git a/ipatests/setup.py.in b/ipatests/setup.py.in
index 7f041465c132b6a5ab941ba253c2fb6e011b458e..6298b3b022bd62456330e3882d6085e2f7f887c0 100644
--- a/ipatests/setup.py.in
+++ b/ipatests/setup.py.in
@@ -78,6 +78,7 @@ def setup_package():
                         "ipatests.test_xmlrpc"],
             scripts=['ipa-run-tests', 'ipa-test-config', 'ipa-test-task'],
             package_data = {
+                'ipatests': ['pytest.ini'],
                 'ipatests.test_install': ['*.update'],
                 'ipatests.test_integration': ['scripts/*'],
                 'ipatests.test_pkcs10': ['*.csr'],
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index fc5ab12cc45beca44a47cdfb9b2e8339c978bc02..0000000000000000000000000000000000000000
--- a/pytest.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[pytest]
-python_classes = test_ Test
-addopts = --doctest-modules
-            # Ignore files for doc tests.
-            # TODO: ideally, these should all use __name__=='__main__' guards
-          --ignore=setup.py
-          --ignore=setup-client.py
-          --ignore=checks/check-ra.py
-          --ignore=daemons/ipa-otpd/test.py
-          --ignore=doc/examples/python-api.py
-          --ignore=install/share/copy-schema-to-ca.py
-          --ignore=install/share/wsgi.py
-          --ignore=ipapython/py_default_encoding/setup.py
diff --git a/pytest.ini b/pytest.ini
new file mode 120000
index 0000000000000000000000000000000000000000..e87991b0fd75787ca830a213d15c0634af811dff
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1 @@
+ipatests/pytest.ini
\ No newline at end of file
-- 
2.1.0

From 536831868928a36c1ad1e032cdde8dbeb1630b5a Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Mon, 13 Oct 2014 14:34:53 +0200
Subject: [PATCH] Use pytest in ipa-run-tests

---
 ipatests/ipa-run-tests | 56 +++++++++-----------------------------------------
 1 file changed, 10 insertions(+), 46 deletions(-)

diff --git a/ipatests/ipa-run-tests b/ipatests/ipa-run-tests
index 7e3270b41e28d9c75473cfdb3f8c36ef016509a0..53fa7b3218e7aad54f065c5042d3a4bef017fcd5 100755
--- a/ipatests/ipa-run-tests
+++ b/ipatests/ipa-run-tests
@@ -20,62 +20,26 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-"""Nose wrapper for running an installed (not in-tree) IPA test suite
+"""Pytest wrapper for running an installed (not in-tree) IPA test suite
 
-Any command-line arguments are passed directly to Nose.
-Note that any relative paths given will be based on the ipatests module's path
+Any command-line arguments are passed directly to py.test.
+The current directory is changed to the locaition of the ipatests package,
+so any relative paths given will be based on the ipatests module's path
 """
 
-import sys
 import os
-from os import path
-import logging
+import sys
 
-import nose
+import pytest
 
-from ipapython.ipa_log_manager import log_mgr
 import ipatests
-from ipatests.beakerlib_plugin import BeakerLibPlugin
-from ipatests.order_plugin import OrderTests
-
-cmd = [
-    sys.argv[0],
-    '--with-doctest',
-    '--doctest-tests',
-    '--with-ordered-tests',
-    '--exclude=plugins',
-    '--nologcapture',
-    '--logging-filter=-paramiko',
-    '--where', os.path.dirname(ipatests.__file__),
-]
-cmd += sys.argv[1:]
-
 
 # This must be set so ipalib.api gets initialized property for tests:
 os.environ['IPA_UNIT_TEST_MODE'] = 'cli_test'
 
+# This is set to store --with-xunit report in an accessible place:
+os.environ['IPATEST_XUNIT_PATH'] = os.path.join(os.getcwd(), 'nosetests.xml')
 
-# Forward IPA logging to a normal Python logger. Nose's logcapture plugin
-# can't work with IPA-managed loggers
-class LogHandler(logging.Handler):
-    name = 'forwarding log handler'
-    logger = logging.getLogger('IPA')
+os.chdir(os.path.dirname(ipatests.__file__))
 
-    def emit(self, record):
-        self.logger.log(record.levelno, self.format(record))
-
-if 'console' in log_mgr.handlers:
-    log_mgr.remove_handler('console')
-log_mgr.configure(
-    {
-        'default_level': 'DEBUG',
-        'handlers': [{'log_handler': LogHandler(),
-                      'format': '[%(name)s] %(message)s',
-                      'level': 'debug'},
-                     {'level': 'debug',
-                      'name': 'console',
-                      'stream': sys.stderr}]},
-    configure_state='tests')
-
-
-nose.main(argv=cmd, addplugins=[BeakerLibPlugin(), OrderTests()])
+sys.exit(pytest.main(sys.argv[1:]))
-- 
2.1.0

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to