You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by di...@apache.org on 2022/11/07 20:37:33 UTC

[allura] branch pytest-finalize updated (3d68b00fc -> 649503257)

This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a change to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git


 discard 3d68b00fc fixup! [#8455] added pytest.ini
 discard d16f8ac68 fixup! [#8455] update test runner to use pytest and pytest-xdist for parallelization
 discard b346a06ff [#8455] change pytest from dev dep to full dep, add pytest-sugar dep
 discard bdd7df7a5 [#8455] misc fixes to tests from pytest conversion
 discard d0f75c3d8 [#8455] remove 'test_suite' and 'tests_require' from setup.py as they are deprecated
 discard 2a3bcb48e [#8455] update docker test run to use pytest
 discard b29a10498 [#8455] updated test docs, removed various old references to nose and replaced with pytest
 discard af18fb315 [#8455] remove @with_nose_compatability
 discard d1f7db655 [#8455] update test runner to use pytest and pytest-xdist for parallelization
 discard 2ebd49ad5 [#8455] remove unused tox.ini
    omit 655079433 [#8445] remove datadiff
    omit 6e0ff5f4d [#8445] pytest - remove alluratest.tools, dtadiff, nose asserts
    omit 8b1713cd0 [#8455] make setup 'method' param optional
    omit ad6acb8ce [#8455] remove @with_setup
    omit 2151e93df [#8455] converted yield test to pytest.mark.parametrize
    omit 829ab1962 [#8455] added pytest.ini
     new 8f2e1003f [#8455] added pytest.ini
     new 8709bdc10 [#8455] converted yield test to pytest.mark.parametrize
     new 5385c24da [#8455] remove @with_setup
     new e962bd479 [#8455] make setup 'method' param optional
     new bb53572e2 [#8445] pytest - remove alluratest.tools, dtadiff, nose asserts
     new c3c5e0f4c [#8445] remove datadiff
     new 49ca10e19 [#8455] remove unused tox.ini
     new a92536f9d [#8455] update test runner to use pytest and pytest-xdist for parallelization
     new 6f7431cd2 [#8455] remove @with_nose_compatability
     new 0cc46ae4d [#8455] updated test docs, removed various old references to nose and replaced with pytest
     new 730afcf7d [#8455] update docker test run to use pytest
     new ebd0d3b34 [#8455] remove 'test_suite' and 'tests_require' from setup.py as they are deprecated
     new 0c064dc0c [#8455] misc fixes to tests from pytest conversion
     new 649503257 [#8455] change pytest from dev dep to full dep, add pytest-sugar dep

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (3d68b00fc)
            \
             N -- N -- N   refs/heads/pytest-finalize (649503257)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 14 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


[allura] 06/14: [#8445] remove datadiff

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit c3c5e0f4cc5b2bdf50209b49e765dc09b26ece25
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Oct 25 14:04:31 2022 +0000

    [#8445] remove datadiff
---
 requirements.txt | 2 --
 1 file changed, 2 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index ccb7c7490..54e502cb9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -32,8 +32,6 @@ creoleparser==0.7.5
     # via pypeline
 cryptography==38.0.3
     # via -r requirements.in
-datadiff==2.0.0
-    # via -r requirements.in
 decorator==5.1.1
     # via -r requirements.in
 docutils==0.19


[allura] 11/14: [#8455] update docker test run to use pytest

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 730afcf7dad4674cc790af5de41fe931017a4d20
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Nov 1 15:30:49 2022 +0000

    [#8455] update docker test run to use pytest
---
 scripts/jenkins-python3.7.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scripts/jenkins-python3.7.sh b/scripts/jenkins-python3.7.sh
index 3746742a8..d8f5baa67 100755
--- a/scripts/jenkins-python3.7.sh
+++ b/scripts/jenkins-python3.7.sh
@@ -42,7 +42,7 @@ echo "==========================================================================
 echo "Run: cleanup previous runs"
 echo "============================================================================="
 rm -rf ./allura-data
-git clean -f -x  # remove test.log, nosetest.xml etc (don't use -d since it'd remove our venv dir)
+git clean -f -x  # remove test.log, pytest.junit.xml etc (don't use -d since it'd remove our venv dir)
 
 docker-compose down
 
@@ -98,7 +98,7 @@ docker-compose exec -T web bash -c "pyflakes Allura* Forge* scripts | awk -F\: '
 docker-compose exec -T web bash -c "pycodestyle Allura* Forge* scripts > pep8.txt"
 
 # TODO: ALLURA_VALIDATION=all
-docker-compose exec -T -e LANG=en_US.UTF-8 web ./run_tests --with-xunitmp # --with-coverage --cover-erase
+docker-compose exec -T -e LANG=en_US.UTF-8 web ./run_tests --junit-xml=pytest.junit.xml # --with-coverage --cover-erase
 retcode=$?
 
 #find . -name .coverage -maxdepth 2 | while read coveragefile; do pushd `dirname $coveragefile`; coverage xml --include='forge*,allura*'; popd; done;


[allura] 04/14: [#8455] make setup 'method' param optional

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit e962bd479eeca8207825fca99acefc9eed3cde53
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Oct 25 13:34:14 2022 +0000

    [#8455] make setup 'method' param optional
---
 AlluraTest/alluratest/controller.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/AlluraTest/alluratest/controller.py b/AlluraTest/alluratest/controller.py
index f0011a74b..207946075 100644
--- a/AlluraTest/alluratest/controller.py
+++ b/AlluraTest/alluratest/controller.py
@@ -169,7 +169,7 @@ class TestController:
     application_under_test = 'main'
     validate_skip = False
 
-    def setup_method(self, method):
+    def setup_method(self, method=None):
         """Method called by nose before running each test"""
         pkg = self.__module__.split('.')[0]
         self.app = ValidatingTestApp(
@@ -181,7 +181,7 @@ class TestController:
             self.smtp_mock = mock.patch('allura.lib.mail_util.smtplib.SMTP')
             self.smtp_mock.start()
 
-    def teardown_method(self, method):
+    def teardown_method(self, method=None):
         """Method called by nose after running each test"""
         if asbool(tg.config.get('smtp.mock')):
             self.smtp_mock.stop()


[allura] 03/14: [#8455] remove @with_setup

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 5385c24da802afae71104435a26649e3c513a06a
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Thu Sep 29 14:04:06 2022 +0000

    [#8455] remove @with_setup
---
 Allura/allura/tests/model/test_artifact.py     | 493 ++++++-------
 Allura/allura/tests/model/test_auth.py         | 823 ++++++++++-----------
 Allura/allura/tests/model/test_discussion.py   | 966 ++++++++++++-------------
 Allura/allura/tests/model/test_monq.py         |   4 +-
 Allura/allura/tests/model/test_neighborhood.py | 107 ++-
 Allura/allura/tests/model/test_oauth.py        |  32 +-
 Allura/allura/tests/model/test_project.py      | 325 ++++-----
 Allura/allura/tests/test_app.py                | 295 ++++----
 8 files changed, 1449 insertions(+), 1596 deletions(-)

diff --git a/Allura/allura/tests/model/test_artifact.py b/Allura/allura/tests/model/test_artifact.py
index 940ae0cac..35402dd0c 100644
--- a/Allura/allura/tests/model/test_artifact.py
+++ b/Allura/allura/tests/model/test_artifact.py
@@ -22,7 +22,6 @@ import re
 from datetime import datetime
 
 from tg import tmpl_context as c
-from alluratest.tools import with_setup
 from mock import patch
 import pytest
 from ming.orm.ormsession import ThreadLocalORMSession
@@ -55,273 +54,239 @@ class Checkmessage(M.Message):
 Mapper.compile_all()
 
 
-def setup_method():
-    setup_basic_test()
-    setup_unit_test()
-    setup_with_tools()
-
-
-def teardown_module():
-    ThreadLocalORMSession.close_all()
-
-
-@td.with_wiki
-def setup_with_tools():
-    h.set_context('test', 'wiki', neighborhood='Projects')
-    Checkmessage.query.remove({})
-    WM.Page.query.remove({})
-    WM.PageHistory.query.remove({})
-    M.Shortlink.query.remove({})
-    c.user = M.User.query.get(username='test-admin')
-    Checkmessage.project = c.project
-    Checkmessage.app_config = c.app.config
-
-
-@with_setup(setup_method)
-def test_artifact():
-    pg = WM.Page(title='TestPage1')
-    assert pg.project == c.project
-    assert pg.project_id == c.project._id
-    assert pg.app.config == c.app.config
-    assert pg.app_config == c.app.config
-    u = M.User.query.get(username='test-user')
-    pr = M.ProjectRole.by_user(u, upsert=True)
-    ThreadLocalORMSession.flush_all()
-    REGISTRY.register(allura.credentials, allura.lib.security.Credentials())
-    assert not security.has_access(pg, 'delete')(user=u)
-    pg.acl.append(M.ACE.allow(pr._id, 'delete'))
-    ThreadLocalORMSession.flush_all()
-    assert security.has_access(pg, 'delete')(user=u)
-    pg.acl.pop()
-    ThreadLocalORMSession.flush_all()
-    assert not security.has_access(pg, 'delete')(user=u)
-
-
-def test_artifact_index():
-    pg = WM.Page(title='TestPage1')
-    idx = pg.index()
-    assert 'title' in idx
-    assert 'url_s' in idx
-    assert 'project_id_s' in idx
-    assert 'mount_point_s' in idx
-    assert 'type_s' in idx
-    assert 'id' in idx
-    assert idx['id'] == pg.index_id()
-    assert 'text' in idx
-    assert 'TestPage' in pg.shorthand_id()
-    assert pg.link_text() == pg.shorthand_id()
-
-
-@with_setup(setup_method)
-def test_artifactlink():
-    pg = WM.Page(title='TestPage2')
-    q_shortlink = M.Shortlink.query.find(dict(
-        project_id=c.project._id,
-        app_config_id=c.app.config._id,
-        link=pg.shorthand_id()))
-    assert q_shortlink.count() == 0
-
-    ThreadLocalORMSession.flush_all()
-    M.MonQTask.run_ready()
-    ThreadLocalORMSession.flush_all()
-    assert q_shortlink.count() == 1
-
-    assert M.Shortlink.lookup('[TestPage2]')
-    assert M.Shortlink.lookup('[wiki:TestPage2]')
-    assert M.Shortlink.lookup('[test:wiki:TestPage2]')
-    assert not M.Shortlink.lookup('[test:wiki:TestPage2:foo]')
-    assert not M.Shortlink.lookup('[Wiki:TestPage2]')
-    assert not M.Shortlink.lookup('[TestPage2_no_such_page]')
-
-    pg.delete()
-    c.project.uninstall_app('wiki')
-    assert not M.Shortlink.lookup('[wiki:TestPage2]')
-    assert q_shortlink.count() == 0
-
-
-@with_setup(setup_method)
-def test_gen_messageid():
-    assert re.match(r'[0-9a-zA-Z]*.wiki@test.p.localhost',
-                    h.gen_message_id())
-
-
-@with_setup(setup_method)
-def test_gen_messageid_with_id_set():
-    oid = ObjectId()
-    assert re.match(r'%s.wiki@test.p.localhost' %
-                    str(oid), h.gen_message_id(oid))
-
-
-@with_setup(setup_method)
-def test_artifact_messageid():
-    p = WM.Page(title='T')
-    assert re.match(r'%s.wiki@test.p.localhost' %
-                    str(p._id), p.message_id())
-
-
-@with_setup(setup_method)
-def test_versioning():
-    pg = WM.Page(title='TestPage3')
-    with patch('allura.model.artifact.request',
-               Request.blank('/', remote_addr='1.1.1.1')):
+class TestArtifact:
+
+    def setup_method(self):
+        setup_basic_test()
+        setup_unit_test()
+        self.setup_with_tools()
+
+    def teardown_class(cls):
+        ThreadLocalORMSession.close_all()
+
+    @td.with_wiki
+    def setup_with_tools(self):
+        h.set_context('test', 'wiki', neighborhood='Projects')
+        Checkmessage.query.remove({})
+        WM.Page.query.remove({})
+        WM.PageHistory.query.remove({})
+        M.Shortlink.query.remove({})
+        c.user = M.User.query.get(username='test-admin')
+        Checkmessage.project = c.project
+        Checkmessage.app_config = c.app.config
+
+    def test_artifact(self):
+        pg = WM.Page(title='TestPage1')
+        assert pg.project == c.project
+        assert pg.project_id == c.project._id
+        assert pg.app.config == c.app.config
+        assert pg.app_config == c.app.config
+        u = M.User.query.get(username='test-user')
+        pr = M.ProjectRole.by_user(u, upsert=True)
+        ThreadLocalORMSession.flush_all()
+        REGISTRY.register(allura.credentials, allura.lib.security.Credentials())
+        assert not security.has_access(pg, 'delete')(user=u)
+        pg.acl.append(M.ACE.allow(pr._id, 'delete'))
+        ThreadLocalORMSession.flush_all()
+        assert security.has_access(pg, 'delete')(user=u)
+        pg.acl.pop()
+        ThreadLocalORMSession.flush_all()
+        assert not security.has_access(pg, 'delete')(user=u)
+
+    def test_artifact_index(self):
+        pg = WM.Page(title='TestPage1')
+        idx = pg.index()
+        assert 'title' in idx
+        assert 'url_s' in idx
+        assert 'project_id_s' in idx
+        assert 'mount_point_s' in idx
+        assert 'type_s' in idx
+        assert 'id' in idx
+        assert idx['id'] == pg.index_id()
+        assert 'text' in idx
+        assert 'TestPage' in pg.shorthand_id()
+        assert pg.link_text() == pg.shorthand_id()
+
+    def test_artifactlink(self):
+        pg = WM.Page(title='TestPage2')
+        q_shortlink = M.Shortlink.query.find(dict(
+            project_id=c.project._id,
+            app_config_id=c.app.config._id,
+            link=pg.shorthand_id()))
+        assert q_shortlink.count() == 0
+
+        ThreadLocalORMSession.flush_all()
+        M.MonQTask.run_ready()
+        ThreadLocalORMSession.flush_all()
+        assert q_shortlink.count() == 1
+
+        assert M.Shortlink.lookup('[TestPage2]')
+        assert M.Shortlink.lookup('[wiki:TestPage2]')
+        assert M.Shortlink.lookup('[test:wiki:TestPage2]')
+        assert not M.Shortlink.lookup('[test:wiki:TestPage2:foo]')
+        assert not M.Shortlink.lookup('[Wiki:TestPage2]')
+        assert not M.Shortlink.lookup('[TestPage2_no_such_page]')
+
+        pg.delete()
+        c.project.uninstall_app('wiki')
+        assert not M.Shortlink.lookup('[wiki:TestPage2]')
+        assert q_shortlink.count() == 0
+
+    def test_gen_messageid(self):
+        assert re.match(r'[0-9a-zA-Z]*.wiki@test.p.localhost',
+                        h.gen_message_id())
+
+    def test_gen_messageid_with_id_set(self):
+        oid = ObjectId()
+        assert re.match(r'%s.wiki@test.p.localhost' %
+                        str(oid), h.gen_message_id(oid))
+
+    def test_artifact_messageid(self):
+        p = WM.Page(title='T')
+        assert re.match(r'%s.wiki@test.p.localhost' %
+                        str(p._id), p.message_id())
+
+    def test_versioning(self):
+        pg = WM.Page(title='TestPage3')
+        with patch('allura.model.artifact.request', Request.blank('/', remote_addr='1.1.1.1')):
+            pg.commit()
+        ThreadLocalORMSession.flush_all()
+        pg.text = 'Here is some text'
         pg.commit()
-    ThreadLocalORMSession.flush_all()
-    pg.text = 'Here is some text'
-    pg.commit()
-    ThreadLocalORMSession.flush_all()
-    ss = pg.get_version(1)
-    assert ss.author.logged_ip == '1.1.1.1'
-    assert ss.index()['is_history_b']
-    assert ss.shorthand_id() == pg.shorthand_id() + '#1'
-    assert ss.title == pg.title
-    assert ss.text != pg.text
-    ss = pg.get_version(-1)
-    assert ss.index()['is_history_b']
-    assert ss.shorthand_id() == pg.shorthand_id() + '#2'
-    assert ss.title == pg.title
-    assert ss.text == pg.text
-    pytest.raises(IndexError, pg.get_version, 42)
-    pg.revert(1)
-    pg.commit()
-    ThreadLocalORMSession.flush_all()
-    assert ss.text != pg.text
-    assert pg.history().count() == 3
-
-
-@with_setup(setup_method)
-def test_messages_unknown_lookup():
-    from bson import ObjectId
-    m = Checkmessage()
-    m.author_id = ObjectId()  # something new
-    assert isinstance(m.author(), M.User), type(m.author())
-    assert m.author() == M.User.anonymous()
-
-
-@with_setup(setup_method)
-@patch('allura.model.artifact.datetime')
-def test_last_updated(_datetime):
-    c.project.last_updated = datetime(2014, 1, 1)
-    _datetime.utcnow.return_value = datetime(2014, 1, 2)
-    WM.Page(title='TestPage1')
-    ThreadLocalORMSession.flush_all()
-    assert c.project.last_updated == datetime(2014, 1, 2)
-
-
-@with_setup(setup_method)
-@patch('allura.model.artifact.datetime')
-def test_last_updated_disabled(_datetime):
-    c.project.last_updated = datetime(2014, 1, 1)
-    _datetime.utcnow.return_value = datetime(2014, 1, 2)
-    try:
-        M.artifact_orm_session._get().skip_last_updated = True
+        ThreadLocalORMSession.flush_all()
+        ss = pg.get_version(1)
+        assert ss.author.logged_ip == '1.1.1.1'
+        assert ss.index()['is_history_b']
+        assert ss.shorthand_id() == pg.shorthand_id() + '#1'
+        assert ss.title == pg.title
+        assert ss.text != pg.text
+        ss = pg.get_version(-1)
+        assert ss.index()['is_history_b']
+        assert ss.shorthand_id() == pg.shorthand_id() + '#2'
+        assert ss.title == pg.title
+        assert ss.text == pg.text
+        pytest.raises(IndexError, pg.get_version, 42)
+        pg.revert(1)
+        pg.commit()
+        ThreadLocalORMSession.flush_all()
+        assert ss.text != pg.text
+        assert pg.history().count() == 3
+
+    def test_messages_unknown_lookup(self):
+        from bson import ObjectId
+        m = Checkmessage()
+        m.author_id = ObjectId()  # something new
+        assert isinstance(m.author(), M.User), type(m.author())
+        assert m.author() == M.User.anonymous()
+
+    @patch('allura.model.artifact.datetime')
+    def test_last_updated(self, _datetime):
+        c.project.last_updated = datetime(2014, 1, 1)
+        _datetime.utcnow.return_value = datetime(2014, 1, 2)
         WM.Page(title='TestPage1')
         ThreadLocalORMSession.flush_all()
-        assert c.project.last_updated == datetime(2014, 1, 1)
-    finally:
-        M.artifact_orm_session._get().skip_last_updated = False
-
-
-@with_setup(setup_method)
-def test_get_discussion_thread_dupe():
-    artif = WM.Page(title='TestSomeArtifact')
-    thr1 = artif.get_discussion_thread()[0]
-    thr1.post('thr1-post1')
-    thr1.post('thr1-post2')
-    thr2 = M.Thread.new(ref_id=thr1.ref_id)
-    thr2.post('thr2-post1')
-    thr2.post('thr2-post2')
-    thr2.post('thr2-post3')
-    thr3 = M.Thread.new(ref_id=thr1.ref_id)
-    thr3.post('thr3-post1')
-    thr4 = M.Thread.new(ref_id=thr1.ref_id)
-
-    thread_q = M.Thread.query.find(dict(ref_id=artif.index_id()))
-    assert thread_q.count() == 4
-
-    thread = artif.get_discussion_thread()[0]  # force cleanup
-    threads = thread_q.all()
-    assert len(threads) == 1
-    assert len(thread.posts) == 6
-    assert not any(p.deleted for p in thread.posts)  # normal thread deletion propagates to children, make sure that doesn't happen
-
-
-def test_snapshot_clear_user_data():
-    s = M.Snapshot(author={'username': 'johndoe',
-                           'display_name': 'John Doe',
-                           'logged_ip': '1.2.3.4'})
-    s.clear_user_data()
-    assert s.author == {'username': '',
+        assert c.project.last_updated == datetime(2014, 1, 2)
+
+    @patch('allura.model.artifact.datetime')
+    def test_last_updated_disabled(self, _datetime):
+        c.project.last_updated = datetime(2014, 1, 1)
+        _datetime.utcnow.return_value = datetime(2014, 1, 2)
+        try:
+            M.artifact_orm_session._get().skip_last_updated = True
+            WM.Page(title='TestPage1')
+            ThreadLocalORMSession.flush_all()
+            assert c.project.last_updated == datetime(2014, 1, 1)
+        finally:
+            M.artifact_orm_session._get().skip_last_updated = False
+
+    def test_get_discussion_thread_dupe(self):
+        artif = WM.Page(title='TestSomeArtifact')
+        thr1 = artif.get_discussion_thread()[0]
+        thr1.post('thr1-post1')
+        thr1.post('thr1-post2')
+        thr2 = M.Thread.new(ref_id=thr1.ref_id)
+        thr2.post('thr2-post1')
+        thr2.post('thr2-post2')
+        thr2.post('thr2-post3')
+        thr3 = M.Thread.new(ref_id=thr1.ref_id)
+        thr3.post('thr3-post1')
+        thr4 = M.Thread.new(ref_id=thr1.ref_id)
+
+        thread_q = M.Thread.query.find(dict(ref_id=artif.index_id()))
+        assert thread_q.count() == 4
+
+        thread = artif.get_discussion_thread()[0]  # force cleanup
+        threads = thread_q.all()
+        assert len(threads) == 1
+        assert len(thread.posts) == 6
+        # normal thread deletion propagates to children, make sure that doesn't happen
+        assert not any(p.deleted for p in thread.posts)
+
+    def test_snapshot_clear_user_data(self):
+        s = M.Snapshot(author={'username': 'johndoe',
+                               'display_name': 'John Doe',
+                               'logged_ip': '1.2.3.4'})
+        s.clear_user_data()
+        assert s.author == {'username': '',
                             'display_name': '',
                             'logged_ip': None,
-                            'id': None,
-                            }
-
-
-@with_setup(setup_method)
-def test_snapshot_from_username():
-    s = M.Snapshot(author={'username': 'johndoe',
-                           'display_name': 'John Doe',
-                           'logged_ip': '1.2.3.4'})
-    s = M.Snapshot(author={'username': 'johnsmith',
-                           'display_name': 'John Doe',
-                           'logged_ip': '1.2.3.4'})
-    ThreadLocalORMSession.flush_all()
-    assert len(M.Snapshot.from_username('johndoe')) == 1
-
-
-def test_feed_clear_user_data():
-    f = M.Feed(author_name='John Doe',
+                            'id': None}
+
+    def test_snapshot_from_username(self):
+        s = M.Snapshot(author={'username': 'johndoe',
+                               'display_name': 'John Doe',
+                               'logged_ip': '1.2.3.4'})
+        s = M.Snapshot(author={'username': 'johnsmith',
+                               'display_name': 'John Doe',
+                               'logged_ip': '1.2.3.4'})
+        ThreadLocalORMSession.flush_all()
+        assert len(M.Snapshot.from_username('johndoe')) == 1
+
+    def test_feed_clear_user_data(self):
+        f = M.Feed(author_name='John Doe',
+                   author_link='/u/johndoe/',
+                   title='Something')
+        f.clear_user_data()
+        assert f.author_name == ''
+        assert f.author_link == ''
+        assert f.title == 'Something'
+
+        f = M.Feed(author_name='John Doe',
+                   author_link='/u/johndoe/',
+                   title='Home Page modified by John Doe')
+        f.clear_user_data()
+        assert f.author_name == ''
+        assert f.author_link == ''
+        assert f.title == 'Home Page modified by <REDACTED>'
+
+    def test_feed_from_username(self):
+        M.Feed(author_name='John Doe',
                author_link='/u/johndoe/',
                title='Something')
-    f.clear_user_data()
-    assert f.author_name == ''
-    assert f.author_link == ''
-    assert f.title == 'Something'
-
-    f = M.Feed(author_name='John Doe',
-               author_link='/u/johndoe/',
-               title='Home Page modified by John Doe')
-    f.clear_user_data()
-    assert f.author_name == ''
-    assert f.author_link == ''
-    assert f.title == 'Home Page modified by <REDACTED>'
-
-
-@with_setup(setup_method)
-def test_feed_from_username():
-    M.Feed(author_name='John Doe',
-           author_link='/u/johndoe/',
-           title='Something')
-    M.Feed(author_name='John Smith',
-           author_link='/u/johnsmith/',
-           title='Something')
-    ThreadLocalORMSession.flush_all()
-    assert len(M.Feed.from_username('johndoe')) == 1
-
-
-@with_setup(setup_method)
-def test_subscribed():
-    pg = WM.Page(title='TestPage4a')
-    assert pg.subscribed(include_parents=True)  # tool is subscribed to admins by default
-    assert not pg.subscribed(include_parents=False)
-
-
-@with_setup(setup_method)
-def test_subscribed_no_tool_sub():
-    pg = WM.Page(title='TestPage4b')
-    M.Mailbox.unsubscribe(user_id=c.user._id,
-                          project_id=c.project._id,
-                          app_config_id=c.app.config._id)
-    pg.subscribe()
-    assert pg.subscribed(include_parents=True)
-    assert pg.subscribed(include_parents=False)
-
-
-@with_setup(setup_method)
-def test_not_subscribed():
-    pg = WM.Page(title='TestPage4c')
-    M.Mailbox.unsubscribe(user_id=c.user._id,
-                          project_id=c.project._id,
-                          app_config_id=c.app.config._id)
-    assert not pg.subscribed(include_parents=True)
-    assert not pg.subscribed(include_parents=False)
+        M.Feed(author_name='John Smith',
+               author_link='/u/johnsmith/',
+               title='Something')
+        ThreadLocalORMSession.flush_all()
+        assert len(M.Feed.from_username('johndoe')) == 1
+
+    def test_subscribed(self):
+        pg = WM.Page(title='TestPage4a')
+        assert pg.subscribed(include_parents=True)  # tool is subscribed to admins by default
+        assert not pg.subscribed(include_parents=False)
+
+    def test_subscribed_no_tool_sub(self):
+        pg = WM.Page(title='TestPage4b')
+        M.Mailbox.unsubscribe(user_id=c.user._id,
+                              project_id=c.project._id,
+                              app_config_id=c.app.config._id)
+        pg.subscribe()
+        assert pg.subscribed(include_parents=True)
+        assert pg.subscribed(include_parents=False)
+
+    def test_not_subscribed(self):
+        pg = WM.Page(title='TestPage4c')
+        M.Mailbox.unsubscribe(user_id=c.user._id,
+                              project_id=c.project._id,
+                              app_config_id=c.app.config._id)
+        assert not pg.subscribed(include_parents=True)
+        assert not pg.subscribed(include_parents=False)
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index b4e5e80de..7b6364938 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -22,15 +22,7 @@ Model tests for auth
 import textwrap
 from datetime import datetime, timedelta
 
-from alluratest.tools import (
-    with_setup,
-    assert_equal,
-    assert_not_equal,
-    assert_true,
-    assert_not_in,
-    assert_in,
-)
-from tg import tmpl_context as c, app_globals as g, request
+from tg import tmpl_context as c, app_globals as g, request as r
 from webob import Request
 from mock import patch, Mock
 
@@ -41,437 +33,404 @@ from allura import model as M
 from allura.lib import helpers as h
 from allura.lib import plugin
 from allura.tests import decorators as td
-from alluratest.controller import setup_basic_test, setup_global_objects, setup_functional_test
+from alluratest.controller import setup_basic_test, setup_global_objects, setup_functional_test, setup_unit_test
 from alluratest.pytest_helpers import with_nose_compatibility
 
 
-def setup_method():
-    setup_basic_test()
-    ThreadLocalORMSession.close_all()
-    setup_global_objects()
-
-
-@with_setup(setup_method)
-def test_email_address():
-    addr = M.EmailAddress(email='test_admin@domain.net',
-                          claimed_by_user_id=c.user._id)
-    ThreadLocalORMSession.flush_all()
-    assert addr.claimed_by_user() == c.user
-    addr2 = M.EmailAddress.create('test@domain.net')
-    addr3 = M.EmailAddress.create('test_admin@domain.net')
-
-    # Duplicate emails are allowed, until the email is confirmed
-    assert addr3 is not addr
-
-    assert addr2 is not addr
-    assert addr2
-    addr4 = M.EmailAddress.create('test@DOMAIN.NET')
-    assert addr4 is not addr2
-
-    assert addr is c.user.address_object('test_admin@domain.net')
-    c.user.claim_address('test@DOMAIN.NET')
-    assert 'test@domain.net' in c.user.email_addresses
-
-
-@with_setup(setup_method)
-def test_email_address_lookup_helpers():
-    addr = M.EmailAddress.create('TEST@DOMAIN.NET')
-    nobody = M.EmailAddress.create('nobody@example.com')
-    ThreadLocalORMSession.flush_all()
-    assert addr.email == 'TEST@domain.net'
-
-    assert M.EmailAddress.get(email='TEST@DOMAIN.NET') == addr
-    assert M.EmailAddress.get(email='TEST@domain.net') == addr
-    assert M.EmailAddress.get(email='test@domain.net') == None
-    assert M.EmailAddress.get(email=None) == None
-    assert M.EmailAddress.get(email='nobody@example.com') == nobody
-    # invalid email returns None, but not nobody@example.com as before
-    assert M.EmailAddress.get(email='invalid') == None
-
-    assert M.EmailAddress.find(dict(email='TEST@DOMAIN.NET')).all() == [addr]
-    assert M.EmailAddress.find(dict(email='TEST@domain.net')).all() == [addr]
-    assert M.EmailAddress.find(dict(email='test@domain.net')).all() == []
-    assert M.EmailAddress.find(dict(email=None)).all() == []
-    assert M.EmailAddress.find(dict(email='nobody@example.com')).all() == [nobody]
-    # invalid email returns empty query, but not nobody@example.com as before
-    assert M.EmailAddress.find(dict(email='invalid')).all() == []
-
-
-@with_setup(setup_method)
-def test_email_address_canonical():
-    assert (M.EmailAddress.canonical('nobody@EXAMPLE.COM') ==
-                 'nobody@example.com')
-    assert (M.EmailAddress.canonical('nobody@example.com') ==
-                 'nobody@example.com')
-    assert (M.EmailAddress.canonical('I Am Nobody <no...@example.com>') ==
-                 'nobody@example.com')
-    assert (M.EmailAddress.canonical('  nobody@example.com\t') ==
-                 'nobody@example.com')
-    assert (M.EmailAddress.canonical('I Am@Nobody <no...@example.com> ') ==
-                 'nobody@example.com')
-    assert (M.EmailAddress.canonical(' No@body <no...@example.com> ') ==
-                 'no@body@example.com')
-    assert (M.EmailAddress.canonical('no@body@example.com') ==
-                 'no@body@example.com')
-    assert M.EmailAddress.canonical('invalid') == None
-
-@with_setup(setup_method)
-def test_email_address_send_verification_link():
-    addr = M.EmailAddress(email='test_admin@domain.net',
-                          claimed_by_user_id=c.user._id)
-
-    addr.send_verification_link()
-
-    with patch('allura.tasks.mail_tasks.smtp_client._client') as _client:
-        M.MonQTask.run_ready()
-    return_path, rcpts, body = _client.sendmail.call_args[0]
-    assert rcpts == ['test_admin@domain.net']
-
-
-@with_setup(setup_method)
-@td.with_user_project('test-admin')
-def test_user():
-    assert c.user.url() .endswith('/u/test-admin/')
-    assert c.user.script_name .endswith('/u/test-admin/')
-    assert ({p.shortname for p in c.user.my_projects()} ==
-                 {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
-    # delete one of the projects and make sure it won't appear in my_projects()
-    p = M.Project.query.get(shortname='test2')
-    p.deleted = True
-    ThreadLocalORMSession.flush_all()
-    assert ({p.shortname for p in c.user.my_projects()} ==
-                 {'test', 'u/test-admin', 'adobe-1', '--init--'})
-    u = M.User.register(dict(
-        username='nosetest-user'))
-    ThreadLocalORMSession.flush_all()
-    assert u.reg_date
-    assert u.private_project().shortname == 'u/nosetest-user'
-    roles = g.credentials.user_roles(
-        u._id, project_id=u.private_project().root_project._id)
-    assert len(roles) == 3, roles
-    u.set_password('foo')
-    provider = plugin.LocalAuthenticationProvider(Request.blank('/'))
-    assert provider._validate_password(u, 'foo')
-    assert not provider._validate_password(u, 'foobar')
-    u.set_password('foobar')
-    assert provider._validate_password(u, 'foobar')
-    assert not provider._validate_password(u, 'foo')
-
-
-@with_setup(setup_method)
-def test_user_project_creates_on_demand():
-    u = M.User.register(dict(username='foobar123'), make_project=False)
-    ThreadLocalORMSession.flush_all()
-    assert not M.Project.query.get(shortname='u/foobar123')
-    assert u.private_project()
-    assert M.Project.query.get(shortname='u/foobar123')
-
-
-@with_setup(setup_method)
-def test_user_project_already_deleted_creates_on_demand():
-    u = M.User.register(dict(username='foobar123'), make_project=True)
-    p = M.Project.query.get(shortname='u/foobar123')
-    p.deleted = True
-    ThreadLocalORMSession.flush_all()
-    assert not M.Project.query.get(shortname='u/foobar123', deleted=False)
-    assert u.private_project()
-    ThreadLocalORMSession.flush_all()
-    assert M.Project.query.get(shortname='u/foobar123', deleted=False)
-
-
-@with_setup(setup_method)
-def test_user_project_does_not_create_on_demand_for_disabled_user():
-    u = M.User.register(
-        dict(username='foobar123', disabled=True), make_project=False)
-    ThreadLocalORMSession.flush_all()
-    assert not u.private_project()
-    assert not M.Project.query.get(shortname='u/foobar123')
-
-
-@with_setup(setup_method)
-def test_user_project_does_not_create_on_demand_for_anonymous_user():
-    u = M.User.anonymous()
-    ThreadLocalORMSession.flush_all()
-    assert not u.private_project()
-    assert not M.Project.query.get(shortname='u/anonymous')
-    assert not M.Project.query.get(shortname='u/*anonymous')
-
-
-@with_setup(setup_method)
-@patch('allura.model.auth.log')
-def test_user_by_email_address(log):
-    u1 = M.User.register(dict(username='abc1'), make_project=False)
-    u2 = M.User.register(dict(username='abc2'), make_project=False)
-    addr1 = M.EmailAddress(email='abc123@abc.me', confirmed=True,
-                           claimed_by_user_id=u1._id)
-    addr2 = M.EmailAddress(email='abc123@abc.me', confirmed=True,
-                           claimed_by_user_id=u2._id)
-    # both users are disabled
-    u1.disabled, u2.disabled = True, True
-    ThreadLocalORMSession.flush_all()
-    assert M.User.by_email_address('abc123@abc.me') == None
-    assert log.warn.call_count == 0
-
-    # only u2 is active
-    u1.disabled, u2.disabled = True, False
-    ThreadLocalORMSession.flush_all()
-    assert M.User.by_email_address('abc123@abc.me') == u2
-    assert log.warn.call_count == 0
-
-    # both are active
-    u1.disabled, u2.disabled = False, False
-    ThreadLocalORMSession.flush_all()
-    assert M.User.by_email_address('abc123@abc.me') in [u1, u2]
-    assert log.warn.call_count == 1
-
-    # invalid email returns None, but not user which claimed
-    # nobody@example.com as before
-    nobody = M.EmailAddress(email='nobody@example.com', confirmed=True,
-                            claimed_by_user_id=u1._id)
-    ThreadLocalORMSession.flush_all()
-    assert M.User.by_email_address('nobody@example.com') == u1
-    assert M.User.by_email_address('invalid') == None
-
-
-def test_user_equality():
-    assert M.User.by_username('test-user') == M.User.by_username('test-user')
-    assert M.User.anonymous() == M.User.anonymous()
-    assert M.User.by_username('*anonymous') == M.User.anonymous()
-
-    assert M.User.by_username('test-user') != M.User.by_username('test-admin')
-    assert M.User.by_username('test-user') != M.User.anonymous()
-    assert M.User.anonymous() != None
-    assert M.User.anonymous() != 12345
-    assert M.User.anonymous() != M.User()
-
-
-def test_user_hash():
-    assert M.User.by_username('test-user') in {M.User.by_username('test-user')}
-    assert M.User.anonymous() in {M.User.anonymous()}
-    assert M.User.by_username('*anonymous') in {M.User.anonymous()}
-
-    assert M.User.by_username('test-user') not in {M.User.by_username('test-admin')}
-    assert M.User.anonymous() not in {M.User.by_username('test-admin')}
-    assert M.User.anonymous() not in {0, None}
-
-
-@with_setup(setup_method)
-def test_project_role():
-    role = M.ProjectRole(project_id=c.project._id, name='test_role')
-    M.ProjectRole.by_user(c.user, upsert=True).roles.append(role._id)
-    ThreadLocalORMSession.flush_all()
-    roles = g.credentials.user_roles(
-        c.user._id, project_id=c.project.root_project._id)
-    roles_ids = [r['_id'] for r in roles]
-    roles = M.ProjectRole.query.find({'_id': {'$in': roles_ids}})
-    for pr in roles:
-        assert pr.display()
-        pr.special
-        assert pr.user in (c.user, None, M.User.anonymous())
-
-
-@with_setup(setup_method)
-def test_default_project_roles():
-    roles = {
-        pr.name: pr
-        for pr in M.ProjectRole.query.find(dict(
-            project_id=c.project._id)).all()
-        if pr.name}
-    assert 'Admin' in list(roles.keys()), list(roles.keys())
-    assert 'Developer' in list(roles.keys()), list(roles.keys())
-    assert 'Member' in list(roles.keys()), list(roles.keys())
-    assert roles['Developer']._id in roles['Admin'].roles
-    assert roles['Member']._id in roles['Developer'].roles
-
-    # There're 1 user assigned to project, represented by
-    # relational (vs named) ProjectRole's
-    assert len(roles) == M.ProjectRole.query.find(dict(
-        project_id=c.project._id)).count() - 1
-
-
-@with_setup(setup_method)
-def test_email_address_claimed_by_user():
-    addr = M.EmailAddress(email='test_admin@domain.net',
-                          claimed_by_user_id=c.user._id)
-    c.user.disabled = True
-    ThreadLocalORMSession.flush_all()
-    assert addr.claimed_by_user() is None
-
-
-@with_setup(setup_method)
-@td.with_user_project('test-admin')
-def test_user_projects_by_role():
-    assert ({p.shortname for p in c.user.my_projects()} ==
-                 {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
-    assert ({p.shortname for p in c.user.my_projects_by_role_name('Admin')} ==
-                 {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
-    # Remove admin access from c.user to test2 project
-    project = M.Project.query.get(shortname='test2')
-    admin_role = M.ProjectRole.by_name('Admin', project)
-    developer_role = M.ProjectRole.by_name('Developer', project)
-    user_role = M.ProjectRole.by_user(c.user, project=project, upsert=True)
-    user_role.roles.remove(admin_role._id)
-    user_role.roles.append(developer_role._id)
-    ThreadLocalORMSession.flush_all()
-    g.credentials.clear()
-    assert ({p.shortname for p in c.user.my_projects()} ==
-                 {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
-    assert ({p.shortname for p in c.user.my_projects_by_role_name('Admin')} ==
-                 {'test', 'u/test-admin', 'adobe-1', '--init--'})
-
-
-@td.with_user_project('test-admin')
-@with_setup(setup_method)
-def test_user_projects_unnamed():
-    """
-    Confirm that spurious ProjectRoles associating a user with
-    a project to which they do not belong to any named group
-    don't cause the user to count as a member of the project.
-    """
-    sub1 = M.Project.query.get(shortname='test/sub1')
-    M.ProjectRole(
-        user_id=c.user._id,
-        project_id=sub1._id)
-    ThreadLocalORMSession.flush_all()
-    project_names = [p.shortname for p in c.user.my_projects()]
-    assert 'test/sub1' not in project_names
-    assert 'test' in project_names
-
-
-@patch.object(g, 'user_message_max_messages', 3)
-def test_check_sent_user_message_times():
-    user1 = M.User.register(dict(username='test-user'), make_project=False)
-    time1 = datetime.utcnow() - timedelta(minutes=30)
-    time2 = datetime.utcnow() - timedelta(minutes=45)
-    time3 = datetime.utcnow() - timedelta(minutes=70)
-    user1.sent_user_message_times = [time1, time2, time3]
-    assert user1.can_send_user_message()
-    assert len(user1.sent_user_message_times) == 2
-    user1.sent_user_message_times.append(
-        datetime.utcnow() - timedelta(minutes=15))
-    assert not user1.can_send_user_message()
-
-
-@with_setup(setup_method)
-@td.with_user_project('test-admin')
-def test_user_track_active():
-    # without this session flushing inside track_active raises Exception
-    setup_functional_test()
-    c.user = M.User.by_username('test-admin')
-
-    assert c.user.last_access['session_date'] == None
-    assert c.user.last_access['session_ip'] == None
-    assert c.user.last_access['session_ua'] == None
-
-    req = Mock(headers={'User-Agent': 'browser'}, remote_addr='addr')
-    c.user.track_active(req)
-    c.user = M.User.by_username(c.user.username)
-    assert c.user.last_access['session_date'] != None
-    assert c.user.last_access['session_ip'] == 'addr'
-    assert c.user.last_access['session_ua'] == 'browser'
-
-    # ensure that session activity tracked with a whole-day granularity
-    prev_date = c.user.last_access['session_date']
-    c.user.track_active(req)
-    c.user = M.User.by_username(c.user.username)
-    assert c.user.last_access['session_date'] == prev_date
-    yesterday = datetime.utcnow() - timedelta(1)
-    c.user.last_access['session_date'] = yesterday
-    session(c.user).flush(c.user)
-    c.user.track_active(req)
-    c.user = M.User.by_username(c.user.username)
-    assert c.user.last_access['session_date'] > yesterday
-
-    # ...or if IP or User Agent has changed
-    req.remote_addr = 'new addr'
-    c.user.track_active(req)
-    c.user = M.User.by_username(c.user.username)
-    assert c.user.last_access['session_ip'] == 'new addr'
-    assert c.user.last_access['session_ua'] == 'browser'
-    req.headers['User-Agent'] = 'new browser'
-    c.user.track_active(req)
-    c.user = M.User.by_username(c.user.username)
-    assert c.user.last_access['session_ip'] == 'new addr'
-    assert c.user.last_access['session_ua'] == 'new browser'
-
-
-@with_setup(setup_method)
-def test_user_index():
-    c.user.email_addresses = ['email1', 'email2']
-    c.user.set_pref('email_address', 'email2')
-    idx = c.user.index()
-    assert idx['id'] == c.user.index_id()
-    assert idx['title'] == 'User test-admin'
-    assert idx['type_s'] == 'User'
-    assert idx['username_s'] == 'test-admin'
-    assert idx['email_addresses_t'] == 'email1 email2'
-    assert idx['email_address_s'] == 'email2'
-    assert 'last_password_updated_dt' in idx
-    assert idx['disabled_b'] == False
-    assert 'results_per_page_i' in idx
-    assert 'email_format_s' in idx
-    assert 'disable_user_messages_b' in idx
-    assert idx['display_name_t'] == 'Test Admin'
-    assert idx['sex_s'] == 'Unknown'
-    assert 'birthdate_dt' in idx
-    assert 'localization_s' in idx
-    assert 'timezone_s' in idx
-    assert 'socialnetworks_t' in idx
-    assert 'telnumbers_t' in idx
-    assert 'skypeaccount_s' in idx
-    assert 'webpages_t' in idx
-    assert 'skills_t' in idx
-    assert 'last_access_login_date_dt' in idx
-    assert 'last_access_login_ip_s' in idx
-    assert 'last_access_login_ua_t' in idx
-    assert 'last_access_session_date_dt' in idx
-    assert 'last_access_session_ip_s' in idx
-    assert 'last_access_session_ua_t' in idx
-    # provided bby auth provider
-    assert 'user_registration_date_dt' in idx
-
-
-@with_setup(setup_method)
-def test_user_index_none_values():
-    c.user.email_addresses = [None]
-    c.user.set_pref('telnumbers', [None])
-    c.user.set_pref('webpages', [None])
-    idx = c.user.index()
-    assert idx['email_addresses_t'] == ''
-    assert idx['telnumbers_t'] == ''
-    assert idx['webpages_t'] == ''
-
-
-@with_setup(setup_method)
-def test_user_backfill_login_details():
-    with h.push_config(request, user_agent='TestBrowser/55'):
-        # these shouldn't match
-        h.auditlog_user('something happened')
-        h.auditlog_user('blah blah Password changed')
-    with h.push_config(request, user_agent='TestBrowser/56'):
-        # these should all match, but only one entry created for this ip/ua
-        h.auditlog_user('Account activated')
-        h.auditlog_user('Successful login')
-        h.auditlog_user('Password changed')
-    with h.push_config(request, user_agent='TestBrowser/57'):
-        # this should match too
-        h.auditlog_user('Set up multifactor TOTP')
-    ThreadLocalORMSession.flush_all()
-
-    auth_provider = plugin.AuthenticationProvider.get(None)
-    c.user.backfill_login_details(auth_provider)
-
-    details = M.UserLoginDetails.query.find({'user_id': c.user._id}).sort('ua').all()
-    assert len(details) == 2, details
-    assert details[0].ip == '127.0.0.1'
-    assert details[0].ua == 'TestBrowser/56'
-    assert details[1].ip == '127.0.0.1'
-    assert details[1].ua == 'TestBrowser/57'
+class TestAuth:
+
+    def setup_method(self):
+        setup_basic_test()
+        setup_global_objects()
+
+    def test_email_address(self):
+        addr = M.EmailAddress(email='test_admin@domain.net',
+                              claimed_by_user_id=c.user._id)
+        ThreadLocalORMSession.flush_all()
+        assert addr.claimed_by_user() == c.user
+        addr2 = M.EmailAddress.create('test@domain.net')
+        addr3 = M.EmailAddress.create('test_admin@domain.net')
+        ThreadLocalORMSession.flush_all()
+
+        # Duplicate emails are allowed, until the email is confirmed
+        assert addr3 is not addr
+
+        assert addr2 is not addr
+        assert addr2
+        addr4 = M.EmailAddress.create('test@DOMAIN.NET')
+        assert addr4 is not addr2
+
+        assert addr is c.user.address_object('test_admin@domain.net')
+        c.user.claim_address('test@DOMAIN.NET')
+        assert 'test@domain.net' in c.user.email_addresses
+
+    def selftest_email_address_lookup_helpers():
+        addr = M.EmailAddress.create('TEST@DOMAIN.NET')
+        nobody = M.EmailAddress.create('nobody@example.com')
+        ThreadLocalORMSession.flush_all()
+        assert addr.email == 'TEST@domain.net'
+
+        assert M.EmailAddress.get(email='TEST@DOMAIN.NET') == addr
+        assert M.EmailAddress.get(email='TEST@domain.net') == addr
+        assert M.EmailAddress.get(email='test@domain.net') is None
+        assert M.EmailAddress.get(email=None) is None
+        assert M.EmailAddress.get(email='nobody@example.com') == nobody
+        # invalid email returns None, but not nobody@example.com as before
+        assert M.EmailAddress.get(email='invalid') is None
+
+        assert M.EmailAddress.find(dict(email='TEST@DOMAIN.NET')).all() == [addr]
+        assert M.EmailAddress.find(dict(email='TEST@domain.net')).all() == [addr]
+        assert M.EmailAddress.find(dict(email='test@domain.net')).all() == []
+        assert M.EmailAddress.find(dict(email=None)).all() == []
+        assert M.EmailAddress.find(dict(email='nobody@example.com')).all() == [nobody]
+        # invalid email returns empty query, but not nobody@example.com as before
+        assert M.EmailAddress.find(dict(email='invalid')).all() == []
+
+    def test_email_address_canonical(self):
+        assert M.EmailAddress.canonical('nobody@EXAMPLE.COM') == \
+               'nobody@example.com'
+        assert M.EmailAddress.canonical('nobody@example.com') == \
+               'nobody@example.com'
+        assert M.EmailAddress.canonical('I Am Nobody <no...@example.com>') == \
+               'nobody@example.com'
+        assert M.EmailAddress.canonical('  nobody@example.com\t') == \
+               'nobody@example.com'
+        assert M.EmailAddress.canonical('I Am@Nobody <no...@example.com> ') == \
+               'nobody@example.com'
+        assert M.EmailAddress.canonical(' No@body <no...@example.com> ') == \
+               'no@body@example.com'
+        assert M.EmailAddress.canonical('no@body@example.com') == \
+               'no@body@example.com'
+        assert M.EmailAddress.canonical('invalid') is None
+
+    def test_email_address_send_verification_link(self):
+        addr = M.EmailAddress(email='test_admin@domain.net',
+                              claimed_by_user_id=c.user._id)
+
+        addr.send_verification_link()
+
+        with patch('allura.tasks.mail_tasks.smtp_client._client') as _client:
+            M.MonQTask.run_ready()
+        return_path, rcpts, body = _client.sendmail.call_args[0]
+        assert rcpts == ['test_admin@domain.net']
+
+    @td.with_user_project('test-admin')
+    def test_user(self):
+        assert c.user.url() .endswith('/u/test-admin/')
+        assert c.user.script_name .endswith('/u/test-admin/')
+        assert ({p.shortname for p in c.user.my_projects()} ==
+                {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
+        # delete one of the projects and make sure it won't appear in my_projects()
+        p = M.Project.query.get(shortname='test2')
+        p.deleted = True
+        ThreadLocalORMSession.flush_all()
+        assert ({p.shortname for p in c.user.my_projects()} ==
+                {'test', 'u/test-admin', 'adobe-1', '--init--'})
+        u = M.User.register(dict(
+            username='nosetest-user'))
+        ThreadLocalORMSession.flush_all()
+        assert u.reg_date
+        assert u.private_project().shortname == 'u/nosetest-user'
+        roles = g.credentials.user_roles(
+            u._id, project_id=u.private_project().root_project._id)
+        assert len(roles) == 3, roles
+        u.set_password('foo')
+        provider = plugin.LocalAuthenticationProvider(Request.blank('/'))
+        assert provider._validate_password(u, 'foo')
+        assert not provider._validate_password(u, 'foobar')
+        u.set_password('foobar')
+        assert provider._validate_password(u, 'foobar')
+        assert not provider._validate_password(u, 'foo')
+
+    def test_user_project_creates_on_demand(self):
+        u = M.User.register(dict(username='foobar123'), make_project=False)
+        ThreadLocalORMSession.flush_all()
+        assert not M.Project.query.get(shortname='u/foobar123')
+        assert u.private_project()
+        assert M.Project.query.get(shortname='u/foobar123')
+
+    def test_user_project_already_deleted_creates_on_demand(self):
+        u = M.User.register(dict(username='foobar123'), make_project=True)
+        p = M.Project.query.get(shortname='u/foobar123')
+        p.deleted = True
+        ThreadLocalORMSession.flush_all()
+        assert not M.Project.query.get(shortname='u/foobar123', deleted=False)
+        assert u.private_project()
+        ThreadLocalORMSession.flush_all()
+        assert M.Project.query.get(shortname='u/foobar123', deleted=False)
+
+    def test_user_project_does_not_create_on_demand_for_disabled_user(self):
+        u = M.User.register(
+            dict(username='foobar123', disabled=True), make_project=False)
+        ThreadLocalORMSession.flush_all()
+        assert not u.private_project()
+        assert not M.Project.query.get(shortname='u/foobar123')
+
+    def test_user_project_does_not_create_on_demand_for_anonymous_user(self):
+        u = M.User.anonymous()
+        ThreadLocalORMSession.flush_all()
+        assert not u.private_project()
+        assert not M.Project.query.get(shortname='u/anonymous')
+        assert not M.Project.query.get(shortname='u/*anonymous')
+
+    @patch('allura.model.auth.log')
+    def test_user_by_email_address(self, log):
+        u1 = M.User.register(dict(username='abc1'), make_project=False)
+        u2 = M.User.register(dict(username='abc2'), make_project=False)
+        addr1 = M.EmailAddress(email='abc123@abc.me', confirmed=True,
+                               claimed_by_user_id=u1._id)
+        addr2 = M.EmailAddress(email='abc123@abc.me', confirmed=True,
+                               claimed_by_user_id=u2._id)
+        # both users are disabled
+        u1.disabled, u2.disabled = True, True
+        ThreadLocalORMSession.flush_all()
+        assert M.User.by_email_address('abc123@abc.me') is None
+        assert log.warn.call_count == 0
+
+        # only u2 is active
+        u1.disabled, u2.disabled = True, False
+        ThreadLocalORMSession.flush_all()
+        assert M.User.by_email_address('abc123@abc.me') == u2
+        assert log.warn.call_count == 0
+
+        # both are active
+        u1.disabled, u2.disabled = False, False
+        ThreadLocalORMSession.flush_all()
+        assert M.User.by_email_address('abc123@abc.me') in [u1, u2]
+        assert log.warn.call_count == 1
+
+        # invalid email returns None, but not user which claimed
+        # nobody@example.com as before
+        nobody = M.EmailAddress(email='nobody@example.com', confirmed=True,
+                                claimed_by_user_id=u1._id)
+        ThreadLocalORMSession.flush_all()
+        assert M.User.by_email_address('nobody@example.com') == u1
+        assert M.User.by_email_address('invalid') is None
+
+    def test_user_equality(self):
+        assert M.User.by_username('test-user') == M.User.by_username('test-user')
+        assert M.User.anonymous() == M.User.anonymous()
+        assert M.User.by_username('*anonymous') == M.User.anonymous()
+
+        assert M.User.by_username('test-user') != M.User.by_username('test-admin')
+        assert M.User.by_username('test-user') != M.User.anonymous()
+        assert M.User.anonymous() is not None
+        assert M.User.anonymous() != 12345
+        assert M.User.anonymous() != M.User()
+
+    def test_user_hash(self):
+        assert M.User.by_username('test-user') in {M.User.by_username('test-user')}
+        assert M.User.anonymous() in {M.User.anonymous()}
+        assert M.User.by_username('*anonymous') in {M.User.anonymous()}
+
+        assert M.User.by_username('test-user') not in {M.User.by_username('test-admin')}
+        assert M.User.anonymous() not in {M.User.by_username('test-admin')}
+        assert M.User.anonymous() not in {0, None}
+
+    def test_project_role(self):
+        role = M.ProjectRole(project_id=c.project._id, name='test_role')
+        M.ProjectRole.by_user(c.user, upsert=True).roles.append(role._id)
+        ThreadLocalORMSession.flush_all()
+        roles = g.credentials.user_roles(
+            c.user._id, project_id=c.project.root_project._id)
+        roles_ids = [r['_id'] for r in roles]
+        roles = M.ProjectRole.query.find({'_id': {'$in': roles_ids}})
+        for pr in roles:
+            assert pr.display()
+            pr.special
+            assert pr.user in (c.user, None, M.User.anonymous())
+
+    def test_default_project_roles(self):
+        roles = {
+            pr.name: pr
+            for pr in M.ProjectRole.query.find(dict(
+                project_id=c.project._id)).all()
+            if pr.name}
+        assert 'Admin' in list(roles.keys()), list(roles.keys())
+        assert 'Developer' in list(roles.keys()), list(roles.keys())
+        assert 'Member' in list(roles.keys()), list(roles.keys())
+        assert roles['Developer']._id in roles['Admin'].roles
+        assert roles['Member']._id in roles['Developer'].roles
+
+        # There're 1 user assigned to project, represented by
+        # relational (vs named) ProjectRole's
+        assert len(roles) == M.ProjectRole.query.find(dict(
+            project_id=c.project._id)).count() - 1
+
+    def test_email_address_claimed_by_user(self):
+        addr = M.EmailAddress(email='test_admin@domain.net',
+                              claimed_by_user_id=c.user._id)
+        c.user.disabled = True
+        ThreadLocalORMSession.flush_all()
+        assert addr.claimed_by_user() is None
+
+    @td.with_user_project('test-admin')
+    def test_user_projects_by_role(self):
+        assert ({p.shortname for p in c.user.my_projects()} ==
+                {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
+        assert ({p.shortname for p in c.user.my_projects_by_role_name('Admin')} ==
+                {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
+        # Remove admin access from c.user to test2 project
+        project = M.Project.query.get(shortname='test2')
+        admin_role = M.ProjectRole.by_name('Admin', project)
+        developer_role = M.ProjectRole.by_name('Developer', project)
+        user_role = M.ProjectRole.by_user(c.user, project=project, upsert=True)
+        user_role.roles.remove(admin_role._id)
+        user_role.roles.append(developer_role._id)
+        ThreadLocalORMSession.flush_all()
+        g.credentials.clear()
+        assert ({p.shortname for p in c.user.my_projects()} ==
+                {'test', 'test2', 'u/test-admin', 'adobe-1', '--init--'})
+        assert ({p.shortname for p in c.user.my_projects_by_role_name('Admin')} ==
+                {'test', 'u/test-admin', 'adobe-1', '--init--'})
+
+    @td.with_user_project('test-admin')
+    def test_user_projects_unnamed(self):
+        """
+        Confirm that spurious ProjectRoles associating a user with
+        a project to which they do not belong to any named group
+        don't cause the user to count as a member of the project.
+        """
+        sub1 = M.Project.query.get(shortname='test/sub1')
+        M.ProjectRole(
+            user_id=c.user._id,
+            project_id=sub1._id)
+        ThreadLocalORMSession.flush_all()
+        project_names = [p.shortname for p in c.user.my_projects()]
+        assert 'test/sub1' not in project_names
+        assert 'test' in project_names
+
+    @patch.object(g, 'user_message_max_messages', 3)
+    def test_check_sent_user_message_times(self):
+        user1 = M.User.register(dict(username='test-user'), make_project=False)
+        time1 = datetime.utcnow() - timedelta(minutes=30)
+        time2 = datetime.utcnow() - timedelta(minutes=45)
+        time3 = datetime.utcnow() - timedelta(minutes=70)
+        user1.sent_user_message_times = [time1, time2, time3]
+        assert user1.can_send_user_message()
+        assert len(user1.sent_user_message_times) == 2
+        user1.sent_user_message_times.append(
+            datetime.utcnow() - timedelta(minutes=15))
+        assert not user1.can_send_user_message()
+
+    @td.with_user_project('test-admin')
+    def test_user_track_active(self):
+        # without this session flushing inside track_active raises Exception
+        setup_functional_test()
+        c.user = M.User.by_username('test-admin')
+
+        assert c.user.last_access['session_date'] is None
+        assert c.user.last_access['session_ip'] is None
+        assert c.user.last_access['session_ua'] is None
+
+        req = Mock(headers={'User-Agent': 'browser'}, remote_addr='addr')
+        c.user.track_active(req)
+        c.user = M.User.by_username(c.user.username)
+        assert c.user.last_access['session_date'] is not None
+        assert c.user.last_access['session_ip'] == 'addr'
+        assert c.user.last_access['session_ua'] == 'browser'
+
+        # ensure that session activity tracked with a whole-day granularity
+        prev_date = c.user.last_access['session_date']
+        c.user.track_active(req)
+        c.user = M.User.by_username(c.user.username)
+        assert c.user.last_access['session_date'] == prev_date
+        yesterday = datetime.utcnow() - timedelta(1)
+        c.user.last_access['session_date'] = yesterday
+        session(c.user).flush(c.user)
+        c.user.track_active(req)
+        c.user = M.User.by_username(c.user.username)
+        assert c.user.last_access['session_date'] > yesterday
+
+        # ...or if IP or User Agent has changed
+        req.remote_addr = 'new addr'
+        c.user.track_active(req)
+        c.user = M.User.by_username(c.user.username)
+        assert c.user.last_access['session_ip'] == 'new addr'
+        assert c.user.last_access['session_ua'] == 'browser'
+        req.headers['User-Agent'] = 'new browser'
+        c.user.track_active(req)
+        c.user = M.User.by_username(c.user.username)
+        assert c.user.last_access['session_ip'] == 'new addr'
+        assert c.user.last_access['session_ua'] == 'new browser'
+
+    def test_user_index(self):
+        c.user.email_addresses = ['email1', 'email2']
+        c.user.set_pref('email_address', 'email2')
+        idx = c.user.index()
+        assert idx['id'] == c.user.index_id()
+        assert idx['title'] == 'User test-admin'
+        assert idx['type_s'] == 'User'
+        assert idx['username_s'] == 'test-admin'
+        assert idx['email_addresses_t'] == 'email1 email2'
+        assert idx['email_address_s'] == 'email2'
+        assert 'last_password_updated_dt' in idx
+        assert idx['disabled_b'] is False
+        assert 'results_per_page_i' in idx
+        assert 'email_format_s' in idx
+        assert 'disable_user_messages_b' in idx
+        assert idx['display_name_t'] == 'Test Admin'
+        assert idx['sex_s'] == 'Unknown'
+        assert 'birthdate_dt' in idx
+        assert 'localization_s' in idx
+        assert 'timezone_s' in idx
+        assert 'socialnetworks_t' in idx
+        assert 'telnumbers_t' in idx
+        assert 'skypeaccount_s' in idx
+        assert 'webpages_t' in idx
+        assert 'skills_t' in idx
+        assert 'last_access_login_date_dt' in idx
+        assert 'last_access_login_ip_s' in idx
+        assert 'last_access_login_ua_t' in idx
+        assert 'last_access_session_date_dt' in idx
+        assert 'last_access_session_ip_s' in idx
+        assert 'last_access_session_ua_t' in idx
+        # provided bby auth provider
+        assert 'user_registration_date_dt' in idx
+
+    def test_user_index_none_values(self):
+        c.user.email_addresses = [None]
+        c.user.set_pref('telnumbers', [None])
+        c.user.set_pref('webpages', [None])
+        idx = c.user.index()
+        assert idx['email_addresses_t'] == ''
+        assert idx['telnumbers_t'] == ''
+        assert idx['webpages_t'] == ''
+
+    def test_user_backfill_login_details(self):
+        with h.push_config(r, user_agent='TestBrowser/55'):
+            # these shouldn't match
+            h.auditlog_user('something happened')
+            h.auditlog_user('blah blah Password changed')
+        with h.push_config(r, user_agent='TestBrowser/56'):
+            # these should all match, but only one entry created for this ip/ua
+            h.auditlog_user('Account activated')
+            h.auditlog_user('Successful login')
+            h.auditlog_user('Password changed')
+        with h.push_config(r, user_agent='TestBrowser/57'):
+            # this should match too
+            h.auditlog_user('Set up multifactor TOTP')
+        ThreadLocalORMSession.flush_all()
+
+        auth_provider = plugin.AuthenticationProvider.get(None)
+        c.user.backfill_login_details(auth_provider)
+
+        details = M.UserLoginDetails.query.find({'user_id': c.user._id}).sort('ua').all()
+        assert len(details) == 2, details
+        assert details[0].ip == '127.0.0.1'
+        assert details[0].ua == 'TestBrowser/56'
+        assert details[1].ip == '127.0.0.1'
+        assert details[1].ua == 'TestBrowser/57'
 
 
 @with_nose_compatibility
 class TestAuditLog:
 
+    @classmethod
+    def setup_class(cls):
+        setup_basic_test()
+        setup_global_objects()
+
     def test_message_html(self):
         al = h.auditlog_user('our message <script>alert(1)</script>')
         assert al.message == textwrap.dedent('''\
diff --git a/Allura/allura/tests/model/test_discussion.py b/Allura/allura/tests/model/test_discussion.py
index d5abf1a05..3c158cd3f 100644
--- a/Allura/allura/tests/model/test_discussion.py
+++ b/Allura/allura/tests/model/test_discussion.py
@@ -24,10 +24,8 @@ from datetime import datetime, timedelta
 from cgi import FieldStorage
 
 from tg import tmpl_context as c
-from alluratest.tools import assert_equals, with_setup
 import mock
 from mock import patch
-from alluratest.tools import assert_equal, assert_in
 
 from ming.orm import session, ThreadLocalORMSession
 from webob import exc
@@ -38,513 +36,469 @@ from allura.tests import TestController
 from alluratest.controller import setup_global_objects
 
 
-def setup_method():
-    controller = TestController()
-    controller.setup_method(None)
-    controller.app.get('/wiki/Home/')
-    setup_global_objects()
-    ThreadLocalORMSession.close_all()
-    h.set_context('test', 'wiki', neighborhood='Projects')
-    ThreadLocalORMSession.flush_all()
-    ThreadLocalORMSession.close_all()
-
-
-def teardown_module():
-    ThreadLocalORMSession.close_all()
-
-
-@with_setup(setup_method)
-def test_discussion_methods():
-    d = M.Discussion(shortname='test', name='test')
-    assert d.thread_class() == M.Thread
-    assert d.post_class() == M.Post
-    assert d.attachment_class() == M.DiscussionAttachment
-    ThreadLocalORMSession.flush_all()
-    d.update_stats()
-    ThreadLocalORMSession.flush_all()
-    assert d.last_post is None
-    assert d.url().endswith('wiki/_discuss/')
-    assert d.index()['name_s'] == 'test'
-    assert d.find_posts().count() == 0
-    jsn = d.__json__()
-    assert jsn['name'] == d.name
-    d.delete()
-    ThreadLocalORMSession.flush_all()
-    ThreadLocalORMSession.close_all()
-
-
-@with_setup(setup_method)
-def test_thread_methods():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    assert t.discussion_class() == M.Discussion
-    assert t.post_class() == M.Post
-    assert t.attachment_class() == M.DiscussionAttachment
-    p0 = t.post('This is a post')
-    p1 = t.post('This is another post')
-    time.sleep(0.25)
-    t.post('This is a reply', parent_id=p0._id)
-    ThreadLocalORMSession.flush_all()
-    ThreadLocalORMSession.close_all()
-    d = M.Discussion.query.get(shortname='test')
-    t = d.threads[0]
-    assert d.last_post is not None
-    assert t.last_post is not None
-    t.create_post_threads(t.posts)
-    posts0 = t.find_posts(page=0, limit=10, style='threaded')
-    posts1 = t.find_posts(page=0, limit=10, style='timestamp')
-    assert posts0 != posts1
-    ts = p0.timestamp.replace(
-        microsecond=int(p0.timestamp.microsecond // 1000) * 1000)
-    posts2 = t.find_posts(page=0, limit=10, style='threaded', timestamp=ts)
-    assert len(posts2) > 0
-
-    assert 'wiki/_discuss/' in t.url()
-    assert t.index()['views_i'] == 0
-    assert t.post_count == 3
-    jsn = t.__json__()
-    assert '_id' in jsn
-    assert len(jsn['posts']) == 3
-    (p.approve() for p in (p0, p1))
-    ThreadLocalORMSession.flush_all()
-    assert t.num_replies == 3
-    t.spam()
-    assert t.num_replies == 0
-    ThreadLocalORMSession.flush_all()
-    assert len(t.find_posts()) == 0
-    t.delete()
-
-
-@with_setup(setup_method)
-def test_thread_new():
-    with mock.patch('allura.model.discuss.h.nonce') as nonce:
-        nonce.side_effect = ['deadbeef', 'deadbeef', 'beefdead']
+class TestDiscussion:
+
+    def setup_method(self):
+        controller = TestController()
+        controller.setup_method(None)
+        controller.app.get('/wiki/Home/')
+        setup_global_objects()
+        ThreadLocalORMSession.close_all()
+        h.set_context('test', 'wiki', neighborhood='Projects')
+        ThreadLocalORMSession.flush_all()
+        ThreadLocalORMSession.close_all()
+
+    @classmethod
+    def teardown_class(cls):
+        ThreadLocalORMSession.close_all()
+
+    def test_discussion_methods(self):
         d = M.Discussion(shortname='test', name='test')
-        t1 = M.Thread.new(discussion_id=d._id, subject='Test Thread One')
-        t2 = M.Thread.new(discussion_id=d._id, subject='Test Thread Two')
+        assert d.thread_class() == M.Thread
+        assert d.post_class() == M.Post
+        assert d.attachment_class() == M.DiscussionAttachment
         ThreadLocalORMSession.flush_all()
-        session(t1).expunge(t1)
-        session(t2).expunge(t2)
-        t1_2 = M.Thread.query.get(_id=t1._id)
-        t2_2 = M.Thread.query.get(_id=t2._id)
-        assert t1._id == 'deadbeef'
-        assert t2._id == 'beefdead'
-        assert t1_2.subject == 'Test Thread One'
-        assert t2_2.subject == 'Test Thread Two'
-
-
-@with_setup(setup_method)
-def test_post_methods():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    p = t.post('This is a post')
-    p2 = t.post('This is another post')
-    assert p.discussion_class() == M.Discussion
-    assert p.thread_class() == M.Thread
-    assert p.attachment_class() == M.DiscussionAttachment
-    p.commit()
-    assert p.parent is None
-    assert p.subject == 'Test Thread'
-    assert p.attachments == []
-    assert 'wiki/_discuss' in p.url()
-    assert p.reply_subject() == 'Re: Test Thread'
-    assert p.link_text() == p.subject
-
-    ss = p.history().first()
-    assert 'version' in h.get_first(ss.index(), 'title')
-    assert '#' in ss.shorthand_id()
-
-    jsn = p.__json__()
-    assert jsn["thread_id"] == t._id
-
-    (p.approve() for p in (p, p2))
-    ThreadLocalORMSession.flush_all()
-    assert t.num_replies == 2
-    p.spam()
-    assert t.num_replies == 1
-    p.undo('ok')
-    assert t.num_replies == 2
-    p.delete()
-    assert t.num_replies == 1
-
-
-@with_setup(setup_method)
-def test_attachment_methods():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    p = t.post('This is a post')
-    p_att = p.attach('foo.text', BytesIO(b'Hello, world!'),
-                     discussion_id=d._id,
-                     thread_id=t._id,
-                     post_id=p._id)
-    t_att = p.attach('foo2.text', BytesIO(b'Hello, thread!'),
-                     discussion_id=d._id,
-                     thread_id=t._id)
-    d_att = p.attach('foo3.text', BytesIO(b'Hello, discussion!'),
-                     discussion_id=d._id)
-
-    ThreadLocalORMSession.flush_all()
-    assert p_att.post == p
-    assert p_att.thread == t
-    assert p_att.discussion == d
-    for att in (p_att, t_att, d_att):
-        assert 'wiki/_discuss' in att.url()
-        assert 'attachment/' in att.url()
-
-    # Test notification in mail
-    t = M.Thread.new(discussion_id=d._id, subject='Test comment notification')
-    fs = FieldStorage()
-    fs.name = 'file_info'
-    fs.filename = 'fake.txt'
-    fs.type = 'text/plain'
-    fs.file = BytesIO(b'this is the content of the fake file\n')
-    p = t.post(text='test message', forum=None, subject='', file_info=fs)
-    ThreadLocalORMSession.flush_all()
-    n = M.Notification.query.get(
-        subject='[test:wiki] Test comment notification')
-    url = h.absurl(f'{p.url()}attachment/{fs.filename}')
-    assert (
-        '\nAttachments:\n\n'
-        '- [fake.txt]({}) (37 Bytes; text/plain)'.format(url) in
-        n.text)
-
-
-@with_setup(setup_method)
-def test_multiple_attachments():
-    test_file1 = FieldStorage()
-    test_file1.name = 'file_info'
-    test_file1.filename = 'test1.txt'
-    test_file1.type = 'text/plain'
-    test_file1.file = BytesIO(b'test file1\n')
-    test_file2 = FieldStorage()
-    test_file2.name = 'file_info'
-    test_file2.filename = 'test2.txt'
-    test_file2.type = 'text/plain'
-    test_file2.file = BytesIO(b'test file2\n')
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    test_post = t.post('test post')
-    test_post.add_multiple_attachments([test_file1, test_file2])
-    ThreadLocalORMSession.flush_all()
-    assert len(test_post.attachments) == 2
-    attaches = test_post.attachments
-    assert 'test1.txt' in [attaches[0].filename, attaches[1].filename]
-    assert 'test2.txt' in [attaches[0].filename, attaches[1].filename]
-
-
-@with_setup(setup_method)
-def test_add_attachment():
-    test_file = FieldStorage()
-    test_file.name = 'file_info'
-    test_file.filename = 'test.txt'
-    test_file.type = 'text/plain'
-    test_file.file = BytesIO(b'test file\n')
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    test_post = t.post('test post')
-    test_post.add_attachment(test_file)
-    ThreadLocalORMSession.flush_all()
-    assert len(test_post.attachments) == 1
-    attach = test_post.attachments[0]
-    assert attach.filename == 'test.txt', attach.filename
-    assert attach.content_type == 'text/plain', attach.content_type
-
-
-def test_notification_two_attaches():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test comment notification')
-    fs1 = FieldStorage()
-    fs1.name = 'file_info'
-    fs1.filename = 'fake.txt'
-    fs1.type = 'text/plain'
-    fs1.file = BytesIO(b'this is the content of the fake file\n')
-    fs2 = FieldStorage()
-    fs2.name = 'file_info'
-    fs2.filename = 'fake2.txt'
-    fs2.type = 'text/plain'
-    fs2.file = BytesIO(b'this is the content of the fake file\n')
-    p = t.post(text='test message', forum=None, subject='', file_info=[fs1, fs2])
-    ThreadLocalORMSession.flush_all()
-    n = M.Notification.query.get(
-        subject='[test:wiki] Test comment notification')
-    base_url = h.absurl(f'{p.url()}attachment/')
-    assert (
-        '\nAttachments:\n\n'
-        '- [fake.txt]({0}fake.txt) (37 Bytes; text/plain)\n'
-        '- [fake2.txt]({0}fake2.txt) (37 Bytes; text/plain)'.format(base_url) in
-        n.text)
-
-
-@with_setup(setup_method)
-def test_discussion_delete():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    p = t.post('This is a post')
-    p.attach('foo.text', BytesIO(b''),
-             discussion_id=d._id,
-             thread_id=t._id,
-             post_id=p._id)
-    M.ArtifactReference.from_artifact(d)
-    rid = d.index_id()
-    ThreadLocalORMSession.flush_all()
-    d.delete()
-    ThreadLocalORMSession.flush_all()
-    assert M.ArtifactReference.query.find(dict(_id=rid)).count() == 0
-
-
-@with_setup(setup_method)
-def test_thread_delete():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    p = t.post('This is a post')
-    p.attach('foo.text', BytesIO(b''),
-             discussion_id=d._id,
-             thread_id=t._id,
-             post_id=p._id)
-    ThreadLocalORMSession.flush_all()
-    t.delete()
-
-
-@with_setup(setup_method)
-def test_post_delete():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    p = t.post('This is a post')
-    p.attach('foo.text', BytesIO(b''),
-             discussion_id=d._id,
-             thread_id=t._id,
-             post_id=p._id)
-    ThreadLocalORMSession.flush_all()
-    p.delete()
-
-
-@with_setup(setup_method)
-def test_post_undo():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    p = t.post('This is a post')
-    t.post('This is a post2')
-    t.post('This is a post3')
-    ThreadLocalORMSession.flush_all()
-    assert t.num_replies == 3
-    p.spam()
-    assert t.num_replies == 2
-    p.undo('ok')
-    assert t.num_replies == 3
-
-
-@with_setup(setup_method)
-def test_post_permission_check():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    c.user = M.User.anonymous()
-    try:
-        t.post('This post will fail the check.')
-        assert False, "Expected an anonymous post to fail."
-    except exc.HTTPUnauthorized:
-        pass
-    t.post('This post will pass the check.', ignore_security=True)
-
-
-@with_setup(setup_method)
-def test_post_url_paginated():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    p = []  # posts in display order
-    ts = datetime.utcnow() - timedelta(days=1)
-    for i in range(5):
-        ts += timedelta(minutes=1)
-        p.append(t.post('This is a post #%s' % i, timestamp=ts))
-
-    ts += timedelta(minutes=1)
-    p.insert(1, t.post(
-        'This is reply #0 to post #0', parent_id=p[0]._id, timestamp=ts))
-
-    ts += timedelta(minutes=1)
-    p.insert(2, t.post(
-        'This is reply #1 to post #0', parent_id=p[0]._id, timestamp=ts))
-
-    ts += timedelta(minutes=1)
-    p.insert(4, t.post(
-        'This is reply #0 to post #1', parent_id=p[3]._id, timestamp=ts))
-
-    ts += timedelta(minutes=1)
-    p.insert(6, t.post(
-        'This is reply #0 to post #2', parent_id=p[5]._id, timestamp=ts))
-
-    ts += timedelta(minutes=1)
-    p.insert(7, t.post(
-        'This is reply #1 to post #2', parent_id=p[5]._id, timestamp=ts))
-
-    ts += timedelta(minutes=1)
-    p.insert(8, t.post(
-        'This is reply #0 to reply #1 to post #2',
-        parent_id=p[7]._id, timestamp=ts))
-
-    # with default paging limit
-    for _p in p:
-        url = t.url() + '?limit=25#' + _p.slug
-        assert _p.url_paginated() == url, _p.url_paginated()
-
-    # with user paging limit
-    limit = 3
-    c.user.set_pref('results_per_page', limit)
-    for i, _p in enumerate(p):
-        page = i // limit
-        url = t.url() + '?limit=%s' % limit
-        if page > 0:
-            url += '&page=%s' % page
-        url += '#' + _p.slug
-        assert _p.url_paginated() == url
-
-
-@with_setup(setup_method)
-def test_post_url_paginated_with_artifact():
-    """Post.url_paginated should return link to attached artifact, if any"""
-    from forgewiki.model import Page
-    page = Page.upsert(title='Test Page')
-    thread = page.discussion_thread
-    comment = thread.post('Comment')
-    url = page.url() + '?limit=25#' + comment.slug
-    assert comment.url_paginated() == url
-
-
-@with_setup(setup_method)
-def test_post_notify():
-    d = M.Discussion(shortname='test', name='test')
-    d.monitoring_email = 'darthvader@deathstar.org'
-    t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
-    with patch('allura.model.notification.Notification.send_simple') as send:
-        t.post('This is a post')
-        send.assert_called_with(d.monitoring_email)
+        d.update_stats()
+        ThreadLocalORMSession.flush_all()
+        assert d.last_post is None
+        assert d.url().endswith('wiki/_discuss/')
+        assert d.index()['name_s'] == 'test'
+        assert d.find_posts().count() == 0
+        jsn = d.__json__()
+        assert jsn['name'] == d.name
+        d.delete()
+        ThreadLocalORMSession.flush_all()
+        ThreadLocalORMSession.close_all()
+
+    def test_thread_methods(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        assert t.discussion_class() == M.Discussion
+        assert t.post_class() == M.Post
+        assert t.attachment_class() == M.DiscussionAttachment
+        p0 = t.post('This is a post')
+        p1 = t.post('This is another post')
+        time.sleep(0.25)
+        t.post('This is a reply', parent_id=p0._id)
+        ThreadLocalORMSession.flush_all()
+        ThreadLocalORMSession.close_all()
+        d = M.Discussion.query.get(shortname='test')
+        t = d.threads[0]
+        assert d.last_post is not None
+        assert t.last_post is not None
+        t.create_post_threads(t.posts)
+        posts0 = t.find_posts(page=0, limit=10, style='threaded')
+        posts1 = t.find_posts(page=0, limit=10, style='timestamp')
+        assert posts0 != posts1
+        ts = p0.timestamp.replace(
+            microsecond=int(p0.timestamp.microsecond // 1000) * 1000)
+        posts2 = t.find_posts(page=0, limit=10, style='threaded', timestamp=ts)
+        assert len(posts2) > 0
+
+        assert 'wiki/_discuss/' in t.url()
+        assert t.index()['views_i'] == 0
+        assert t.post_count == 3
+        jsn = t.__json__()
+        assert '_id' in jsn
+        assert len(jsn['posts']) == 3
+        (p.approve() for p in (p0, p1))
+        ThreadLocalORMSession.flush_all()
+        assert t.num_replies == 3
+        t.spam()
+        assert t.num_replies == 0
+        ThreadLocalORMSession.flush_all()
+        assert len(t.find_posts()) == 0
+        t.delete()
+
+    def test_thread_new(self):
+        with mock.patch('allura.model.discuss.h.nonce') as nonce:
+            nonce.side_effect = ['deadbeef', 'deadbeef', 'beefdead']
+            d = M.Discussion(shortname='test', name='test')
+            t1 = M.Thread.new(discussion_id=d._id, subject='Test Thread One')
+            t2 = M.Thread.new(discussion_id=d._id, subject='Test Thread Two')
+            ThreadLocalORMSession.flush_all()
+            session(t1).expunge(t1)
+            session(t2).expunge(t2)
+            t1_2 = M.Thread.query.get(_id=t1._id)
+            t2_2 = M.Thread.query.get(_id=t2._id)
+            assert t1._id == 'deadbeef'
+            assert t2._id == 'beefdead'
+            assert t1_2.subject == 'Test Thread One'
+            assert t2_2.subject == 'Test Thread Two'
+
+    def test_post_methods(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        p = t.post('This is a post')
+        p2 = t.post('This is another post')
+        assert p.discussion_class() == M.Discussion
+        assert p.thread_class() == M.Thread
+        assert p.attachment_class() == M.DiscussionAttachment
+        p.commit()
+        assert p.parent is None
+        assert p.subject == 'Test Thread'
+        assert p.attachments == []
+        assert 'wiki/_discuss' in p.url()
+        assert p.reply_subject() == 'Re: Test Thread'
+        assert p.link_text() == p.subject
+
+        ss = p.history().first()
+        assert 'version' in h.get_first(ss.index(), 'title')
+        assert '#' in ss.shorthand_id()
+
+        jsn = p.__json__()
+        assert jsn["thread_id"] == t._id
+
+        (p.approve() for p in (p, p2))
+        ThreadLocalORMSession.flush_all()
+        assert t.num_replies == 2
+        p.spam()
+        assert t.num_replies == 1
+        p.undo('ok')
+        assert t.num_replies == 2
+        p.delete()
+        assert t.num_replies == 1
+
+    def test_attachment_methods(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        p = t.post('This is a post')
+        p_att = p.attach('foo.text', BytesIO(b'Hello, world!'),
+                         discussion_id=d._id,
+                         thread_id=t._id,
+                         post_id=p._id)
+        t_att = p.attach('foo2.text', BytesIO(b'Hello, thread!'),
+                         discussion_id=d._id,
+                         thread_id=t._id)
+        d_att = p.attach('foo3.text', BytesIO(b'Hello, discussion!'),
+                         discussion_id=d._id)
+
+        ThreadLocalORMSession.flush_all()
+        assert p_att.post == p
+        assert p_att.thread == t
+        assert p_att.discussion == d
+        for att in (p_att, t_att, d_att):
+            assert 'wiki/_discuss' in att.url()
+            assert 'attachment/' in att.url()
+
+        # Test notification in mail
+        t = M.Thread.new(discussion_id=d._id, subject='Test comment notification')
+        fs = FieldStorage()
+        fs.name = 'file_info'
+        fs.filename = 'fake.txt'
+        fs.type = 'text/plain'
+        fs.file = BytesIO(b'this is the content of the fake file\n')
+        p = t.post(text='test message', forum=None, subject='', file_info=fs)
+        ThreadLocalORMSession.flush_all()
+        n = M.Notification.query.get(
+            subject='[test:wiki] Test comment notification')
+        url = h.absurl(f'{p.url()}attachment/{fs.filename}')
+        assert (
+            '\nAttachments:\n\n'
+            '- [fake.txt]({}) (37 Bytes; text/plain)'.format(url) in
+            n.text)
+
+    def test_multiple_attachments(self):
+        test_file1 = FieldStorage()
+        test_file1.name = 'file_info'
+        test_file1.filename = 'test1.txt'
+        test_file1.type = 'text/plain'
+        test_file1.file = BytesIO(b'test file1\n')
+        test_file2 = FieldStorage()
+        test_file2.name = 'file_info'
+        test_file2.filename = 'test2.txt'
+        test_file2.type = 'text/plain'
+        test_file2.file = BytesIO(b'test file2\n')
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        test_post = t.post('test post')
+        test_post.add_multiple_attachments([test_file1, test_file2])
+        ThreadLocalORMSession.flush_all()
+        assert len(test_post.attachments) == 2
+        attaches = test_post.attachments
+        assert 'test1.txt' in [attaches[0].filename, attaches[1].filename]
+        assert 'test2.txt' in [attaches[0].filename, attaches[1].filename]
+
+    def test_add_attachment(self):
+        test_file = FieldStorage()
+        test_file.name = 'file_info'
+        test_file.filename = 'test.txt'
+        test_file.type = 'text/plain'
+        test_file.file = BytesIO(b'test file\n')
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        test_post = t.post('test post')
+        test_post.add_attachment(test_file)
+        ThreadLocalORMSession.flush_all()
+        assert len(test_post.attachments) == 1
+        attach = test_post.attachments[0]
+        assert attach.filename == 'test.txt', attach.filename
+        assert attach.content_type == 'text/plain', attach.content_type
+
+    def test_notification_two_attaches(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test comment notification')
+        fs1 = FieldStorage()
+        fs1.name = 'file_info'
+        fs1.filename = 'fake.txt'
+        fs1.type = 'text/plain'
+        fs1.file = BytesIO(b'this is the content of the fake file\n')
+        fs2 = FieldStorage()
+        fs2.name = 'file_info'
+        fs2.filename = 'fake2.txt'
+        fs2.type = 'text/plain'
+        fs2.file = BytesIO(b'this is the content of the fake file\n')
+        p = t.post(text='test message', forum=None, subject='', file_info=[fs1, fs2])
+        ThreadLocalORMSession.flush_all()
+        n = M.Notification.query.get(
+            subject='[test:wiki] Test comment notification')
+        base_url = h.absurl(f'{p.url()}attachment/')
+        assert (
+            '\nAttachments:\n\n'
+            '- [fake.txt]({0}fake.txt) (37 Bytes; text/plain)\n'
+            '- [fake2.txt]({0}fake2.txt) (37 Bytes; text/plain)'.format(base_url) in
+            n.text)
+
+    def test_discussion_delete(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        p = t.post('This is a post')
+        p.attach('foo.text', BytesIO(b''),
+                 discussion_id=d._id,
+                 thread_id=t._id,
+                 post_id=p._id)
+        M.ArtifactReference.from_artifact(d)
+        rid = d.index_id()
+        ThreadLocalORMSession.flush_all()
+        d.delete()
+        ThreadLocalORMSession.flush_all()
+        assert M.ArtifactReference.query.find(dict(_id=rid)).count() == 0
+
+    def test_thread_delete(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        p = t.post('This is a post')
+        p.attach('foo.text', BytesIO(b''),
+                 discussion_id=d._id,
+                 thread_id=t._id,
+                 post_id=p._id)
+        ThreadLocalORMSession.flush_all()
+        t.delete()
+
+    def test_post_delete(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        p = t.post('This is a post')
+        p.attach('foo.text', BytesIO(b''),
+                 discussion_id=d._id,
+                 thread_id=t._id,
+                 post_id=p._id)
+        ThreadLocalORMSession.flush_all()
+        p.delete()
+
+    def test_post_undo(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        p = t.post('This is a post')
+        t.post('This is a post2')
+        t.post('This is a post3')
+        ThreadLocalORMSession.flush_all()
+        assert t.num_replies == 3
+        p.spam()
+        assert t.num_replies == 2
+        p.undo('ok')
+        assert t.num_replies == 3
 
-    c.app.config.project.notifications_disabled = True
-    with patch('allura.model.notification.Notification.send_simple') as send:
-        t.post('Another post')
+    def test_post_permission_check(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        c.user = M.User.anonymous()
         try:
+            t.post('This post will fail the check.')
+            assert False, "Expected an anonymous post to fail."
+        except exc.HTTPUnauthorized:
+            pass
+        t.post('This post will pass the check.', ignore_security=True)
+
+    def test_post_url_paginated(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        p = []  # posts in display order
+        ts = datetime.utcnow() - timedelta(days=1)
+        for i in range(5):
+            ts += timedelta(minutes=1)
+            p.append(t.post('This is a post #%s' % i, timestamp=ts))
+
+        ts += timedelta(minutes=1)
+        p.insert(1, t.post(
+            'This is reply #0 to post #0', parent_id=p[0]._id, timestamp=ts))
+
+        ts += timedelta(minutes=1)
+        p.insert(2, t.post(
+            'This is reply #1 to post #0', parent_id=p[0]._id, timestamp=ts))
+
+        ts += timedelta(minutes=1)
+        p.insert(4, t.post(
+            'This is reply #0 to post #1', parent_id=p[3]._id, timestamp=ts))
+
+        ts += timedelta(minutes=1)
+        p.insert(6, t.post(
+            'This is reply #0 to post #2', parent_id=p[5]._id, timestamp=ts))
+
+        ts += timedelta(minutes=1)
+        p.insert(7, t.post(
+            'This is reply #1 to post #2', parent_id=p[5]._id, timestamp=ts))
+
+        ts += timedelta(minutes=1)
+        p.insert(8, t.post(
+            'This is reply #0 to reply #1 to post #2',
+            parent_id=p[7]._id, timestamp=ts))
+
+        # with default paging limit
+        for _p in p:
+            url = t.url() + '?limit=25#' + _p.slug
+            assert _p.url_paginated() == url, _p.url_paginated()
+
+        # with user paging limit
+        limit = 3
+        c.user.set_pref('results_per_page', limit)
+        for i, _p in enumerate(p):
+            page = i // limit
+            url = t.url() + '?limit=%s' % limit
+            if page > 0:
+                url += '&page=%s' % page
+            url += '#' + _p.slug
+            assert _p.url_paginated() == url
+
+    def test_post_url_paginated_with_artifact(self):
+        """Post.url_paginated should return link to attached artifact, if any"""
+        from forgewiki.model import Page
+        page = Page.upsert(title='Test Page')
+        thread = page.discussion_thread
+        comment = thread.post('Comment')
+        url = page.url() + '?limit=25#' + comment.slug
+        assert comment.url_paginated() == url
+
+    def test_post_notify(self):
+        d = M.Discussion(shortname='test', name='test')
+        d.monitoring_email = 'darthvader@deathstar.org'
+        t = M.Thread.new(discussion_id=d._id, subject='Test Thread')
+        with patch('allura.model.notification.Notification.send_simple') as send:
+            t.post('This is a post')
             send.assert_called_with(d.monitoring_email)
-        except AssertionError:
-            pass  # method not called as expected
-        else:
-            assert False, 'send_simple must not be called'
-
-
-@with_setup(setup_method)
-@patch('allura.model.discuss.c.project.users_with_role')
-def test_is_spam_for_admin(users):
-    users.return_value = [c.user, ]
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    t.post('This is a post')
-    post = M.Post.query.get(text='This is a post')
-    assert not t.is_spam(post), t.is_spam(post)
-
-
-@with_setup(setup_method)
-@patch('allura.model.discuss.c.project.users_with_role')
-def test_is_spam(role):
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    role.return_value = []
-    with mock.patch('allura.controllers.discuss.g.spam_checker') as spam_checker:
+
+        c.app.config.project.notifications_disabled = True
+        with patch('allura.model.notification.Notification.send_simple') as send:
+            t.post('Another post')
+            try:
+                send.assert_called_with(d.monitoring_email)
+            except AssertionError:
+                pass  # method not called as expected
+            else:
+                assert False, 'send_simple must not be called'
+
+    @patch('allura.model.discuss.c.project.users_with_role')
+    def test_is_spam_for_admin(self, users):
+        users.return_value = [c.user, ]
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        t.post('This is a post')
+        post = M.Post.query.get(text='This is a post')
+        assert not t.is_spam(post), t.is_spam(post)
+
+    @patch('allura.model.discuss.c.project.users_with_role')
+    def test_is_spam(self, role):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        role.return_value = []
+        with mock.patch('allura.controllers.discuss.g.spam_checker') as spam_checker:
+            spam_checker.check.return_value = True
+            post = mock.Mock()
+            assert t.is_spam(post), t.is_spam(post)
+            assert spam_checker.check.call_count == 1, spam_checker.call_count
+
+    @mock.patch('allura.controllers.discuss.g.spam_checker')
+    def test_not_spam_and_has_unmoderated_post_permission(self, spam_checker):
+        spam_checker.check.return_value = False
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        role = M.ProjectRole.by_name('*anonymous')._id
+        post_permission = M.ACE.allow(role, 'post')
+        unmoderated_post_permission = M.ACE.allow(role, 'unmoderated_post')
+        t.acl.append(post_permission)
+        t.acl.append(unmoderated_post_permission)
+        with h.push_config(c, user=M.User.anonymous()):
+            post = t.post('Hey')
+        assert post.status == 'ok'
+
+    @mock.patch('allura.controllers.discuss.g.spam_checker')
+    @mock.patch.object(M.Thread, 'notify_moderators')
+    def test_not_spam_but_has_no_unmoderated_post_permission(self, notify_moderators, spam_checker):
+        spam_checker.check.return_value = False
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        role = M.ProjectRole.by_name('*anonymous')._id
+        post_permission = M.ACE.allow(role, 'post')
+        t.acl.append(post_permission)
+        with h.push_config(c, user=M.User.anonymous()):
+            post = t.post('Hey')
+        assert post.status == 'pending'
+        assert notify_moderators.call_count == 1
+
+    @mock.patch('allura.controllers.discuss.g.spam_checker')
+    @mock.patch.object(M.Thread, 'notify_moderators')
+    def test_spam_and_has_unmoderated_post_permission(self, notify_moderators, spam_checker):
         spam_checker.check.return_value = True
-        post = mock.Mock()
-        assert t.is_spam(post), t.is_spam(post)
-        assert spam_checker.check.call_count == 1, spam_checker.call_count
-
-
-@with_setup(setup_method)
-@mock.patch('allura.controllers.discuss.g.spam_checker')
-def test_not_spam_and_has_unmoderated_post_permission(spam_checker):
-    spam_checker.check.return_value = False
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    role = M.ProjectRole.by_name('*anonymous')._id
-    post_permission = M.ACE.allow(role, 'post')
-    unmoderated_post_permission = M.ACE.allow(role, 'unmoderated_post')
-    t.acl.append(post_permission)
-    t.acl.append(unmoderated_post_permission)
-    with h.push_config(c, user=M.User.anonymous()):
-        post = t.post('Hey')
-    assert post.status == 'ok'
-
-
-@with_setup(setup_method)
-@mock.patch('allura.controllers.discuss.g.spam_checker')
-@mock.patch.object(M.Thread, 'notify_moderators')
-def test_not_spam_but_has_no_unmoderated_post_permission(notify_moderators, spam_checker):
-    spam_checker.check.return_value = False
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    role = M.ProjectRole.by_name('*anonymous')._id
-    post_permission = M.ACE.allow(role, 'post')
-    t.acl.append(post_permission)
-    with h.push_config(c, user=M.User.anonymous()):
-        post = t.post('Hey')
-    assert post.status == 'pending'
-    assert notify_moderators.call_count == 1
-
-
-@with_setup(setup_method)
-@mock.patch('allura.controllers.discuss.g.spam_checker')
-@mock.patch.object(M.Thread, 'notify_moderators')
-def test_spam_and_has_unmoderated_post_permission(notify_moderators, spam_checker):
-    spam_checker.check.return_value = True
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    role = M.ProjectRole.by_name('*anonymous')._id
-    post_permission = M.ACE.allow(role, 'post')
-    unmoderated_post_permission = M.ACE.allow(role, 'unmoderated_post')
-    t.acl.append(post_permission)
-    t.acl.append(unmoderated_post_permission)
-    with h.push_config(c, user=M.User.anonymous()):
-        post = t.post('Hey')
-    assert post.status == 'pending'
-    assert notify_moderators.call_count == 1
-
-
-@with_setup(setup_method)
-@mock.patch('allura.controllers.discuss.g.spam_checker')
-def test_thread_subject_not_included_in_text_checked(spam_checker):
-    spam_checker.check.return_value = False
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    t.post('Hello')
-    assert spam_checker.check.call_count == 1
-    assert spam_checker.check.call_args[0][0] == 'Hello'
-
-
-def test_post_count():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    M.Post(discussion_id=d._id, thread_id=t._id, status='spam')
-    M.Post(discussion_id=d._id, thread_id=t._id, status='ok')
-    M.Post(discussion_id=d._id, thread_id=t._id, status='pending')
-    ThreadLocalORMSession.flush_all()
-    assert t.post_count == 2
-
-
-@mock.patch('allura.controllers.discuss.g.spam_checker')
-def test_spam_num_replies(spam_checker):
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread', num_replies=2)
-    M.Post(discussion_id=d._id, thread_id=t._id, status='ok')
-    ThreadLocalORMSession.flush_all()
-    p1 = M.Post(discussion_id=d._id, thread_id=t._id, status='spam')
-    p1.spam()
-    assert t.num_replies == 1
-
-
-def test_deleted_thread_index():
-    d = M.Discussion(shortname='test', name='test')
-    t = M.Thread(discussion_id=d._id, subject='Test Thread')
-    p = M.Post(discussion_id=d._id, thread_id=t._id, status='ok')
-    t.delete()
-    ThreadLocalORMSession.flush_all()
-
-    # re-query, so relationships get reloaded
-    ThreadLocalORMSession.close_all()
-    p = M.Post.query.get(_id=p._id)
-
-    # just make sure this doesn't fail
-    p.index()
\ No newline at end of file
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        role = M.ProjectRole.by_name('*anonymous')._id
+        post_permission = M.ACE.allow(role, 'post')
+        unmoderated_post_permission = M.ACE.allow(role, 'unmoderated_post')
+        t.acl.append(post_permission)
+        t.acl.append(unmoderated_post_permission)
+        with h.push_config(c, user=M.User.anonymous()):
+            post = t.post('Hey')
+        assert post.status == 'pending'
+        assert notify_moderators.call_count == 1
+
+    @mock.patch('allura.controllers.discuss.g.spam_checker')
+    def test_thread_subject_not_included_in_text_checked(self, spam_checker):
+        spam_checker.check.return_value = False
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        t.post('Hello')
+        assert spam_checker.check.call_count == 1
+        assert spam_checker.check.call_args[0][0] == 'Hello'
+
+    def test_post_count(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        M.Post(discussion_id=d._id, thread_id=t._id, status='spam')
+        M.Post(discussion_id=d._id, thread_id=t._id, status='ok')
+        M.Post(discussion_id=d._id, thread_id=t._id, status='pending')
+        ThreadLocalORMSession.flush_all()
+        assert t.post_count == 2
+
+    @mock.patch('allura.controllers.discuss.g.spam_checker')
+    def test_spam_num_replies(self, spam_checker):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread', num_replies=2)
+        M.Post(discussion_id=d._id, thread_id=t._id, status='ok')
+        ThreadLocalORMSession.flush_all()
+        p1 = M.Post(discussion_id=d._id, thread_id=t._id, status='spam')
+        p1.spam()
+        assert t.num_replies == 1
+
+    def test_deleted_thread_index(self):
+        d = M.Discussion(shortname='test', name='test')
+        t = M.Thread(discussion_id=d._id, subject='Test Thread')
+        p = M.Post(discussion_id=d._id, thread_id=t._id, status='ok')
+        t.delete()
+        ThreadLocalORMSession.flush_all()
+
+        # re-query, so relationships get reloaded
+        ThreadLocalORMSession.close_all()
+        p = M.Post.query.get(_id=p._id)
+
+        # just make sure this doesn't fail
+        p.index()
diff --git a/Allura/allura/tests/model/test_monq.py b/Allura/allura/tests/model/test_monq.py
index 9dc682149..9b2d51754 100644
--- a/Allura/allura/tests/model/test_monq.py
+++ b/Allura/allura/tests/model/test_monq.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import pprint
-from alluratest.tools import with_setup
 
 from ming.orm import ThreadLocalORMSession
 
@@ -24,14 +23,13 @@ from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 
 
-def setup_method():
+def setup_module():
     setup_basic_test()
     ThreadLocalORMSession.close_all()
     setup_global_objects()
     M.MonQTask.query.remove({})
 
 
-@with_setup(setup_method)
 def test_basic_task():
     task = M.MonQTask.post(pprint.pformat, ([5, 6],))
     ThreadLocalORMSession.flush_all()
diff --git a/Allura/allura/tests/model/test_neighborhood.py b/Allura/allura/tests/model/test_neighborhood.py
index 26a773331..247370888 100644
--- a/Allura/allura/tests/model/test_neighborhood.py
+++ b/Allura/allura/tests/model/test_neighborhood.py
@@ -25,65 +25,64 @@ from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-def setup_method():
-    setup_basic_test()
-    setup_with_tools()
+class TestNeighboorhoodModel:
 
+    def setup_method(self):
+        setup_basic_test()
+        self.setup_with_tools()
 
-@td.with_wiki
-def setup_with_tools():
-    setup_global_objects()
+    @td.with_wiki
+    def setup_with_tools(self):
+        setup_global_objects()
 
+    def test_neighborhood(self):
+        neighborhood = M.Neighborhood.query.get(name='Projects')
+        # Check css output depends of neighborhood level
+        test_css = ".text{color:#000;}"
+        neighborhood.css = test_css
+        neighborhood.features['css'] = 'none'
+        assert neighborhood.get_custom_css() == ""
+        neighborhood.features['css'] = 'picker'
+        assert neighborhood.get_custom_css() == test_css
+        neighborhood.features['css'] = 'custom'
+        assert neighborhood.get_custom_css() == test_css
+        # Check max projects
+        neighborhood.features['max_projects'] = None
+        assert neighborhood.get_max_projects() is None
+        neighborhood.features['max_projects'] = 500
+        assert neighborhood.get_max_projects() == 500
 
-@with_setup(setup_method)
-def test_neighborhood():
-    neighborhood = M.Neighborhood.query.get(name='Projects')
-    # Check css output depends of neighborhood level
-    test_css = ".text{color:#000;}"
-    neighborhood.css = test_css
-    neighborhood.features['css'] = 'none'
-    assert neighborhood.get_custom_css() == ""
-    neighborhood.features['css'] = 'picker'
-    assert neighborhood.get_custom_css() == test_css
-    neighborhood.features['css'] = 'custom'
-    assert neighborhood.get_custom_css() == test_css
-    # Check max projects
-    neighborhood.features['max_projects'] = None
-    assert neighborhood.get_max_projects() is None
-    neighborhood.features['max_projects'] = 500
-    assert neighborhood.get_max_projects() == 500
+        # Check picker css styles
+        test_css_dict = {'barontop': '#444',
+                        'titlebarbackground': '#555',
+                        'projecttitlefont': 'arial,sans-serif',
+                        'projecttitlecolor': '#333',
+                        'titlebarcolor': '#666'}
+        css_text = neighborhood.compile_css_for_picker(test_css_dict)
+        assert '#333' in css_text
+        assert '#444' in css_text
+        assert '#555' in css_text
+        assert '#666' in css_text
+        assert 'arial,sans-serif' in css_text
+        neighborhood.css = css_text
+        styles_list = neighborhood.get_css_for_picker()
+        for style in styles_list:
+            assert test_css_dict[style['name']] == style['value']
 
-    # Check picker css styles
-    test_css_dict = {'barontop': '#444',
-                     'titlebarbackground': '#555',
-                     'projecttitlefont': 'arial,sans-serif',
-                     'projecttitlecolor': '#333',
-                     'titlebarcolor': '#666'}
-    css_text = neighborhood.compile_css_for_picker(test_css_dict)
-    assert '#333' in css_text
-    assert '#444' in css_text
-    assert '#555' in css_text
-    assert '#666' in css_text
-    assert 'arial,sans-serif' in css_text
-    neighborhood.css = css_text
-    styles_list = neighborhood.get_css_for_picker()
-    for style in styles_list:
-        assert test_css_dict[style['name']] == style['value']
+        # Check neighborhood custom css showing
+        neighborhood.features['css'] = 'none'
+        assert not neighborhood.allow_custom_css
+        neighborhood.features['css'] = 'picker'
+        assert neighborhood.allow_custom_css
+        neighborhood.features['css'] = 'custom'
+        assert neighborhood.allow_custom_css
 
-    # Check neighborhood custom css showing
-    neighborhood.features['css'] = 'none'
-    assert not neighborhood.allow_custom_css
-    neighborhood.features['css'] = 'picker'
-    assert neighborhood.allow_custom_css
-    neighborhood.features['css'] = 'custom'
-    assert neighborhood.allow_custom_css
+        neighborhood.anchored_tools = 'wiki:Wiki, tickets:Tickets'
+        assert neighborhood.get_anchored_tools()['wiki'] == 'Wiki'
+        assert neighborhood.get_anchored_tools()['tickets'] == 'Tickets'
 
-    neighborhood.anchored_tools = 'wiki:Wiki, tickets:Tickets'
-    assert neighborhood.get_anchored_tools()['wiki'] == 'Wiki'
-    assert neighborhood.get_anchored_tools()['tickets'] == 'Tickets'
+        neighborhood.prohibited_tools = 'wiki, tickets'
+        assert neighborhood.get_prohibited_tools() == ['wiki', 'tickets']
 
-    neighborhood.prohibited_tools = 'wiki, tickets'
-    assert neighborhood.get_prohibited_tools() == ['wiki', 'tickets']
-
-    # Check properties
-    assert neighborhood.shortname == "p"
+        # Check properties
+        assert neighborhood.shortname == "p"
diff --git a/Allura/allura/tests/model/test_oauth.py b/Allura/allura/tests/model/test_oauth.py
index 67d12b094..edb396abc 100644
--- a/Allura/allura/tests/model/test_oauth.py
+++ b/Allura/allura/tests/model/test_oauth.py
@@ -16,28 +16,26 @@
 #       under the License.
 
 
-from alluratest.tools import with_setup, assert_equal, assert_not_equal
-
 from ming.odm import ThreadLocalORMSession
 
 from allura import model as M
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-def setup_method():
-    setup_basic_test()
-    ThreadLocalORMSession.close_all()
-    setup_global_objects()
+class TestOAuthModel:
 
+    def setup_method(self):
+        setup_basic_test()
+        ThreadLocalORMSession.close_all()
+        setup_global_objects()
 
-@with_setup(setup_method)
-def test_upsert():
-    admin = M.User.by_username('test-admin')
-    user = M.User.by_username('test-user')
-    name = 'test-token'
-    token1 = M.OAuthConsumerToken.upsert(name, admin)
-    token2 = M.OAuthConsumerToken.upsert(name, admin)
-    token3 = M.OAuthConsumerToken.upsert(name, user)
-    assert M.OAuthConsumerToken.query.find().count() == 2
-    assert token1._id == token2._id
-    assert token1._id != token3._id
+    def test_upsert(self):
+        admin = M.User.by_username('test-admin')
+        user = M.User.by_username('test-user')
+        name = 'test-token'
+        token1 = M.OAuthConsumerToken.upsert(name, admin)
+        token2 = M.OAuthConsumerToken.upsert(name, admin)
+        token3 = M.OAuthConsumerToken.upsert(name, user)
+        assert M.OAuthConsumerToken.query.find().count() == 2
+        assert token1._id == token2._id
+        assert token1._id != token3._id
diff --git a/Allura/allura/tests/model/test_project.py b/Allura/allura/tests/model/test_project.py
index 524754bc3..6a1d44a8a 100644
--- a/Allura/allura/tests/model/test_project.py
+++ b/Allura/allura/tests/model/test_project.py
@@ -31,172 +31,161 @@ from allura.lib.exceptions import ToolError, Invalid
 from mock import MagicMock, patch
 
 
-def setup_method():
-    setup_basic_test()
-    setup_with_tools()
-
-
-@td.with_wiki
-def setup_with_tools():
-    setup_global_objects()
-
-
-@with_setup(setup_method)
-def test_project():
-    assert type(c.project.sidebar_menu()) == list
-    assert c.project.script_name in c.project.url()
-    old_proj = c.project
-    h.set_context('test/sub1', neighborhood='Projects')
-    assert type(c.project.sidebar_menu()) == list
-    assert type(c.project.sitemap()) == list
-    assert c.project.sitemap()[1].label == 'Admin'
-    assert old_proj in list(c.project.parent_iter())
-    h.set_context('test', 'wiki', neighborhood='Projects')
-    adobe_nbhd = M.Neighborhood.query.get(name='Adobe')
-    p = M.Project.query.get(
-        shortname='adobe-1', neighborhood_id=adobe_nbhd._id)
-    # assert 'http' in p.url() # We moved adobe into /adobe/, not
-    # http://adobe....
-    assert p.script_name in p.url()
-    assert c.project.shortname == 'test'
-    assert '<p>' in c.project.description_html
-    c.project.uninstall_app('hello-test-mount-point')
-    ThreadLocalORMSession.flush_all()
-
-    c.project.install_app('Wiki', 'hello-test-mount-point')
-    c.project.support_page = 'hello-test-mount-point'
-    assert c.project.app_config('wiki').tool_name == 'wiki'
-    ThreadLocalORMSession.flush_all()
-    with td.raises(ToolError):
-        # already installed
+class TestProjectModel:
+
+    def setup_method(self):
+        setup_basic_test()
+        self.setup_with_tools()
+
+    @td.with_wiki
+    def setup_with_tools(self):
+        setup_global_objects()
+
+    def test_project(self):
+        assert type(c.project.sidebar_menu()) == list
+        assert c.project.script_name in c.project.url()
+        old_proj = c.project
+        h.set_context('test/sub1', neighborhood='Projects')
+        assert type(c.project.sidebar_menu()) == list
+        assert type(c.project.sitemap()) == list
+        assert c.project.sitemap()[1].label == 'Admin'
+        assert old_proj in list(c.project.parent_iter())
+        h.set_context('test', 'wiki', neighborhood='Projects')
+        adobe_nbhd = M.Neighborhood.query.get(name='Adobe')
+        p = M.Project.query.get(
+            shortname='adobe-1', neighborhood_id=adobe_nbhd._id)
+        # assert 'http' in p.url() # We moved adobe into /adobe/, not
+        # http://adobe....
+        assert p.script_name in p.url()
+        assert c.project.shortname == 'test'
+        assert '<p>' in c.project.description_html
+        c.project.uninstall_app('hello-test-mount-point')
+        ThreadLocalORMSession.flush_all()
+
         c.project.install_app('Wiki', 'hello-test-mount-point')
-    ThreadLocalORMSession.flush_all()
-    c.project.uninstall_app('hello-test-mount-point')
-    ThreadLocalORMSession.flush_all()
-    with td.raises(ToolError):
-        # mount point reserved
-        c.project.install_app('Wiki', 'feed')
-    with td.raises(ToolError):
-        # mount point too long
-        c.project.install_app('Wiki', 'a' * 64)
-    with td.raises(ToolError):
-        # mount point must begin with letter
-        c.project.install_app('Wiki', '1')
-    # single letter mount points are allowed
-    c.project.install_app('Wiki', 'a')
-    # Make sure the project support page is reset if the tool it was pointing
-    # to is uninstalled.
-    assert c.project.support_page == ''
-    app_config = c.project.app_config('hello')
-    app_inst = c.project.app_instance(app_config)
-    app_inst = c.project.app_instance('hello')
-    app_inst = c.project.app_instance('hello2123')
-    c.project.breadcrumbs()
-    c.app.config.breadcrumbs()
-
-
-@with_setup(setup_method)
-def test_install_app_validates_options():
-    from forgetracker.tracker_main import ForgeTrackerApp
-    name = 'TicketMonitoringEmail'
-    opt = [o for o in ForgeTrackerApp.config_options if o.name == name][0]
-    opt.validator = fev.Email(not_empty=True)
-    with patch.object(ForgeTrackerApp, 'config_on_install', new=[opt.name]):
-        for v in [None, '', 'bad@email']:
-            with td.raises(ToolError):
-                c.project.install_app('Tickets', 'test-tickets', **{name: v})
-            assert c.project.app_instance('test-tickets') == None
-        c.project.install_app('Tickets', 'test-tickets', **{name: 'e@e.com'})
-        app = c.project.app_instance('test-tickets')
-        assert app.config.options[name] == 'e@e.com'
-
-
-def test_project_index():
-    project, idx = c.project, c.project.index()
-    assert 'id' in idx
-    assert idx['id'] == project.index_id()
-    assert 'title' in idx
-    assert 'type_s' in idx
-    assert 'deleted_b' in idx
-    assert 'private_b' in idx
-    assert 'neighborhood_id_s' in idx
-    assert 'short_description_t' in idx
-    assert 'url_s' in idx
-
-
-def test_subproject():
-    project = M.Project.query.get(shortname='test')
-    with td.raises(ToolError):
-        with patch('allura.lib.plugin.ProjectRegistrationProvider') as Provider:
-            Provider.get().shortname_validator.to_python.side_effect = Invalid(
-                'name', 'value', {})
-            # name doesn't validate
-            sp = project.new_subproject('test-proj-nose')
-    sp = project.new_subproject('test-proj-nose')
-    spp = sp.new_subproject('spp')
-    ThreadLocalORMSession.flush_all()
-    sp.delete()
-    ThreadLocalORMSession.flush_all()
-
-
-@td.with_wiki
-def test_anchored_tools():
-    c.project.neighborhood.anchored_tools = 'wiki:Wiki, tickets:Ticket'
-    c.project.install_app = MagicMock()
-    assert c.project.sitemap()[0].label == 'Wiki'
-    assert c.project.install_app.call_args[0][0] == 'tickets'
-    assert c.project.ordered_mounts()[0]['ac'].tool_name == 'wiki'
-
-
-def test_set_ordinal_to_admin_tool():
-    with h.push_config(c,
-                       user=M.User.by_username('test-admin'),
-                       project=M.Project.query.get(shortname='test')):
-        sm = c.project.sitemap()
-        assert sm[-1].tool_name == 'admin'
-
-
-@with_setup(setup_method)
-def test_users_and_roles():
-    p = M.Project.query.get(shortname='test')
-    sub = p.direct_subprojects[0]
-    u = M.User.by_username('test-admin')
-    assert p.users_with_role('Admin') == [u]
-    assert p.users_with_role('Admin') == sub.users_with_role('Admin')
-    assert p.users_with_role('Admin') == p.admins()
-
-    user = p.admins()[0]
-    user.disabled = True
-    ThreadLocalORMSession.flush_all()
-    assert p.users_with_role('Admin') == []
-    assert p.users_with_role('Admin') == p.admins()
-
-
-@with_setup(setup_method)
-def test_project_disabled_users():
-    p = M.Project.query.get(shortname='test')
-    users = p.users()
-    assert users[0].username == 'test-admin'
-    user = M.User.by_username('test-admin')
-    user.disabled = True
-    ThreadLocalORMSession.flush_all()
-    users = p.users()
-    assert users == []
-
-def test_screenshot_unicode_serialization():
-    p = M.Project.query.get(shortname='test')
-    screenshot_unicode = M.ProjectFile(project_id=p._id, category='screenshot', caption="ConSelección", filename='ConSelección.jpg')
-    screenshot_ascii = M.ProjectFile(project_id=p._id, category='screenshot', caption='test-screenshot', filename='test_file.jpg')
-    ThreadLocalORMSession.flush_all()
-
-    serialized = p.__json__()
-    screenshots = sorted(serialized['screenshots'], key=lambda k: k['caption'])
-
-    assert len(screenshots) == 2
-    assert screenshots[0]['url'] == 'http://localhost/p/test/screenshot/ConSelecci%C3%B3n.jpg'
-    assert screenshots[0]['caption'] == "ConSelección"
-    assert screenshots[0]['thumbnail_url'] == 'http://localhost/p/test/screenshot/ConSelecci%C3%B3n.jpg/thumb'
-
-    assert screenshots[1]['url'] == 'http://localhost/p/test/screenshot/test_file.jpg'
-    assert screenshots[1]['caption'] == 'test-screenshot'
-    assert screenshots[1]['thumbnail_url'] == 'http://localhost/p/test/screenshot/test_file.jpg/thumb'
+        c.project.support_page = 'hello-test-mount-point'
+        assert c.project.app_config('wiki').tool_name == 'wiki'
+        ThreadLocalORMSession.flush_all()
+        with td.raises(ToolError):
+            # already installed
+            c.project.install_app('Wiki', 'hello-test-mount-point')
+        ThreadLocalORMSession.flush_all()
+        c.project.uninstall_app('hello-test-mount-point')
+        ThreadLocalORMSession.flush_all()
+        with td.raises(ToolError):
+            # mount point reserved
+            c.project.install_app('Wiki', 'feed')
+        with td.raises(ToolError):
+            # mount point too long
+            c.project.install_app('Wiki', 'a' * 64)
+        with td.raises(ToolError):
+            # mount point must begin with letter
+            c.project.install_app('Wiki', '1')
+        # single letter mount points are allowed
+        c.project.install_app('Wiki', 'a')
+        # Make sure the project support page is reset if the tool it was pointing
+        # to is uninstalled.
+        assert c.project.support_page == ''
+        app_config = c.project.app_config('hello')
+        app_inst = c.project.app_instance(app_config)
+        app_inst = c.project.app_instance('hello')
+        app_inst = c.project.app_instance('hello2123')
+        c.project.breadcrumbs()
+        c.app.config.breadcrumbs()
+
+    def test_install_app_validates_options(self):
+        from forgetracker.tracker_main import ForgeTrackerApp
+        name = 'TicketMonitoringEmail'
+        opt = [o for o in ForgeTrackerApp.config_options if o.name == name][0]
+        opt.validator = fev.Email(not_empty=True)
+        with patch.object(ForgeTrackerApp, 'config_on_install', new=[opt.name]):
+            for v in [None, '', 'bad@email']:
+                with td.raises(ToolError):
+                    c.project.install_app('Tickets', 'test-tickets', **{name: v})
+                assert c.project.app_instance('test-tickets') == None
+            c.project.install_app('Tickets', 'test-tickets', **{name: 'e@e.com'})
+            app = c.project.app_instance('test-tickets')
+            assert app.config.options[name] == 'e@e.com'
+
+    def test_project_index(self):
+        project, idx = c.project, c.project.index()
+        assert 'id' in idx
+        assert idx['id'] == project.index_id()
+        assert 'title' in idx
+        assert 'type_s' in idx
+        assert 'deleted_b' in idx
+        assert 'private_b' in idx
+        assert 'neighborhood_id_s' in idx
+        assert 'short_description_t' in idx
+        assert 'url_s' in idx
+
+    def test_subproject(self):
+        project = M.Project.query.get(shortname='test')
+        with td.raises(ToolError):
+            with patch('allura.lib.plugin.ProjectRegistrationProvider') as Provider:
+                Provider.get().shortname_validator.to_python.side_effect = Invalid(
+                    'name', 'value', {})
+                # name doesn't validate
+                sp = project.new_subproject('test-proj-nose')
+        sp = project.new_subproject('test-proj-nose')
+        spp = sp.new_subproject('spp')
+        ThreadLocalORMSession.flush_all()
+        sp.delete()
+        ThreadLocalORMSession.flush_all()
+
+    @td.with_wiki
+    def test_anchored_tools(self):
+        c.project.neighborhood.anchored_tools = 'wiki:Wiki, tickets:Ticket'
+        c.project.install_app = MagicMock()
+        assert c.project.sitemap()[0].label == 'Wiki'
+        assert c.project.install_app.call_args[0][0] == 'tickets'
+        assert c.project.ordered_mounts()[0]['ac'].tool_name == 'wiki'
+
+    def test_set_ordinal_to_admin_tool(self):
+        with h.push_config(c,
+                        user=M.User.by_username('test-admin'),
+                        project=M.Project.query.get(shortname='test')):
+            sm = c.project.sitemap()
+            assert sm[-1].tool_name == 'admin'
+
+    def test_users_and_roles(self):
+        p = M.Project.query.get(shortname='test')
+        sub = p.direct_subprojects[0]
+        u = M.User.by_username('test-admin')
+        assert p.users_with_role('Admin') == [u]
+        assert p.users_with_role('Admin') == sub.users_with_role('Admin')
+        assert p.users_with_role('Admin') == p.admins()
+
+        user = p.admins()[0]
+        user.disabled = True
+        ThreadLocalORMSession.flush_all()
+        assert p.users_with_role('Admin') == []
+        assert p.users_with_role('Admin') == p.admins()
+
+    def test_project_disabled_users(self):
+        p = M.Project.query.get(shortname='test')
+        users = p.users()
+        assert users[0].username == 'test-admin'
+        user = M.User.by_username('test-admin')
+        user.disabled = True
+        ThreadLocalORMSession.flush_all()
+        users = p.users()
+        assert users == []
+
+    def test_screenshot_unicode_serialization(self):
+        p = M.Project.query.get(shortname='test')
+        screenshot_unicode = M.ProjectFile(project_id=p._id, category='screenshot', caption="ConSelección", filename='ConSelección.jpg')
+        screenshot_ascii = M.ProjectFile(project_id=p._id, category='screenshot', caption='test-screenshot', filename='test_file.jpg')
+        ThreadLocalORMSession.flush_all()
+
+        serialized = p.__json__()
+        screenshots = sorted(serialized['screenshots'], key=lambda k: k['caption'])
+
+        assert len(screenshots) == 2
+        assert screenshots[0]['url'] == 'http://localhost/p/test/screenshot/ConSelecci%C3%B3n.jpg'
+        assert screenshots[0]['caption'] == "ConSelección"
+        assert screenshots[0]['thumbnail_url'] == 'http://localhost/p/test/screenshot/ConSelecci%C3%B3n.jpg/thumb'
+
+        assert screenshots[1]['url'] == 'http://localhost/p/test/screenshot/test_file.jpg'
+        assert screenshots[1]['caption'] == 'test-screenshot'
+        assert screenshots[1]['thumbnail_url'] == 'http://localhost/p/test/screenshot/test_file.jpg/thumb'
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index 71733a2d5..8a6924ce2 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -20,161 +20,152 @@ import mock
 from ming.base import Object
 import pytest
 from formencode import validators as fev
+from textwrap import dedent
 
 from alluratest.controller import setup_unit_test
 from alluratest.tools import with_setup
 from allura import app
 from allura.lib.app_globals import Icon
 from allura.lib import mail_util
-from alluratest.pytest_helpers import with_nose_compatibility
-
-
-def setup_method():
-    setup_unit_test()
-    c.user._id = None
-    c.project = mock.Mock()
-    c.project.name = 'Test Project'
-    c.project.shortname = 'tp'
-    c.project._id = 'testproject/'
-    c.project.url = lambda: '/testproject/'
-    app_config = mock.Mock()
-    app_config._id = None
-    app_config.project_id = 'testproject/'
-    app_config.tool_name = 'tool'
-    app_config.options = Object(mount_point='foo')
-    c.app = mock.Mock()
-    c.app.config = app_config
-    c.app.config.script_name = lambda: '/testproject/test_application/'
-    c.app.config.url = lambda: 'http://testproject/test_application/'
-    c.app.url = c.app.config.url()
-    c.app.__version__ = '0.0'
-
-def test_config_options():
-    options = [
-        app.ConfigOption('test1', str, 'MyTestValue'),
-        app.ConfigOption('test2', str, lambda:'MyTestValue')]
-    assert options[0].default == 'MyTestValue'
-    assert options[1].default == 'MyTestValue'
-
-
-def test_config_options_render_attrs():
-    opt = app.ConfigOption('test1', str, None, extra_attrs={'type': 'url'})
-    assert opt.render_attrs() == 'type="url"'
-
-
-def test_config_option_without_validator():
-    opt = app.ConfigOption('test1', str, None)
-    assert opt.validate(None) == None
-    assert opt.validate('') == ''
-    assert opt.validate('val') == 'val'
-
-
-def test_config_option_with_validator():
-    v = fev.NotEmpty()
-    opt = app.ConfigOption('test1', str, None, validator=v)
-    assert opt.validate('val') == 'val'
-    pytest.raises(fev.Invalid, opt.validate, None)
-    pytest.raises(fev.Invalid, opt.validate, '')
-
-
-@with_setup(setup_method)
-def test_options_on_install_default():
-    a = app.Application(c.project, c.app.config)
-    assert a.options_on_install() == []
-
-
-@with_setup(setup_method)
-def test_options_on_install():
-    opts = [app.ConfigOption('url', str, None),
-            app.ConfigOption('private', bool, None)]
-    class TestApp(app.Application):
-        config_options = app.Application.config_options + opts + [
-            app.ConfigOption('not_on_install', str, None),
-        ]
-        config_on_install = ['url', 'private']
-
-    a = TestApp(c.project, c.app.config)
-    assert a.options_on_install() == opts
-
-@with_setup(setup_method)
-def test_main_menu():
-    class TestApp(app.Application):
-        @property
-        def sitemap(self):
-            children = [app.SitemapEntry('New', 'new', ui_icon=Icon('some-icon')),
-                        app.SitemapEntry('Recent', 'recent'),
-                        ]
-            return [app.SitemapEntry('My Tool', '.')[children]]
-
-    a = TestApp(c.project, c.app.config)
-    main_menu = a.main_menu()
-    assert len(main_menu) == 1
-    assert main_menu[0].children == []  # default main_menu implementation should drop the children from sitemap()
-
-
-@with_setup(setup_method)
-def test_sitemap():
-    sm = app.SitemapEntry('test', '')[
-        app.SitemapEntry('a', 'a/'),
-        app.SitemapEntry('b', 'b/')]
-    sm[app.SitemapEntry(lambda app:app.config.script_name(), 'c/')]
-    bound_sm = sm.bind_app(c.app)
-    assert bound_sm.url == 'http://testproject/test_application/', bound_sm.url
-    assert bound_sm.children[
-        -1].label == '/testproject/test_application/', bound_sm.children[-1].label
-    assert len(sm.children) == 3
-    sm.extend([app.SitemapEntry('a', 'a/')[
-        app.SitemapEntry('d', 'd/')]])
-    assert len(sm.children) == 3
-
-
-@with_setup(setup_method)
-@mock.patch('allura.app.Application.PostClass.query.get')
-def test_handle_artifact_unicode(qg):
-    """
-    Tests that app.handle_artifact_message can accept utf strings
-    """
-    ticket = mock.MagicMock()
-    ticket.get_discussion_thread.return_value = (mock.MagicMock(), mock.MagicMock())
-    post = mock.MagicMock()
-    qg.return_value = post
-
-    a = app.Application(c.project, c.app.config)
-
-    msg = dict(payload='foo ƒ†©¥˙¨ˆ'.encode(), message_id=1, headers={})
-    a.handle_artifact_message(ticket, msg)
-    assert post.attach.call_args[0][1].getvalue() == 'foo ƒ†©¥˙¨ˆ'.encode()
-
-    msg = dict(payload=b'foo', message_id=1, headers={})
-    a.handle_artifact_message(ticket, msg)
-    assert post.attach.call_args[0][1].getvalue() == b'foo'
-
-    msg = dict(payload="\x94my quote\x94".encode(), message_id=1, headers={})
-    a.handle_artifact_message(ticket, msg)
-    assert post.attach.call_args[0][1].getvalue() == '\x94my quote\x94'.encode()
-
-    # assert against prod example
-    msg_raw = """Message-Id: <15...@webmail.messagingengine.com>
-From: foo <fo...@bar.com>
-To: "[forge:site-support]" <15...@site-support.forge.p.re.sf.net>
-MIME-Version: 1.0
-Content-Transfer-Encoding: 7bit
-Content-Type: multipart/alternative; boundary="_----------=_150235203132168580"
-Date: Thu, 10 Aug 2017 10:00:31 +0200
-Subject: Re: [forge:site-support] #15391 Unable to join (my own) mailing list
-This is a multi-part message in MIME format.
---_----------=_150235203132168580
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain; charset="utf-8"
-Hi
---_----------=_150235203132168580
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/html; charset="utf-8"
-<!DOCTYPE html>
-<html><body>Hi</body></html>
---_----------=_150235203132168580--
-    """
-    msg = mail_util.parse_message(msg_raw)
-    for p in [p for p in msg['parts'] if p['payload'] is not None]:
-        # filter here mimics logic in `route_email`
-        a.handle_artifact_message(ticket, p)
+
+
+class TestApp:
+
+    def setup_method(self):
+        setup_unit_test()
+        c.user._id = None
+        c.project = mock.Mock()
+        c.project.name = 'Test Project'
+        c.project.shortname = 'tp'
+        c.project._id = 'testproject/'
+        c.project.url = lambda: '/testproject/'
+        app_config = mock.Mock()
+        app_config._id = None
+        app_config.project_id = 'testproject/'
+        app_config.tool_name = 'tool'
+        app_config.options = Object(mount_point='foo')
+        c.app = mock.Mock()
+        c.app.config = app_config
+        c.app.config.script_name = lambda: '/testproject/test_application/'
+        c.app.config.url = lambda: 'http://testproject/test_application/'
+        c.app.url = c.app.config.url()
+        c.app.__version__ = '0.0'
+
+    def test_config_options(self):
+        options = [
+            app.ConfigOption('test1', str, 'MyTestValue'),
+            app.ConfigOption('test2', str, lambda:'MyTestValue')]
+        assert options[0].default == 'MyTestValue'
+        assert options[1].default == 'MyTestValue'
+
+    def test_config_options_render_attrs(self):
+        opt = app.ConfigOption('test1', str, None, extra_attrs={'type': 'url'})
+        assert opt.render_attrs() == 'type="url"'
+
+    def test_config_option_without_validator(self):
+        opt = app.ConfigOption('test1', str, None)
+        assert opt.validate(None) == None
+        assert opt.validate('') == ''
+        assert opt.validate('val') == 'val'
+
+    def test_config_option_with_validator(self):
+        v = fev.NotEmpty()
+        opt = app.ConfigOption('test1', str, None, validator=v)
+        assert opt.validate('val') == 'val'
+        pytest.raises(fev.Invalid, opt.validate, None)
+        pytest.raises(fev.Invalid, opt.validate, '')
+
+    def test_options_on_install_default(self):
+        a = app.Application(c.project, c.app.config)
+        assert a.options_on_install() == []
+
+    def test_options_on_install(self):
+        opts = [app.ConfigOption('url', str, None),
+                app.ConfigOption('private', bool, None)]
+        class TestApp(app.Application):
+            config_options = app.Application.config_options + opts + [
+                app.ConfigOption('not_on_install', str, None),
+            ]
+            config_on_install = ['url', 'private']
+
+        a = TestApp(c.project, c.app.config)
+        assert a.options_on_install() == opts
+
+    def test_main_menu(self):
+        class TestApp(app.Application):
+            @property
+            def sitemap(self):
+                children = [app.SitemapEntry('New', 'new', ui_icon=Icon('some-icon')),
+                            app.SitemapEntry('Recent', 'recent'),
+                            ]
+                return [app.SitemapEntry('My Tool', '.')[children]]
+
+        a = TestApp(c.project, c.app.config)
+        main_menu = a.main_menu()
+        assert len(main_menu) == 1
+        assert main_menu[0].children == []  # default main_menu implementation should drop the children from sitemap()
+
+    def test_sitemap(self):
+        sm = app.SitemapEntry('test', '')[
+            app.SitemapEntry('a', 'a/'),
+            app.SitemapEntry('b', 'b/')]
+        sm[app.SitemapEntry(lambda app:app.config.script_name(), 'c/')]
+        bound_sm = sm.bind_app(c.app)
+        assert bound_sm.url == 'http://testproject/test_application/', bound_sm.url
+        assert bound_sm.children[
+            -1].label == '/testproject/test_application/', bound_sm.children[-1].label
+        assert len(sm.children) == 3
+        sm.extend([app.SitemapEntry('a', 'a/')[
+            app.SitemapEntry('d', 'd/')]])
+        assert len(sm.children) == 3
+
+    @mock.patch('allura.app.Application.PostClass.query.get')
+    def test_handle_artifact_unicode(self, qg):
+        """
+        Tests that app.handle_artifact_message can accept utf strings
+        """
+        ticket = mock.MagicMock()
+        ticket.get_discussion_thread.return_value = (mock.MagicMock(), mock.MagicMock())
+        post = mock.MagicMock()
+        qg.return_value = post
+
+        a = app.Application(c.project, c.app.config)
+
+        msg = dict(payload='foo ƒ†©¥˙¨ˆ'.encode(), message_id=1, headers={})
+        a.handle_artifact_message(ticket, msg)
+        assert post.attach.call_args[0][1].getvalue() == 'foo ƒ†©¥˙¨ˆ'.encode()
+
+        msg = dict(payload=b'foo', message_id=1, headers={})
+        a.handle_artifact_message(ticket, msg)
+        assert post.attach.call_args[0][1].getvalue() == b'foo'
+
+        msg = dict(payload="\x94my quote\x94".encode(), message_id=1, headers={})
+        a.handle_artifact_message(ticket, msg)
+        assert post.attach.call_args[0][1].getvalue() == '\x94my quote\x94'.encode()
+
+        # assert against prod example
+        msg_raw = dedent("""\
+            Message-Id: <15...@webmail.messagingengine.com>
+            From: foo <fo...@bar.com>
+            To: "[forge:site-support]" <15...@site-support.forge.p.re.sf.net>
+            MIME-Version: 1.0
+            Content-Transfer-Encoding: 7bit
+            Content-Type: multipart/alternative; boundary="_----------=_150235203132168580"
+            Date: Thu, 10 Aug 2017 10:00:31 +0200
+            Subject: Re: [forge:site-support] #15391 Unable to join (my own) mailing list
+            This is a multi-part message in MIME format.
+            --_----------=_150235203132168580
+            Content-Transfer-Encoding: quoted-printable
+            Content-Type: text/plain; charset="utf-8"
+            Hi
+            --_----------=_150235203132168580
+            Content-Transfer-Encoding: quoted-printable
+            Content-Type: text/html; charset="utf-8"
+            <!DOCTYPE html>
+            <html><body>Hi</body></html>
+            --_----------=_150235203132168580--
+        """)
+        msg = mail_util.parse_message(msg_raw)
+        for p in [p for p in msg['parts'] if p['payload'] is not None]:
+            # filter here mimics logic in `route_email`
+            a.handle_artifact_message(ticket, p)


[allura] 09/14: [#8455] remove @with_nose_compatability

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 6f7431cd218662a8318427e82fdd3f3a6616192d
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Nov 1 15:28:04 2022 +0000

    [#8455] remove @with_nose_compatability
---
 Allura/allura/tests/functional/test_admin.py       |  9 ----
 Allura/allura/tests/functional/test_auth.py        | 10 -----
 Allura/allura/tests/functional/test_discuss.py     |  4 --
 Allura/allura/tests/functional/test_feeds.py       |  2 -
 Allura/allura/tests/functional/test_gravatar.py    |  2 -
 Allura/allura/tests/functional/test_home.py        |  2 -
 Allura/allura/tests/functional/test_nav.py         |  2 -
 .../allura/tests/functional/test_neighborhood.py   |  4 --
 Allura/allura/tests/functional/test_newforge.py    |  2 -
 .../tests/functional/test_personal_dashboard.py    |  4 --
 Allura/allura/tests/functional/test_rest.py        |  5 ---
 Allura/allura/tests/functional/test_root.py        |  3 --
 Allura/allura/tests/functional/test_search.py      |  2 -
 Allura/allura/tests/functional/test_site_admin.py  |  7 ----
 Allura/allura/tests/functional/test_static.py      |  2 -
 Allura/allura/tests/functional/test_subscriber.py  |  2 -
 Allura/allura/tests/functional/test_tool_list.py   |  2 -
 .../allura/tests/functional/test_trovecategory.py  |  3 --
 .../allura/tests/functional/test_user_profile.py   |  3 --
 Allura/allura/tests/model/test_auth.py             |  2 -
 Allura/allura/tests/model/test_filesystem.py       |  2 -
 Allura/allura/tests/model/test_notification.py     |  5 ---
 Allura/allura/tests/model/test_repo.py             |  5 ---
 Allura/allura/tests/model/test_timeline.py         |  2 -
 .../tests/scripts/test_create_sitemap_files.py     |  2 -
 .../allura/tests/scripts/test_delete_projects.py   |  2 -
 Allura/allura/tests/scripts/test_misc_scripts.py   |  2 -
 Allura/allura/tests/scripts/test_reindexes.py      |  3 --
 .../tests/templates/jinja_master/test_lib.py       |  2 -
 Allura/allura/tests/test_commands.py               |  7 ----
 Allura/allura/tests/test_decorators.py             |  3 --
 Allura/allura/tests/test_diff.py                   |  2 -
 Allura/allura/tests/test_dispatch.py               |  2 -
 Allura/allura/tests/test_globals.py                |  7 ----
 Allura/allura/tests/test_helpers.py                |  5 ---
 Allura/allura/tests/test_mail_util.py              |  6 ---
 Allura/allura/tests/test_markdown.py               |  6 ---
 Allura/allura/tests/test_middlewares.py            |  2 -
 Allura/allura/tests/test_multifactor.py            | 11 -----
 Allura/allura/tests/test_plugin.py                 |  8 ----
 Allura/allura/tests/test_scripttask.py             |  2 -
 Allura/allura/tests/test_security.py               |  2 -
 Allura/allura/tests/test_tasks.py                  |  9 ----
 Allura/allura/tests/test_utils.py                  | 11 -----
 Allura/allura/tests/test_validators.py             | 12 ------
 Allura/allura/tests/test_webhooks.py               |  8 ----
 .../test_discussion_moderation_controller.py       |  4 --
 Allura/allura/tests/unit/phone/test_nexmo.py       |  2 -
 .../allura/tests/unit/phone/test_phone_service.py  |  2 -
 Allura/allura/tests/unit/spam/test_akismet.py      |  2 -
 Allura/allura/tests/unit/spam/test_spam_filter.py  |  4 --
 .../allura/tests/unit/spam/test_stopforumspam.py   |  2 -
 Allura/allura/tests/unit/test_app.py               |  5 ---
 Allura/allura/tests/unit/test_artifact.py          |  2 -
 Allura/allura/tests/unit/test_discuss.py           |  2 -
 Allura/allura/tests/unit/test_helpers/test_ago.py  |  2 -
 .../tests/unit/test_helpers/test_set_context.py    |  7 ----
 .../allura/tests/unit/test_ldap_auth_provider.py   |  2 -
 Allura/allura/tests/unit/test_mixins.py            |  2 -
 .../allura/tests/unit/test_package_path_loader.py  |  2 -
 Allura/allura/tests/unit/test_post_model.py        |  2 -
 Allura/allura/tests/unit/test_project.py           |  2 -
 Allura/allura/tests/unit/test_repo.py              |  8 ----
 Allura/allura/tests/unit/test_session.py           |  5 ---
 Allura/allura/tests/unit/test_sitemapentry.py      |  2 -
 Allura/allura/tests/unit/test_solr.py              |  4 --
 AlluraTest/alluratest/controller.py                |  5 ---
 AlluraTest/alluratest/pytest_helpers.py            | 49 ----------------------
 AlluraTest/alluratest/test_syntax.py               |  2 +-
 69 files changed, 1 insertion(+), 318 deletions(-)

diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 20fc7a554..11ac71129 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -30,7 +30,6 @@ import mock
 
 import allura
 from allura.tests import TestController
-from alluratest.pytest_helpers import with_nose_compatibility
 from allura.tests import decorators as td
 from allura.tests.decorators import audits
 from alluratest.controller import TestRestApiBase, setup_trove_categories
@@ -46,7 +45,6 @@ from forgewiki.wiki_main import ForgeWikiApp
 log = logging.getLogger(__name__)
 
 
-@with_nose_compatibility
 class TestProjectAdmin(TestController):
 
     def test_admin_controller(self):
@@ -959,7 +957,6 @@ class TestProjectAdmin(TestController):
         r.mustcontain('Neighborhood Invitation(s) for test')
 
 
-@with_nose_compatibility
 class TestExport(TestController):
 
     def setup_method(self, method):
@@ -1086,7 +1083,6 @@ class TestExport(TestController):
         assert 'Check All</label>' in r
 
 
-@with_nose_compatibility
 class TestRestExport(TestRestApiBase):
 
     @mock.patch('allura.model.project.MonQTask')
@@ -1154,7 +1150,6 @@ class TestRestExport(TestRestApiBase):
             ['tickets', 'discussion'], 'test.zip', send_email=False, with_attachments=False)
 
 
-@with_nose_compatibility
 class TestRestInstallTool(TestRestApiBase):
 
     def test_missing_mount_info(self):
@@ -1328,7 +1323,6 @@ class TestRestInstallTool(TestRestApiBase):
             get_labels() == ['t1', 'Admin', 'Search', 'Activity', 'A Subproject', 'ta', 'tb', 'tc'])
 
 
-@with_nose_compatibility
 class TestRestAdminOptions(TestRestApiBase):
     def test_no_mount_point(self):
         r = self.api_get('/rest/p/test/admin/admin_options/', status=400)
@@ -1344,7 +1338,6 @@ class TestRestAdminOptions(TestRestApiBase):
         assert r.json['options'] is not None
 
 
-@with_nose_compatibility
 class TestRestMountOrder(TestRestApiBase):
     def test_no_kw(self):
         r = self.api_post('/rest/p/test/admin/mount_order/', status=400)
@@ -1394,7 +1387,6 @@ class TestRestMountOrder(TestRestApiBase):
         assert b > a
 
 
-@with_nose_compatibility
 class TestRestToolGrouping(TestRestApiBase):
     def test_invalid_grouping_threshold(self):
         for invalid_value in ('100', 'asdf'):
@@ -1421,7 +1413,6 @@ class TestRestToolGrouping(TestRestApiBase):
         assert 'wiki' in [tool['mount_point'] for tool in result2.json['menu']]
 
 
-@with_nose_compatibility
 class TestInstallableTools(TestRestApiBase):
     def test_installable_tools_response(self):
         r = self.api_get('/rest/p/test/admin/installable_tools', status=200)
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index c016a39b1..c1030a3cb 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -38,7 +38,6 @@ from tg import tmpl_context as c, app_globals as g
 from allura.tests import TestController
 from allura.tests import decorators as td
 from allura.tests.decorators import audits, out_audits, assert_logmsg
-from alluratest.pytest_helpers import with_nose_compatibility
 from alluratest.controller import setup_trove_categories, TestRestApiBase, oauth1_webtest
 from allura import model as M
 from allura.model.oauth import dummy_oauths
@@ -51,7 +50,6 @@ def unentity(s):
     return s.replace('&quot;', '"').replace('&#34;', '"')
 
 
-@with_nose_compatibility
 class TestAuth(TestController):
     def test_login(self):
         self.app.get('/auth/')
@@ -1134,7 +1132,6 @@ class TestAuth(TestController):
         assert r.content_length != 777
 
 
-@with_nose_compatibility
 class TestAuthRest(TestRestApiBase):
 
     def test_tools_list_anon(self):
@@ -1174,7 +1171,6 @@ class TestAuthRest(TestRestApiBase):
         }
 
 
-@with_nose_compatibility
 class TestPreferences(TestController):
     @td.with_user_project('test-admin')
     def test_personal_data(self):
@@ -1558,7 +1554,6 @@ class TestPreferences(TestController):
             self.app.get('/auth/not_page', status=404)
 
 
-@with_nose_compatibility
 class TestPasswordReset(TestController):
     test_primary_email = 'testprimaryaddr@mail.com'
 
@@ -1810,7 +1805,6 @@ To update your password on %s, please visit the following URL:
         assert 'Log Out' in r, r
 
 
-@with_nose_compatibility
 class TestOAuth(TestController):
     def test_register_deregister_app(self):
         # register
@@ -2145,7 +2139,6 @@ class TestOAuthAccessToken(TestController):
         self.test_access_token_ok(signature_type='query')
 
 
-@with_nose_compatibility
 class TestDisableAccount(TestController):
     def test_not_authenticated(self):
         r = self.app.get(
@@ -2190,7 +2183,6 @@ class TestDisableAccount(TestController):
         assert user.disabled == True
 
 
-@with_nose_compatibility
 class TestPasswordExpire(TestController):
     def login(self, username='test-user', pwd='foo', query_string=''):
         extra = {'username': '*anonymous', 'REMOTE_ADDR': '127.0.0.1'}
@@ -2386,7 +2378,6 @@ class TestPasswordExpire(TestController):
             assert r.location == 'http://localhost/p/test/tickets/?milestone=1.0&page=2'
 
 
-@with_nose_compatibility
 class TestCSRFProtection(TestController):
     def test_blocks_invalid(self):
         # so test-admin isn't automatically logged in for all requests
@@ -2421,7 +2412,6 @@ class TestCSRFProtection(TestController):
         assert r.form['_session_id'].value
 
 
-@with_nose_compatibility
 class TestTwoFactor(TestController):
 
     sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
diff --git a/Allura/allura/tests/functional/test_discuss.py b/Allura/allura/tests/functional/test_discuss.py
index 3c0ed26ef..8035fb93f 100644
--- a/Allura/allura/tests/functional/test_discuss.py
+++ b/Allura/allura/tests/functional/test_discuss.py
@@ -25,10 +25,8 @@ from allura.tests import TestController
 from allura import model as M
 from allura.lib import helpers as h
 from tg import config
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestDiscussBase(TestController):
 
     def _thread_link(self):
@@ -44,7 +42,6 @@ class TestDiscussBase(TestController):
         return thread_link.split('/')[-2]
 
 
-@with_nose_compatibility
 class TestDiscuss(TestDiscussBase):
 
     def _is_subscribed(self, user, thread):
@@ -399,7 +396,6 @@ class TestDiscuss(TestDiscussBase):
         r = self.app.get(post_link, status=404)
 
 
-@with_nose_compatibility
 class TestAttachment(TestDiscussBase):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/functional/test_feeds.py b/Allura/allura/tests/functional/test_feeds.py
index e795f95a9..d3d2eaa1f 100644
--- a/Allura/allura/tests/functional/test_feeds.py
+++ b/Allura/allura/tests/functional/test_feeds.py
@@ -20,10 +20,8 @@ from formencode.variabledecode import variable_encode
 from allura.tests import TestController
 from allura.tests import decorators as td
 from allura.lib import helpers as h
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestFeeds(TestController):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/functional/test_gravatar.py b/Allura/allura/tests/functional/test_gravatar.py
index b42f64a71..7f8364518 100644
--- a/Allura/allura/tests/functional/test_gravatar.py
+++ b/Allura/allura/tests/functional/test_gravatar.py
@@ -22,10 +22,8 @@ from mock import patch
 
 from allura.tests import TestController
 import allura.lib.gravatar as gravatar
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestGravatar(TestController):
 
     def test_id(self):
diff --git a/Allura/allura/tests/functional/test_home.py b/Allura/allura/tests/functional/test_home.py
index 81c11a16a..6b62fe97b 100644
--- a/Allura/allura/tests/functional/test_home.py
+++ b/Allura/allura/tests/functional/test_home.py
@@ -26,10 +26,8 @@ import allura
 from allura.tests import TestController
 from allura.tests import decorators as td
 from allura import model as M
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestProjectHome(TestController):
 
     @td.with_wiki
diff --git a/Allura/allura/tests/functional/test_nav.py b/Allura/allura/tests/functional/test_nav.py
index e7113cc77..376fad571 100644
--- a/Allura/allura/tests/functional/test_nav.py
+++ b/Allura/allura/tests/functional/test_nav.py
@@ -22,10 +22,8 @@ from tg import app_globals as g
 
 from allura.tests import TestController
 from allura.lib import helpers as h
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestNavigation(TestController):
     """
     Test div-logo and nav-left:
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index 7261e0ce0..636830de4 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -36,10 +36,8 @@ from allura.tests import decorators as td
 from allura.lib import helpers as h
 from allura.lib import utils
 from alluratest.controller import setup_trove_categories
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestNeighborhood(TestController):
 
     def setup_method(self, method):
@@ -978,7 +976,6 @@ class TestNeighborhood(TestController):
         self.app.get('/p/_nav.json')
 
 
-@with_nose_compatibility
 class TestPhoneVerificationOnProjectRegistration(TestController):
     def test_phone_verification_fragment_renders(self):
         self.app.get('/p/phone_verification_fragment', status=200)
@@ -1114,7 +1111,6 @@ class TestPhoneVerificationOnProjectRegistration(TestController):
             assert iframe.get('src') == '/p/phone_verification_fragment'
 
 
-@with_nose_compatibility
 class TestProjectImport(TestController):
 
     def test_not_found(self):
diff --git a/Allura/allura/tests/functional/test_newforge.py b/Allura/allura/tests/functional/test_newforge.py
index 83470c1e4..9dfabe614 100644
--- a/Allura/allura/tests/functional/test_newforge.py
+++ b/Allura/allura/tests/functional/test_newforge.py
@@ -21,10 +21,8 @@ from six.moves.urllib.parse import quote
 from allura.tests import TestController
 from allura.tests import decorators as td
 from allura import model as M
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestNewForgeController(TestController):
 
     @td.with_wiki
diff --git a/Allura/allura/tests/functional/test_personal_dashboard.py b/Allura/allura/tests/functional/test_personal_dashboard.py
index 06c73dea7..8cdae92ab 100644
--- a/Allura/allura/tests/functional/test_personal_dashboard.py
+++ b/Allura/allura/tests/functional/test_personal_dashboard.py
@@ -29,10 +29,8 @@ from allura.tests import TestController
 from allura.tests import decorators as td
 from alluratest.controller import setup_global_objects, setup_unit_test
 from forgetracker.tests.functional.test_root import TrackerTestController
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestPersonalDashboard(TestController):
 
     def test_dashboard(self):
@@ -69,7 +67,6 @@ class TestPersonalDashboard(TestController):
                 assert 'Section f' not in r.text
 
 
-@with_nose_compatibility
 class TestTicketsSection(TrackerTestController):
 
     @td.with_tracker
@@ -85,7 +82,6 @@ class TestTicketsSection(TrackerTestController):
         assert 'foo' in str(ticket_rows)
 
 
-@with_nose_compatibility
 class TestMergeRequestsSection(TestController):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index 26d3b5620..265cfcd12 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -30,10 +30,8 @@ from alluratest.controller import TestRestApiBase
 from allura.lib import helpers as h
 from allura.lib.exceptions import Invalid
 from allura import model as M
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestRestHome(TestRestApiBase):
 
     def _patch_token(self, OAuthAccessToken):
@@ -416,7 +414,6 @@ class TestRestHome(TestRestApiBase):
         assert r.json == {}
 
 
-@with_nose_compatibility
 class TestRestNbhdAddProject(TestRestApiBase):
 
     def setup_method(self, method):
@@ -559,7 +556,6 @@ class TestRestNbhdAddProject(TestRestApiBase):
         }
 
 
-@with_nose_compatibility
 class TestDoap(TestRestApiBase):
     validate_skip = True
     ns = '{http://usefulinc.com/ns/doap#}'
@@ -625,7 +621,6 @@ class TestDoap(TestRestApiBase):
         assert ('Tickets', 'http://localhost/p/test/private-bugs/') not in tools
 
 
-@with_nose_compatibility
 class TestUserProfile(TestRestApiBase):
     @td.with_user_project('test-admin')
     def test_profile_data(self):
diff --git a/Allura/allura/tests/functional/test_root.py b/Allura/allura/tests/functional/test_root.py
index ff196261e..6b5f56ca3 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -40,10 +40,8 @@ from allura.tests import TestController
 from allura import model as M
 from allura.lib import helpers as h
 from alluratest.controller import setup_trove_categories
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestRootController(TestController):
 
     def setup_method(self, method):
@@ -215,7 +213,6 @@ class TestRootController(TestController):
                in resp.headers.getall('Content-Security-Policy')[0]
 
 
-@with_nose_compatibility
 class TestRootWithSSLPattern(TestController):
     def setup_method(self, method):
         with td.patch_middleware_config({'force_ssl.pattern': '^/auth'}):
diff --git a/Allura/allura/tests/functional/test_search.py b/Allura/allura/tests/functional/test_search.py
index a24bc0497..3a1a72b44 100644
--- a/Allura/allura/tests/functional/test_search.py
+++ b/Allura/allura/tests/functional/test_search.py
@@ -24,10 +24,8 @@ from allura.tests import TestController
 from allura.tests.decorators import with_tool
 
 from forgewiki.model import Page
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestSearch(TestController):
 
     @patch('allura.lib.search.search')
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index 788fb4fed..f4cb04855 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -31,10 +31,8 @@ from allura.tests import decorators as td
 from allura.lib import helpers as h
 from allura.lib.decorators import task
 from allura.lib.plugin import LocalAuthenticationProvider
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestSiteAdmin(TestController):
 
     def test_access(self):
@@ -184,7 +182,6 @@ class TestSiteAdmin(TestController):
         assert json.loads(r.text)['doc'] == 'test_task doc string'
 
 
-@with_nose_compatibility
 class TestSiteAdminNotifications(TestController):
 
     def test_site_notifications_access(self):
@@ -338,7 +335,6 @@ class TestSiteAdminNotifications(TestController):
         assert M.notification.SiteNotification.query.get(_id=bson.ObjectId(note._id)) is None
 
 
-@with_nose_compatibility
 class TestProjectsSearch(TestController):
 
     TEST_HIT = MagicMock(hits=1, docs=[{
@@ -393,7 +389,6 @@ class TestProjectsSearch(TestController):
         assert ths == ['Short name', 'Full name', 'Registered', 'Deleted?', 'url', 'Details']
 
 
-@with_nose_compatibility
 class TestUsersSearch(TestController):
 
     TEST_HIT = MagicMock(hits=1, docs=[{
@@ -449,7 +444,6 @@ class TestUsersSearch(TestController):
                            'Status', 'url', 'Details']
 
 
-@with_nose_compatibility
 class TestUserDetails(TestController):
 
     def test_404(self):
@@ -745,7 +739,6 @@ To update your password on %s, please visit the following URL:
         assert hash in r.text
 
 
-@with_nose_compatibility
 class TestDeleteProjects(TestController):
 
     def confirm_form(self, r):
diff --git a/Allura/allura/tests/functional/test_static.py b/Allura/allura/tests/functional/test_static.py
index 38e4ba760..4d530a57a 100644
--- a/Allura/allura/tests/functional/test_static.py
+++ b/Allura/allura/tests/functional/test_static.py
@@ -17,10 +17,8 @@
 
 
 from allura.tests import TestController
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestStaticFilesMiddleware(TestController):
 
     # this tests StaticFilesMiddleware
diff --git a/Allura/allura/tests/functional/test_subscriber.py b/Allura/allura/tests/functional/test_subscriber.py
index 32333f92d..93b822e60 100644
--- a/Allura/allura/tests/functional/test_subscriber.py
+++ b/Allura/allura/tests/functional/test_subscriber.py
@@ -19,10 +19,8 @@ from allura.tests import TestController
 from allura.tests import decorators as td
 from allura.model.notification import Mailbox
 from allura import model as M
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestSubscriber(TestController):
 
     @td.with_user_project('test-admin')
diff --git a/Allura/allura/tests/functional/test_tool_list.py b/Allura/allura/tests/functional/test_tool_list.py
index 3562463b4..7f43bea5e 100644
--- a/Allura/allura/tests/functional/test_tool_list.py
+++ b/Allura/allura/tests/functional/test_tool_list.py
@@ -17,10 +17,8 @@
 
 from allura.tests import TestController
 from allura.tests import decorators as td
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestToolListController(TestController):
 
     @td.with_wiki
diff --git a/Allura/allura/tests/functional/test_trovecategory.py b/Allura/allura/tests/functional/test_trovecategory.py
index ae9057dd3..5067d586d 100644
--- a/Allura/allura/tests/functional/test_trovecategory.py
+++ b/Allura/allura/tests/functional/test_trovecategory.py
@@ -25,10 +25,8 @@ from allura.lib import helpers as h
 from allura.tests import TestController
 from alluratest.controller import setup_trove_categories
 from allura.tests import decorators as td
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestTroveCategory(TestController):
     @mock.patch('allura.model.project.g.post_event')
     def test_events(self, post_event):
@@ -82,7 +80,6 @@ class TestTroveCategory(TestController):
             check_access(username='root', status=200)
 
 
-@with_nose_compatibility
 class TestTroveCategoryController(TestController):
     def create_some_cats(self):
         root_parent = M.TroveCategory(fullname="Root", trove_cat_id=1, trove_parent_id=0)
diff --git a/Allura/allura/tests/functional/test_user_profile.py b/Allura/allura/tests/functional/test_user_profile.py
index 2a5e8674d..0b93bdd42 100644
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -22,7 +22,6 @@ from alluratest.controller import TestRestApiBase
 from allura.model import Project, User
 from allura.tests import decorators as td
 from allura.tests import TestController
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 class TestUserProfileSections(TestController):
@@ -65,7 +64,6 @@ class TestUserProfileSections(TestController):
         assert 'Section f' not in r.text
 
 
-@with_nose_compatibility
 class TestUserProfile(TestController):
 
     @td.with_user_project('test-admin')
@@ -278,7 +276,6 @@ class TestUserProfile(TestController):
         assert 'content="noindex, follow"' not in r.text
 
 
-@with_nose_compatibility
 class TestUserProfileHasAccessAPI(TestRestApiBase):
 
     @td.with_user_project('test-admin')
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index 7b6364938..2e77f7486 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -34,7 +34,6 @@ from allura.lib import helpers as h
 from allura.lib import plugin
 from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, setup_global_objects, setup_functional_test, setup_unit_test
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 class TestAuth:
@@ -423,7 +422,6 @@ class TestAuth:
         assert details[1].ua == 'TestBrowser/57'
 
 
-@with_nose_compatibility
 class TestAuditLog:
 
     @classmethod
diff --git a/Allura/allura/tests/model/test_filesystem.py b/Allura/allura/tests/model/test_filesystem.py
index 5625df27d..70ccdedb4 100644
--- a/Allura/allura/tests/model/test_filesystem.py
+++ b/Allura/allura/tests/model/test_filesystem.py
@@ -27,7 +27,6 @@ from webob import Request, Response
 
 from allura import model as M
 from alluratest.controller import setup_unit_test
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 class File(M.File):
@@ -37,7 +36,6 @@ class File(M.File):
 Mapper.compile_all()
 
 
-@with_nose_compatibility
 class TestFile(TestCase):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/model/test_notification.py b/Allura/allura/tests/model/test_notification.py
index 774c8dae7..3a2ba9cf8 100644
--- a/Allura/allura/tests/model/test_notification.py
+++ b/Allura/allura/tests/model/test_notification.py
@@ -30,10 +30,8 @@ from allura.model.notification import MailFooter
 from allura.lib import helpers as h
 from allura.tests import decorators as td
 from forgewiki import model as WM
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestNotification(unittest.TestCase):
 
     def setup_method(self, method):
@@ -165,7 +163,6 @@ class TestNotification(unittest.TestCase):
         )
 
 
-@with_nose_compatibility
 class TestPostNotifications(unittest.TestCase):
 
     def setup_method(self, method):
@@ -304,7 +301,6 @@ class TestPostNotifications(unittest.TestCase):
         return M.Notification.post(self.pg, 'metadata')
 
 
-@with_nose_compatibility
 class TestSubscriptionTypes(unittest.TestCase):
 
     def setup_method(self, method):
@@ -478,7 +474,6 @@ class TestSubscriptionTypes(unittest.TestCase):
         assert count == 1
 
 
-@with_nose_compatibility
 class TestSiteNotification(unittest.TestCase):
     def setup_method(self, method):
         self.note = M.SiteNotification(
diff --git a/Allura/allura/tests/model/test_repo.py b/Allura/allura/tests/model/test_repo.py
index b5fffdaa1..9b89a6805 100644
--- a/Allura/allura/tests/model/test_repo.py
+++ b/Allura/allura/tests/model/test_repo.py
@@ -28,10 +28,8 @@ from tg import config
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 from allura.lib import helpers as h
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestGitLikeTree:
     def test_set_blob(self):
         tree = M.GitLikeTree()
@@ -129,7 +127,6 @@ class RepoTestBase(unittest.TestCase):
         ]
 
 
-@with_nose_compatibility
 class TestLastCommit(unittest.TestCase):
     def setup_method(self, method):
         setup_basic_test()
@@ -402,7 +399,6 @@ class TestLastCommit(unittest.TestCase):
         self.assertEqual(lcd.by_name['file2'], commit3._id)
 
 
-@with_nose_compatibility
 class TestModelCache(unittest.TestCase):
     def setup_method(self, method):
         self.cache = M.repository.ModelCache()
@@ -681,7 +677,6 @@ class TestModelCache(unittest.TestCase):
         session.return_value.expunge.assert_called_once_with(tree1)
 
 
-@with_nose_compatibility
 class TestMergeRequest:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/model/test_timeline.py b/Allura/allura/tests/model/test_timeline.py
index f5af23bb1..ad1b252af 100644
--- a/Allura/allura/tests/model/test_timeline.py
+++ b/Allura/allura/tests/model/test_timeline.py
@@ -18,10 +18,8 @@
 from allura import model as M
 from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, setup_global_objects
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestActivityObject_Functional:
     # NOTE not for unit tests, this class sets up all the junk
 
diff --git a/Allura/allura/tests/scripts/test_create_sitemap_files.py b/Allura/allura/tests/scripts/test_create_sitemap_files.py
index 906ff600b..8097f169e 100644
--- a/Allura/allura/tests/scripts/test_create_sitemap_files.py
+++ b/Allura/allura/tests/scripts/test_create_sitemap_files.py
@@ -26,10 +26,8 @@ from alluratest.controller import setup_basic_test
 from allura import model as M
 from allura.lib import helpers as h
 from allura.scripts.create_sitemap_files import CreateSitemapFiles
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestCreateSitemapFiles:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/scripts/test_delete_projects.py b/Allura/allura/tests/scripts/test_delete_projects.py
index 12d6d69b5..0716cc700 100644
--- a/Allura/allura/tests/scripts/test_delete_projects.py
+++ b/Allura/allura/tests/scripts/test_delete_projects.py
@@ -24,10 +24,8 @@ from allura.tests.decorators import audits, out_audits, with_user_project
 from allura import model as M
 from allura.scripts import delete_projects
 from allura.lib import plugin
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestDeleteProjects(TestController):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/scripts/test_misc_scripts.py b/Allura/allura/tests/scripts/test_misc_scripts.py
index 174bf9c52..2ec6c2583 100644
--- a/Allura/allura/tests/scripts/test_misc_scripts.py
+++ b/Allura/allura/tests/scripts/test_misc_scripts.py
@@ -21,10 +21,8 @@ from allura.scripts.clear_old_notifications import ClearOldNotifications
 from alluratest.controller import setup_basic_test
 from allura import model as M
 from ming.odm import session
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestClearOldNotifications:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/scripts/test_reindexes.py b/Allura/allura/tests/scripts/test_reindexes.py
index e1f995078..ff6339093 100644
--- a/Allura/allura/tests/scripts/test_reindexes.py
+++ b/Allura/allura/tests/scripts/test_reindexes.py
@@ -21,10 +21,8 @@ from allura.scripts.reindex_users import ReindexUsers
 from allura.tests.decorators import assert_logmsg_and_no_warnings_or_errors
 from alluratest.controller import setup_basic_test
 from allura import model as M
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestReindexProjects:
 
     def setup_method(self, method):
@@ -49,7 +47,6 @@ class TestReindexProjects:
         assert M.MonQTask.query.find({'task_name': 'allura.tasks.index_tasks.add_projects'}).count() == 1
 
 
-@with_nose_compatibility
 class TestReindexUsers:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/templates/jinja_master/test_lib.py b/Allura/allura/tests/templates/jinja_master/test_lib.py
index 84108aaa5..4f294e3c7 100644
--- a/Allura/allura/tests/templates/jinja_master/test_lib.py
+++ b/Allura/allura/tests/templates/jinja_master/test_lib.py
@@ -20,7 +20,6 @@ from mock import Mock
 
 from allura.config.app_cfg import AlluraJinjaRenderer
 from alluratest.controller import setup_basic_test
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 def strip_space(s):
@@ -33,7 +32,6 @@ class TemplateTest:
         self.jinja2_env = AlluraJinjaRenderer.create(config, g)['jinja'].jinja2_env
 
 
-@with_nose_compatibility
 class TestRelatedArtifacts(TemplateTest):
 
     def _render_related_artifacts(self, artifact):
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index 9e13f60cb..fadd8ad09 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -33,7 +33,6 @@ from allura.command import base, script, set_neighborhood_features, \
 from allura import model as M
 from allura.lib.exceptions import InvalidNBFeatureValueError
 from allura.tests import decorators as td
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 test_config = pkg_resources.resource_filename(
@@ -182,7 +181,6 @@ def test_update_neighborhood():
     assert nb.has_home_tool is False
 
 
-@with_nose_compatibility
 class TestEnsureIndexCommand:
 
     def test_run(self):
@@ -269,7 +267,6 @@ class TestEnsureIndexCommand:
         ]
 
 
-@with_nose_compatibility
 class TestTaskCommand:
 
     def teardown_method(self, method):
@@ -336,7 +333,6 @@ class TestTaskCommand:
         assert M.MonQTask.query.find().count() == 0
 
 
-@with_nose_compatibility
 class TestTaskdCleanupCommand:
 
     def setup_method(self, method):
@@ -451,7 +447,6 @@ def test_status_log_retries():
     assert cmd._taskd_status.mock_calls == expected_calls
 
 
-@with_nose_compatibility
 class TestShowModels:
 
     def test_show_models(self):
@@ -463,7 +458,6 @@ class TestShowModels:
          - <FieldProperty content>
         ''' in output.captured
 
-@with_nose_compatibility
 class TestReindexAsTask:
 
     cmd = 'allura.command.show_models.ReindexCommand'
@@ -498,7 +492,6 @@ class TestReindexAsTask:
             M.MonQTask.query.remove()
 
 
-@with_nose_compatibility
 class TestReindexCommand:
 
     @patch('allura.command.show_models.g')
diff --git a/Allura/allura/tests/test_decorators.py b/Allura/allura/tests/test_decorators.py
index 2ef77cd8c..80c3bd46e 100644
--- a/Allura/allura/tests/test_decorators.py
+++ b/Allura/allura/tests/test_decorators.py
@@ -20,12 +20,10 @@ from mock import patch
 import random
 import gc
 
-from alluratest.pytest_helpers import with_nose_compatibility
 from allura.lib.decorators import task, memoize
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-@with_nose_compatibility
 class TestTask(TestCase):
 
     def setup_method(self, method):
@@ -63,7 +61,6 @@ class TestTask(TestCase):
         func.post('test', foo=2, delay=1)
 
 
-@with_nose_compatibility
 class TestMemoize:
 
     def test_function(self):
diff --git a/Allura/allura/tests/test_diff.py b/Allura/allura/tests/test_diff.py
index 1485bc1a8..e70b7d6d0 100644
--- a/Allura/allura/tests/test_diff.py
+++ b/Allura/allura/tests/test_diff.py
@@ -18,10 +18,8 @@
 import unittest
 
 from allura.lib.diff import HtmlSideBySideDiff
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestHtmlSideBySideDiff(unittest.TestCase):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/test_dispatch.py b/Allura/allura/tests/test_dispatch.py
index 983ffac72..5e48ec2fd 100644
--- a/Allura/allura/tests/test_dispatch.py
+++ b/Allura/allura/tests/test_dispatch.py
@@ -16,12 +16,10 @@
 #       under the License.
 
 from allura.tests import TestController
-from alluratest.pytest_helpers import with_nose_compatibility
 
 app = None
 
 
-@with_nose_compatibility
 class TestDispatch(TestController):
 
     validate_skip = True
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index 29b318027..01a48daf1 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -43,7 +43,6 @@ from allura import model as M
 from allura.lib import helpers as h
 from allura.lib.app_globals import ForgeMarkdown
 from allura.tests import decorators as td
-from alluratest.pytest_helpers import with_nose_compatibility
 
 from forgewiki import model as WM
 from forgeblog import model as BM
@@ -77,7 +76,6 @@ def get_projects_property_in_the_same_order(names, prop):
     return [projects_dict[name] for name in names]
 
 
-@with_nose_compatibility
 class Test():
 
     @classmethod
@@ -787,7 +785,6 @@ class Test():
             assert 'src="/p/test/screenshot/test_file.jpg/thumb"' in r
 
 
-@with_nose_compatibility
 class TestCachedMarkdown(unittest.TestCase):
 
     def setup_method(self, method):
@@ -905,7 +902,6 @@ class TestCachedMarkdown(unittest.TestCase):
         self.assertEqual(required_keys, keys)
 
 
-@with_nose_compatibility
 class TestEmojis(unittest.TestCase):
 
     def test_markdown_emoji_atomic(self):
@@ -941,7 +937,6 @@ class TestEmojis(unittest.TestCase):
         assert 'More emojis \U0001F44D\U0001F42B\U0001F552 wow!' in output
 
 
-@with_nose_compatibility
 class TestUserMentions(unittest.TestCase):
 
     def test_markdown_user_mention_default(self):
@@ -983,7 +978,6 @@ class TestUserMentions(unittest.TestCase):
         assert 'class="user-mention"' in output
 
 
-@with_nose_compatibility
 class TestHandlePaging(unittest.TestCase):
 
     def setup_method(self, method):
@@ -1044,7 +1038,6 @@ class TestHandlePaging(unittest.TestCase):
         self.assertEqual(g.handle_paging(10, 'asdf', 30), (10, 0, 0))
 
 
-@with_nose_compatibility
 class TestIconRender:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index 46703b142..571d33fea 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -37,7 +37,6 @@ from allura.lib.search import inject_user
 from allura.lib.security import has_access
 from allura.lib.security import Credentials
 from allura.tests import decorators as td
-from alluratest.pytest_helpers import with_nose_compatibility
 from alluratest.controller import setup_basic_test
 import six
 
@@ -47,7 +46,6 @@ def setup_module():
     setup_basic_test()
 
 
-@with_nose_compatibility
 class TestMakeSafePathPortion(TestCase):
 
     def setup_method(self, method):
@@ -454,7 +452,6 @@ back\\\-slash escaped
         r'tab before \(stuff\)'
 
 
-@with_nose_compatibility
 class TestUrlOpen(TestCase):
 
     @patch('six.moves.urllib.request.urlopen')
@@ -531,7 +528,6 @@ def test_login_overlay():
             raise HTTPUnauthorized()
 
 
-@with_nose_compatibility
 class TestIterEntryPoints(TestCase):
 
     def _make_ep(self, name, cls):
@@ -636,7 +632,6 @@ def test_slugify():
     assert h.slugify('Foo.Bar', True)[0] == 'Foo.Bar'
 
 
-@with_nose_compatibility
 class TestRateLimit(TestCase):
     rate_limits = '{"60": 1, "120": 3, "900": 5, "1800": 7, "3600": 10, "7200": 15, "86400": 20, "604800": 50, "2592000": 200}'
     key_comment = 'allura.rate_limits_per_user'
diff --git a/Allura/allura/tests/test_mail_util.py b/Allura/allura/tests/test_mail_util.py
index acc4c22dd..872117cf5 100644
--- a/Allura/allura/tests/test_mail_util.py
+++ b/Allura/allura/tests/test_mail_util.py
@@ -37,7 +37,6 @@ from allura.lib.mail_util import (
     _parse_message_id,
 )
 from allura.lib.exceptions import AddressException
-from alluratest.pytest_helpers import with_nose_compatibility
 from allura.tests import decorators as td
 
 
@@ -46,7 +45,6 @@ config = ConfigProxy(
     return_path='forgemail.return_path')
 
 
-@with_nose_compatibility
 class TestReactor(unittest.TestCase):
 
     def setup_method(self, method):
@@ -211,7 +209,6 @@ Content-Type: text/html; charset="utf-8"
             assert isinstance(part['payload'], str), type(part['payload'])
 
 
-@with_nose_compatibility
 class TestHeader:
 
     def test_bytestring(self):
@@ -233,7 +230,6 @@ class TestHeader:
                      '=?utf-8?b?ItGC0LXRgdC90Y/RgtGB0Y8i?= <da...@b.com>')
 
 
-@with_nose_compatibility
 class TestIsAutoreply:
 
     def setup_method(self, method):
@@ -280,7 +276,6 @@ class TestIsAutoreply:
         assert is_autoreply(self.msg)
 
 
-@with_nose_compatibility
 class TestIdentifySender:
 
     @mock.patch('allura.model.EmailAddress')
@@ -330,7 +325,6 @@ def test_parse_message_id():
     ]
 
 
-@with_nose_compatibility
 class TestMailServer:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/test_markdown.py b/Allura/allura/tests/test_markdown.py
index 6878143f1..992eec8b0 100644
--- a/Allura/allura/tests/test_markdown.py
+++ b/Allura/allura/tests/test_markdown.py
@@ -19,10 +19,8 @@ import unittest
 import mock
 
 from allura.lib import markdown_extensions as mde
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestTracRef1(unittest.TestCase):
 
     @mock.patch('allura.lib.markdown_extensions.M.Shortlink.lookup')
@@ -49,7 +47,6 @@ class TestTracRef1(unittest.TestCase):
                          '[r123](/p/project/tool/artifact)')
 
 
-@with_nose_compatibility
 class TestTracRef2(unittest.TestCase):
 
     @mock.patch('allura.lib.markdown_extensions.M.Shortlink.lookup')
@@ -79,7 +76,6 @@ class TestTracRef2(unittest.TestCase):
                          '[comment:13:ticket:100](/p/project/tool/artifact/)')
 
 
-@with_nose_compatibility
 class TestTracRef3(unittest.TestCase):
 
     def test_no_app_context(self):
@@ -98,7 +94,6 @@ class TestTracRef3(unittest.TestCase):
                          '[source:file.py#L456](/p/project/tool/HEAD/tree/file.py#l456)')
 
 
-@with_nose_compatibility
 class TestPatternReplacingProcessor(unittest.TestCase):
 
     @mock.patch('allura.lib.markdown_extensions.M.Shortlink.lookup')
@@ -113,7 +108,6 @@ class TestPatternReplacingProcessor(unittest.TestCase):
             '[ticket:100](/p/project/tool/artifact)'])
 
 
-@with_nose_compatibility
 class TestCommitMessageExtension(unittest.TestCase):
 
     @mock.patch('allura.lib.markdown_extensions.TracRef2.get_comment_slug')
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index 2435ba8d3..5a2e5c1b2 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -17,10 +17,8 @@
 
 from mock import MagicMock, patch
 from allura.lib.custom_middleware import CORSMiddleware
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestCORSMiddleware:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/test_multifactor.py b/Allura/allura/tests/test_multifactor.py
index 3ce6fdb31..42b8c6ce9 100644
--- a/Allura/allura/tests/test_multifactor.py
+++ b/Allura/allura/tests/test_multifactor.py
@@ -32,10 +32,8 @@ from allura.lib.multifactor import GoogleAuthenticatorFile, TotpService, Mongodb
 from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemTotpService
 from allura.lib.multifactor import RecoveryCodeService, MongodbRecoveryCodeService
 from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemRecoveryCodeService
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestGoogleAuthenticatorFile:
     sample = textwrap.dedent('''\
         7CL3WL756ISQCU5HRVNAODC44Q
@@ -82,7 +80,6 @@ class GenericTotpService(TotpService):
         pass
 
 
-@with_nose_compatibility
 class TestTotpService:
 
     sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
@@ -127,7 +124,6 @@ class TestTotpService:
         assert srv.get_qr_code(totp, user)
 
 
-@with_nose_compatibility
 class TestAnyTotpServiceImplementation:
 
     __test__ = False
@@ -175,7 +171,6 @@ class TestAnyTotpServiceImplementation:
             srv.verify(totp, '283397', user)
 
 
-@with_nose_compatibility
 class TestMongodbTotpService(TestAnyTotpServiceImplementation):
 
     __test__ = True
@@ -188,7 +183,6 @@ class TestMongodbTotpService(TestAnyTotpServiceImplementation):
         ming.configure(**config)
 
 
-@with_nose_compatibility
 class TestGoogleAuthenticatorPamFilesystemMixin:
 
     def setup_method(self, method):
@@ -200,7 +194,6 @@ class TestGoogleAuthenticatorPamFilesystemMixin:
             shutil.rmtree(self.totp_basedir)
 
 
-@with_nose_compatibility
 class TestGoogleAuthenticatorPamFilesystemTotpService(TestAnyTotpServiceImplementation,
                                                       TestGoogleAuthenticatorPamFilesystemMixin):
 
@@ -214,7 +207,6 @@ class TestGoogleAuthenticatorPamFilesystemTotpService(TestAnyTotpServiceImplemen
         super().test_rate_limiting()
 
 
-@with_nose_compatibility
 class TestRecoveryCodeService:
 
     def test_generate_one_code(self):
@@ -237,7 +229,6 @@ class TestRecoveryCodeService:
         assert len(recovery.saved_codes) == asint(config.get('auth.multifactor.recovery_code.count', 10))
 
 
-@with_nose_compatibility
 class TestAnyRecoveryCodeServiceImplementation:
 
     __test__ = False
@@ -305,7 +296,6 @@ class TestAnyRecoveryCodeServiceImplementation:
             recovery.verify_and_remove_code(user, '22222')
 
 
-@with_nose_compatibility
 class TestMongodbRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation):
 
     __test__ = True
@@ -319,7 +309,6 @@ class TestMongodbRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation):
         ming.configure(**config)
 
 
-@with_nose_compatibility
 class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation,
                                                               TestGoogleAuthenticatorPamFilesystemMixin):
 
diff --git a/Allura/allura/tests/test_plugin.py b/Allura/allura/tests/test_plugin.py
index 49206ba64..c6c78fc77 100644
--- a/Allura/allura/tests/test_plugin.py
+++ b/Allura/allura/tests/test_plugin.py
@@ -37,14 +37,12 @@ from allura.lib.exceptions import ProjectConflict, ProjectShortnameInvalid
 from allura.tests.decorators import audits
 from allura.tests.exclude_from_rewrite_hook import ThemeProviderTestApp
 from alluratest.controller import setup_basic_test, setup_global_objects
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 def setup_module(module):
     setup_basic_test()
 
 
-@with_nose_compatibility
 class TestProjectRegistrationProvider:
 
     def setup_method(self, method):
@@ -82,7 +80,6 @@ class TestProjectRegistrationProvider:
         pytest.raises(ProjectConflict, v, 'thisislegit', neighborhood=nbhd)
 
 
-@with_nose_compatibility
 class TestProjectRegistrationProviderParseProjectFromUrl:
 
     def setup_method(self, method):
@@ -157,7 +154,6 @@ class UserMock:
         return self._projects
 
 
-@with_nose_compatibility
 class TestProjectRegistrationProviderPhoneVerification:
 
     def setup_method(self, method):
@@ -275,7 +271,6 @@ class TestProjectRegistrationProviderPhoneVerification:
             assert 5 == g.phone_service.verify.call_count
 
 
-@with_nose_compatibility
 class TestThemeProvider:
 
     @patch('allura.app.g')
@@ -301,7 +296,6 @@ class TestThemeProvider:
         g.theme_href.assert_called_with('images/testapp_24.png')
 
 
-@with_nose_compatibility
 class TestThemeProvider_notifications:
 
     Provider = ThemeProvider
@@ -623,7 +617,6 @@ class TestThemeProvider_notifications:
         assert get_note[1] == 'testid-2-False'
 
 
-@with_nose_compatibility
 class TestLocalAuthenticationProvider:
 
     def setup_method(self, method):
@@ -738,7 +731,6 @@ class TestLocalAuthenticationProvider:
         assert detail.ua == 'mybrowser'
 
 
-@with_nose_compatibility
 class TestAuthenticationProvider:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/test_scripttask.py b/Allura/allura/tests/test_scripttask.py
index ae284a1e9..9992498a5 100644
--- a/Allura/allura/tests/test_scripttask.py
+++ b/Allura/allura/tests/test_scripttask.py
@@ -19,10 +19,8 @@ import unittest
 import mock
 
 from allura.scripts.scripttask import ScriptTask
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestScriptTask(unittest.TestCase):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/test_security.py b/Allura/allura/tests/test_security.py
index fea9bc54d..7a27b9302 100644
--- a/Allura/allura/tests/test_security.py
+++ b/Allura/allura/tests/test_security.py
@@ -28,7 +28,6 @@ from forgewiki import model as WM
 from allura.lib.security import HIBPClientError, HIBPClient
 from mock import Mock, patch
 from requests.exceptions import Timeout
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 def _allow(obj, role, perm):
@@ -55,7 +54,6 @@ def test_check_breached_password(r_get):
         HIBPClient.check_breached_password('qwerty')
 
 
-@with_nose_compatibility
 class TestSecurity(TestController):
 
     validate_skip = True
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 80fffbb8c..3acd939b9 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -47,12 +47,10 @@ from allura.tasks import repo_tasks
 from allura.tasks import export_tasks
 from allura.tasks import admin_tasks
 from allura.tests import decorators as td
-from alluratest.pytest_helpers import with_nose_compatibility
 from allura.tests.exclude_from_rewrite_hook import raise_compound_exception
 from allura.lib.decorators import event_handler, task
 
 
-@with_nose_compatibility
 class TestRepoTasks(unittest.TestCase):
 
     def setup_method(self, method):
@@ -101,7 +99,6 @@ def _task_that_creates_event(event_name,):
     assert not M.MonQTask.query.get(task_name='allura.tasks.event_tasks.event', args=[event_name])
 
 
-@with_nose_compatibility
 class TestEventTasks(unittest.TestCase):
 
     def setup_method(self, method):
@@ -160,7 +157,6 @@ class TestEventTasks(unittest.TestCase):
             assert ('assert %d' % x) in t.result
 
 
-@with_nose_compatibility
 class TestIndexTasks(unittest.TestCase):
 
     def setup_method(self, method):
@@ -246,7 +242,6 @@ class TestIndexTasks(unittest.TestCase):
         solr.delete.assert_called_once_with(q=solr_query)
 
 
-@with_nose_compatibility
 class TestMailTasks(unittest.TestCase):
 
     def setup_method(self, method):
@@ -543,7 +538,6 @@ class TestMailTasks(unittest.TestCase):
             assert hm.call_count == 0
 
 
-@with_nose_compatibility
 class TestUserNotificationTasks(TestController):
     def setup_method(self, method):
         super().setup_method(method)
@@ -575,7 +569,6 @@ class TestUserNotificationTasks(TestController):
         assert 'auth/subscriptions#notifications' in text
 
 
-@with_nose_compatibility
 class TestNotificationTasks(unittest.TestCase):
 
     def setup_method(self, method):
@@ -611,7 +604,6 @@ class _TestArtifact(M.Artifact):
             text=self.text)
 
 
-@with_nose_compatibility
 class TestExportTasks(unittest.TestCase):
 
     def setup_method(self, method):
@@ -666,7 +658,6 @@ class TestExportTasks(unittest.TestCase):
         assert c.project.bulk_export_status() == 'busy'
 
 
-@with_nose_compatibility
 class TestAdminTasks(unittest.TestCase):
 
     def test_install_app_docstring(self):
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index 5afc2c9b8..d9f5229a9 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -39,11 +39,9 @@ from alluratest.controller import setup_unit_test
 from allura import model as M
 from allura.lib import utils
 from allura.lib import helpers as h
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 @patch.dict('allura.lib.utils.tg.config', clear=True, foo='bar', baz='true')
-@with_nose_compatibility
 class TestConfigProxy(unittest.TestCase):
 
     def setup_method(self, method):
@@ -64,7 +62,6 @@ class TestConfigProxy(unittest.TestCase):
         self.assertEqual(self.cp.get_bool("fake"), False)
 
 
-@with_nose_compatibility
 class TestChunkedIterator(unittest.TestCase):
 
     def setup_method(self, method):
@@ -96,7 +93,6 @@ class TestChunkedIterator(unittest.TestCase):
         assert chunks[1][0].username == 'sample-user-3'
 
 
-@with_nose_compatibility
 class TestChunkedList(unittest.TestCase):
 
     def test_chunked_list(self):
@@ -107,7 +103,6 @@ class TestChunkedList(unittest.TestCase):
         self.assertEqual([el for sublist in chunks for el in sublist], l)
 
 
-@with_nose_compatibility
 class TestAntispam(unittest.TestCase):
 
     def setup_method(self, method):
@@ -174,7 +169,6 @@ class TestAntispam(unittest.TestCase):
         return encrypted_form
 
 
-@with_nose_compatibility
 class TestTruthyCallable(unittest.TestCase):
 
     def test_everything(self):
@@ -196,7 +190,6 @@ class TestTruthyCallable(unittest.TestCase):
         assert false_predicate == f
 
 
-@with_nose_compatibility
 class TestCaseInsensitiveDict(unittest.TestCase):
 
     def test_everything(self):
@@ -216,7 +209,6 @@ class TestCaseInsensitiveDict(unittest.TestCase):
         assert d == utils.CaseInsensitiveDict(Foo=1, bar=2)
 
 
-@with_nose_compatibility
 class TestLineAnchorCodeHtmlFormatter(unittest.TestCase):
 
     def test_render(self):
@@ -237,7 +229,6 @@ class TestLineAnchorCodeHtmlFormatter(unittest.TestCase):
             assert '<span class="linenos">1</span>' in hl_code
 
 
-@with_nose_compatibility
 class TestIsTextFile(unittest.TestCase):
 
     def test_is_text_file(self):
@@ -248,7 +239,6 @@ class TestIsTextFile(unittest.TestCase):
         assert not utils.is_text_file(open(bin_file, 'rb').read())
 
 
-@with_nose_compatibility
 class TestCodeStats(unittest.TestCase):
 
     def setup_method(self, method):
@@ -273,7 +263,6 @@ class TestCodeStats(unittest.TestCase):
         assert stats['code_size'] == len(blob.text)
 
 
-@with_nose_compatibility
 class TestHTMLSanitizer(unittest.TestCase):
 
     def walker_from_text(self, text):
diff --git a/Allura/allura/tests/test_validators.py b/Allura/allura/tests/test_validators.py
index a9c011368..fb3dbb008 100644
--- a/Allura/allura/tests/test_validators.py
+++ b/Allura/allura/tests/test_validators.py
@@ -24,7 +24,6 @@ from allura.lib import validators as v
 from allura.lib.decorators import task
 from alluratest.controller import setup_basic_test
 from allura.websetup.bootstrap import create_user
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 def _setup_method():
@@ -36,7 +35,6 @@ def dummy_task(*args, **kw):
     pass
 
 
-@with_nose_compatibility
 class TestJsonConverter(unittest.TestCase):
     val = v.JsonConverter
 
@@ -53,7 +51,6 @@ class TestJsonConverter(unittest.TestCase):
             self.val.to_python('3')
 
 
-@with_nose_compatibility
 class TestJsonFile(unittest.TestCase):
 
     def setup_method(self, method):
@@ -74,7 +71,6 @@ class TestJsonFile(unittest.TestCase):
             self.val.to_python(self.FieldStorage('{'))
 
 
-@with_nose_compatibility
 class TestUserMapFile(unittest.TestCase):
     val = v.UserMapJsonFile()
 
@@ -100,7 +96,6 @@ class TestUserMapFile(unittest.TestCase):
             self.FieldStorage('{"user_old": "user_new"}')))
 
 
-@with_nose_compatibility
 class TestUserValidator(unittest.TestCase):
     val = v.UserValidator
 
@@ -117,7 +112,6 @@ class TestUserValidator(unittest.TestCase):
         self.assertEqual(str(cm.exception), "Invalid username")
 
 
-@with_nose_compatibility
 class TestAnonymousValidator(unittest.TestCase):
     val = v.AnonymousValidator
 
@@ -137,7 +131,6 @@ class TestAnonymousValidator(unittest.TestCase):
         self.assertEqual(str(cm.exception), "Log in to Mark as Private")
 
 
-@with_nose_compatibility
 class TestMountPointValidator(unittest.TestCase):
 
     def setup_method(self, method):
@@ -201,7 +194,6 @@ class TestMountPointValidator(unittest.TestCase):
         self.assertEqual('wiki-0', val.to_python(None))
 
 
-@with_nose_compatibility
 class TestTaskValidator(unittest.TestCase):
     val = v.TaskValidator
 
@@ -235,7 +227,6 @@ class TestTaskValidator(unittest.TestCase):
                          '"allura.tests.test_validators._setup_method" is not a task.')
 
 
-@with_nose_compatibility
 class TestPathValidator(unittest.TestCase):
     val = v.PathValidator(strip=True, if_missing={}, if_empty={})
 
@@ -287,7 +278,6 @@ class TestPathValidator(unittest.TestCase):
         self.assertEqual({}, self.val.to_python(''))
 
 
-@with_nose_compatibility
 class TestUrlValidator(unittest.TestCase):
     val = v.URL
 
@@ -309,7 +299,6 @@ class TestUrlValidator(unittest.TestCase):
         self.assertEqual(str(cm.exception), 'That is not a valid URL')
 
 
-@with_nose_compatibility
 class TestNonHttpUrlValidator(unittest.TestCase):
     val = v.NonHttpUrl
 
@@ -331,7 +320,6 @@ class TestNonHttpUrlValidator(unittest.TestCase):
         self.assertEqual(str(cm.exception), 'You must start your URL with a scheme')
 
 
-@with_nose_compatibility
 class TestIconValidator(unittest.TestCase):
     val = v.IconValidator
 
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 4494d9a6b..d0785343a 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -43,7 +43,6 @@ from alluratest.controller import (
     TestRestApiBase,
 )
 import six
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 # important to be distinct from 'test' and 'test2' which ForgeGit and
@@ -54,7 +53,6 @@ with_git = td.with_tool(test_project_with_repo, 'git', 'src', 'Git')
 with_git2 = td.with_tool(test_project_with_repo, 'git', 'src2', 'Git2')
 
 
-@with_nose_compatibility
 class TestWebhookBase:
     def setup_method(self, method):
         setup_basic_test()
@@ -86,7 +84,6 @@ class TestWebhookBase:
         return [repo_init]
 
 
-@with_nose_compatibility
 class TestValidators(TestWebhookBase):
     @with_git2
     def test_webhook_validator(self):
@@ -124,7 +121,6 @@ class TestValidators(TestWebhookBase):
         assert v.to_python(str(wh._id)) == wh
 
 
-@with_nose_compatibility
 class TestWebhookController(TestController):
 
     def setup_method(self, method):
@@ -417,7 +413,6 @@ class TestWebhookController(TestController):
         return [text(tds[0]), text(tds[1]), link(tds[2]), delete_btn(tds[3])]
 
 
-@with_nose_compatibility
 class TestSendWebhookHelper(TestWebhookBase):
     def setup_method(self, method):
         super().setup_method(method)
@@ -522,7 +517,6 @@ class TestSendWebhookHelper(TestWebhookBase):
                     requests.post.return_value.headers))
 
 
-@with_nose_compatibility
 class TestRepoPushWebhookSender(TestWebhookBase):
     @patch('allura.webhooks.send_webhook', autospec=True)
     def test_send(self, send_webhook):
@@ -629,7 +623,6 @@ class TestRepoPushWebhookSender(TestWebhookBase):
         assert sender._convert_id('a433fa9:13') == 'r13'
 
 
-@with_nose_compatibility
 class TestModels(TestWebhookBase):
     def test_webhook_url(self):
         assert (self.wh.url() ==
@@ -671,7 +664,6 @@ class TestModels(TestWebhookBase):
         assert self.wh.__json__() == expected
 
 
-@with_nose_compatibility
 class TestWebhookRestController(TestRestApiBase):
     def setup_method(self, method):
         super().setup_method(method)
diff --git a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
index 133301f23..f689fa5f7 100644
--- a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
+++ b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
@@ -23,10 +23,8 @@ from allura.tests.unit.factories import create_post, create_discussion
 from allura import model
 from allura.controllers.discuss import ModerationController
 from allura.tests.unit import patches
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestWhenModerating(WithDatabase):
     patches = [patches.fake_app_patch,
                patches.fake_user_patch,
@@ -74,7 +72,6 @@ class TestWhenModerating(WithDatabase):
         return model.Post.query.get(slug='mypost', deleted=False)
 
 
-@with_nose_compatibility
 class TestIndexWithNoPosts(WithDatabase):
     patches = [patches.fake_app_patch]
 
@@ -84,7 +81,6 @@ class TestIndexWithNoPosts(WithDatabase):
         assert template_variables['posts'].all() == []
 
 
-@with_nose_compatibility
 class TestIndexWithAPostInTheDiscussion(WithDatabase):
     patches = [patches.fake_app_patch]
 
diff --git a/Allura/allura/tests/unit/phone/test_nexmo.py b/Allura/allura/tests/unit/phone/test_nexmo.py
index 1b6139fa8..f2e99dacd 100644
--- a/Allura/allura/tests/unit/phone/test_nexmo.py
+++ b/Allura/allura/tests/unit/phone/test_nexmo.py
@@ -17,12 +17,10 @@
 
 import json
 from mock import patch
-from alluratest.pytest_helpers import with_nose_compatibility
 
 from allura.lib.phone.nexmo import NexmoPhoneService
 
 
-@with_nose_compatibility
 class TestPhoneService:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/phone/test_phone_service.py b/Allura/allura/tests/unit/phone/test_phone_service.py
index 739594475..faceb374c 100644
--- a/Allura/allura/tests/unit/phone/test_phone_service.py
+++ b/Allura/allura/tests/unit/phone/test_phone_service.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 from allura.lib.phone import PhoneService
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 class MockPhoneService(PhoneService):
@@ -28,7 +27,6 @@ class MockPhoneService(PhoneService):
         return {'status': 'ok'}
 
 
-@with_nose_compatibility
 class TestPhoneService:
 
     def test_verify(self):
diff --git a/Allura/allura/tests/unit/spam/test_akismet.py b/Allura/allura/tests/unit/spam/test_akismet.py
index 384ce1b32..268545dd9 100644
--- a/Allura/allura/tests/unit/spam/test_akismet.py
+++ b/Allura/allura/tests/unit/spam/test_akismet.py
@@ -26,11 +26,9 @@ from datetime import datetime
 from bson import ObjectId
 
 from allura.lib.spam.akismetfilter import AKISMET_AVAILABLE, AkismetSpamFilter
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 @unittest.skipIf(not AKISMET_AVAILABLE, "Akismet not available")
-@with_nose_compatibility
 class TestAkismet(unittest.TestCase):
 
     @mock.patch('allura.lib.spam.akismetfilter.akismet')
diff --git a/Allura/allura/tests/unit/spam/test_spam_filter.py b/Allura/allura/tests/unit/spam/test_spam_filter.py
index 93b01b984..ad32d690a 100644
--- a/Allura/allura/tests/unit/spam/test_spam_filter.py
+++ b/Allura/allura/tests/unit/spam/test_spam_filter.py
@@ -25,7 +25,6 @@ from allura import model as M
 from allura.model.artifact import SpamCheckResult
 from alluratest.controller import setup_basic_test
 from forgewiki import model as WM
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 class MockFilter(SpamFilter):
@@ -46,7 +45,6 @@ class MockFilter2(SpamFilter):
         return True
 
 
-@with_nose_compatibility
 class TestSpamFilter(unittest.TestCase):
 
     def test_check(self):
@@ -75,7 +73,6 @@ class TestSpamFilter(unittest.TestCase):
         self.assertTrue(log.exception.called)
 
 
-@with_nose_compatibility
 class TestSpamFilterFunctional:
 
     def setup_method(self, method):
@@ -95,7 +92,6 @@ class TestSpamFilterFunctional:
         assert results[0].user.username == 'test-user'
 
 
-@with_nose_compatibility
 class TestChainedSpamFilter:
 
     def test(self):
diff --git a/Allura/allura/tests/unit/spam/test_stopforumspam.py b/Allura/allura/tests/unit/spam/test_stopforumspam.py
index 296f63a2c..bb827183e 100644
--- a/Allura/allura/tests/unit/spam/test_stopforumspam.py
+++ b/Allura/allura/tests/unit/spam/test_stopforumspam.py
@@ -21,10 +21,8 @@ import mock
 from bson import ObjectId
 
 from allura.lib.spam.stopforumspamfilter import StopForumSpamSpamFilter
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestStopForumSpam:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/test_app.py b/Allura/allura/tests/unit/test_app.py
index 65d000fae..e7d0fbd79 100644
--- a/Allura/allura/tests/unit/test_app.py
+++ b/Allura/allura/tests/unit/test_app.py
@@ -22,10 +22,8 @@ from allura import model
 from allura.tests.unit import WithDatabase
 from allura.tests.unit.patches import fake_app_patch
 from allura.tests.unit.factories import create_project, create_app_config
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestApplication(TestCase):
 
     def test_validate_mount_point(self):
@@ -54,7 +52,6 @@ class TestApplication(TestCase):
         self.assertEqual(f('does_not_exist'), '')
 
 
-@with_nose_compatibility
 class TestInstall(WithDatabase):
     patches = [fake_app_patch]
 
@@ -67,7 +64,6 @@ class TestInstall(WithDatabase):
         return model.Discussion.query.find().count()
 
 
-@with_nose_compatibility
 class TestDefaultDiscussion(WithDatabase):
     patches = [fake_app_patch]
 
@@ -88,7 +84,6 @@ class TestDefaultDiscussion(WithDatabase):
         assert self.discussion.shortname == 'my_mounted_app'
 
 
-@with_nose_compatibility
 class TestAppDefaults(WithDatabase):
     patches = [fake_app_patch]
 
diff --git a/Allura/allura/tests/unit/test_artifact.py b/Allura/allura/tests/unit/test_artifact.py
index 1cbe17f47..895eb74ec 100644
--- a/Allura/allura/tests/unit/test_artifact.py
+++ b/Allura/allura/tests/unit/test_artifact.py
@@ -18,10 +18,8 @@
 import unittest
 
 from allura import model as M
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestArtifact(unittest.TestCase):
 
     def test_translate_query(self):
diff --git a/Allura/allura/tests/unit/test_discuss.py b/Allura/allura/tests/unit/test_discuss.py
index 6078dff49..6f8eba841 100644
--- a/Allura/allura/tests/unit/test_discuss.py
+++ b/Allura/allura/tests/unit/test_discuss.py
@@ -18,10 +18,8 @@
 from allura import model as M
 from allura.tests.unit import WithDatabase
 from allura.tests.unit.patches import fake_app_patch
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestThread(WithDatabase):
     patches = [fake_app_patch]
 
diff --git a/Allura/allura/tests/unit/test_helpers/test_ago.py b/Allura/allura/tests/unit/test_helpers/test_ago.py
index ee4d0925f..8e6406e9a 100644
--- a/Allura/allura/tests/unit/test_helpers/test_ago.py
+++ b/Allura/allura/tests/unit/test_helpers/test_ago.py
@@ -20,10 +20,8 @@ from datetime import datetime
 from mock import patch
 
 from allura.lib import helpers
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestAgo:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/test_helpers/test_set_context.py b/Allura/allura/tests/unit/test_helpers/test_set_context.py
index 68b8898e4..171cf4425 100644
--- a/Allura/allura/tests/unit/test_helpers/test_set_context.py
+++ b/Allura/allura/tests/unit/test_helpers/test_set_context.py
@@ -26,10 +26,8 @@ from allura.tests.unit import patches
 from allura.tests.unit.factories import (create_project,
                                          create_app_config,
                                          create_neighborhood)
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestWhenProjectIsFoundAndAppIsNot(WithDatabase):
 
     def setup_method(self, method):
@@ -44,7 +42,6 @@ class TestWhenProjectIsFoundAndAppIsNot(WithDatabase):
         assert c.app is None, c.app
 
 
-@with_nose_compatibility
 class TestWhenProjectIsFoundInNeighborhood(WithDatabase):
 
     def setup_method(self, method):
@@ -59,7 +56,6 @@ class TestWhenProjectIsFoundInNeighborhood(WithDatabase):
         assert c.app is None
 
 
-@with_nose_compatibility
 class TestWhenAppIsFoundByID(WithDatabase):
     patches = [patches.project_app_loading_patch]
 
@@ -77,7 +73,6 @@ class TestWhenAppIsFoundByID(WithDatabase):
         self.project_app_instance_function.assert_called_with(self.app_config)
 
 
-@with_nose_compatibility
 class TestWhenAppIsFoundByMountPoint(WithDatabase):
     patches = [patches.project_app_loading_patch]
 
@@ -96,7 +91,6 @@ class TestWhenAppIsFoundByMountPoint(WithDatabase):
             'my_mounted_app')
 
 
-@with_nose_compatibility
 class TestWhenProjectIsNotFound(WithDatabase):
 
     def test_that_it_raises_an_exception(self):
@@ -114,7 +108,6 @@ class TestWhenProjectIsNotFound(WithDatabase):
                       neighborhood=None)
 
 
-@with_nose_compatibility
 class TestWhenNeighborhoodIsNotFound(WithDatabase):
 
     def test_that_it_raises_an_exception(self):
diff --git a/Allura/allura/tests/unit/test_ldap_auth_provider.py b/Allura/allura/tests/unit/test_ldap_auth_provider.py
index c66da2b24..a480be211 100644
--- a/Allura/allura/tests/unit/test_ldap_auth_provider.py
+++ b/Allura/allura/tests/unit/test_ldap_auth_provider.py
@@ -31,10 +31,8 @@ from allura.lib import plugin
 from allura.lib import helpers as h
 from allura import model as M
 import six
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestLdapAuthenticationProvider:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/test_mixins.py b/Allura/allura/tests/unit/test_mixins.py
index 775ff0358..ac29a59d5 100644
--- a/Allura/allura/tests/unit/test_mixins.py
+++ b/Allura/allura/tests/unit/test_mixins.py
@@ -17,10 +17,8 @@
 
 from mock import Mock
 from allura.model import VotableArtifact
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestVotableArtifact:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/test_package_path_loader.py b/Allura/allura/tests/unit/test_package_path_loader.py
index 5e3702715..44bb6fbfe 100644
--- a/Allura/allura/tests/unit/test_package_path_loader.py
+++ b/Allura/allura/tests/unit/test_package_path_loader.py
@@ -25,10 +25,8 @@ import pytest
 from tg import config
 
 from allura.lib.package_path_loader import PackagePathLoader
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestPackagePathLoader(TestCase):
 
     @mock.patch('pkg_resources.resource_filename')
diff --git a/Allura/allura/tests/unit/test_post_model.py b/Allura/allura/tests/unit/test_post_model.py
index eca936d1e..7cbcf4682 100644
--- a/Allura/allura/tests/unit/test_post_model.py
+++ b/Allura/allura/tests/unit/test_post_model.py
@@ -22,10 +22,8 @@ from allura import model as M
 from allura.tests.unit import WithDatabase
 from allura.tests.unit import patches
 from allura.tests.unit.factories import create_post
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestPostModel(WithDatabase):
     patches = [patches.fake_app_patch,
                patches.disable_notifications_patch]
diff --git a/Allura/allura/tests/unit/test_project.py b/Allura/allura/tests/unit/test_project.py
index 2fb093643..37af31d18 100644
--- a/Allura/allura/tests/unit/test_project.py
+++ b/Allura/allura/tests/unit/test_project.py
@@ -23,10 +23,8 @@ from tg import config
 from allura import model as M
 from allura.lib import helpers as h
 from allura.app import SitemapEntry
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestProject(unittest.TestCase):
 
     def test_grouped_navbar_entries(self):
diff --git a/Allura/allura/tests/unit/test_repo.py b/Allura/allura/tests/unit/test_repo.py
index f67e69a96..47945e24f 100644
--- a/Allura/allura/tests/unit/test_repo.py
+++ b/Allura/allura/tests/unit/test_repo.py
@@ -29,10 +29,8 @@ from allura.model.repository import zipdir, prefix_paths_union
 from allura.model.repo_refresh import (
     _group_commits,
 )
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestTopoSort(unittest.TestCase):
 
     def test_commit_dates_out_of_order(self):
@@ -85,7 +83,6 @@ def blob(name, id):
     return b
 
 
-@with_nose_compatibility
 class TestTree(unittest.TestCase):
 
     @patch('allura.model.repository.Tree.__getitem__')
@@ -103,7 +100,6 @@ class TestTree(unittest.TestCase):
         getitem().__getitem__().__getitem__.assert_called_with('file.txt')
 
 
-@with_nose_compatibility
 class TestBlob(unittest.TestCase):
 
     def test_pypeline_view(self):
@@ -144,7 +140,6 @@ class TestBlob(unittest.TestCase):
         assert blob.has_html_view == True
 
 
-@with_nose_compatibility
 class TestCommit(unittest.TestCase):
 
     def test_activity_extras(self):
@@ -246,7 +241,6 @@ class TestCommit(unittest.TestCase):
         commit.get_tree.assert_called_with(create=True)
 
 
-@with_nose_compatibility
 class TestZipDir(unittest.TestCase):
 
     @patch('allura.model.repository.Popen')
@@ -290,7 +284,6 @@ class TestZipDir(unittest.TestCase):
         self.assertTrue("STDERR: 2" in emsg)
 
 
-@with_nose_compatibility
 class TestPrefixPathsUnion(unittest.TestCase):
 
     def test_disjoint(self):
@@ -309,7 +302,6 @@ class TestPrefixPathsUnion(unittest.TestCase):
         self.assertEqual(prefix_paths_union(a, b), {'a2'})
 
 
-@with_nose_compatibility
 class TestGroupCommits:
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/test_session.py b/Allura/allura/tests/unit/test_session.py
index d7899302f..e2042c039 100644
--- a/Allura/allura/tests/unit/test_session.py
+++ b/Allura/allura/tests/unit/test_session.py
@@ -28,7 +28,6 @@ from allura.model.session import (
     ArtifactSessionExtension,
     substitute_extensions,
 )
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 def test_extensions_cm():
@@ -75,7 +74,6 @@ def test_extensions_cm_flush_raises():
     assert session._kwargs['extensions'] == []
 
 
-@with_nose_compatibility
 class TestSessionExtension(TestCase):
 
     def _mock_indexable(self, **kw):
@@ -85,7 +83,6 @@ class TestSessionExtension(TestCase):
         return m
 
 
-@with_nose_compatibility
 class TestIndexerSessionExtension(TestSessionExtension):
 
     def setup_method(self, method):
@@ -124,7 +121,6 @@ class TestIndexerSessionExtension(TestSessionExtension):
         assert self.tasks['add'].post.call_count == 0
 
 
-@with_nose_compatibility
 class TestArtifactSessionExtension(TestSessionExtension):
 
     def setup_method(self, method):
@@ -154,7 +150,6 @@ class TestArtifactSessionExtension(TestSessionExtension):
         assert index_tasks.add_artifacts.post.call_count == 0
 
 
-@with_nose_compatibility
 class TestBatchIndexer(TestCase):
 
     def setup_method(self, method):
diff --git a/Allura/allura/tests/unit/test_sitemapentry.py b/Allura/allura/tests/unit/test_sitemapentry.py
index ea38b10b7..bdd6399d1 100644
--- a/Allura/allura/tests/unit/test_sitemapentry.py
+++ b/Allura/allura/tests/unit/test_sitemapentry.py
@@ -19,10 +19,8 @@ import unittest
 from mock import Mock
 
 from allura.app import SitemapEntry
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestSitemapEntry(unittest.TestCase):
 
     def test_matches_url(self):
diff --git a/Allura/allura/tests/unit/test_solr.py b/Allura/allura/tests/unit/test_solr.py
index 8d15e65ad..c24bfdb35 100644
--- a/Allura/allura/tests/unit/test_solr.py
+++ b/Allura/allura/tests/unit/test_solr.py
@@ -25,10 +25,8 @@ from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test
 from allura.lib.solr import Solr, escape_solr_arg
 from allura.lib.search import search_app, SearchIndexable
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
-@with_nose_compatibility
 class TestSolr(unittest.TestCase):
 
     def setup_method(self, method):
@@ -122,7 +120,6 @@ class TestSolr(unittest.TestCase):
             'username_s:admin1 || username_s:root', fq=fq, ignore_errors=False)
 
 
-@with_nose_compatibility
 class TestSearchIndexable(unittest.TestCase):
 
     def setup_method(self, method):
@@ -147,7 +144,6 @@ class TestSearchIndexable(unittest.TestCase):
         assert self.obj.solarize() == dict(text='<script>a(1)</script>')
 
 
-@with_nose_compatibility
 class TestSearch_app(unittest.TestCase):
 
     def setup_method(self, method):
diff --git a/AlluraTest/alluratest/controller.py b/AlluraTest/alluratest/controller.py
index 207946075..f110c7a57 100644
--- a/AlluraTest/alluratest/controller.py
+++ b/AlluraTest/alluratest/controller.py
@@ -19,7 +19,6 @@
 from __future__ import annotations
 
 import os
-from alluratest.pytest_helpers import with_nose_compatibility
 import six.moves.urllib.request
 import six.moves.urllib.parse
 import six.moves.urllib.error
@@ -163,14 +162,12 @@ def setup_trove_categories():
         create_trove_categories.run([''])
 
 
-@with_nose_compatibility
 class TestController:
 
     application_under_test = 'main'
     validate_skip = False
 
     def setup_method(self, method=None):
-        """Method called by nose before running each test"""
         pkg = self.__module__.split('.')[0]
         self.app = ValidatingTestApp(
             setup_functional_test(app_name=self.application_under_test, current_pkg=pkg))
@@ -182,7 +179,6 @@ class TestController:
             self.smtp_mock.start()
 
     def teardown_method(self, method=None):
-        """Method called by nose after running each test"""
         if asbool(tg.config.get('smtp.mock')):
             self.smtp_mock.stop()
 
@@ -212,7 +208,6 @@ class TestController:
                 return f
 
 
-@with_nose_compatibility
 class TestRestApiBase(TestController):
 
     def setup_method(self, method):
diff --git a/AlluraTest/alluratest/pytest_helpers.py b/AlluraTest/alluratest/pytest_helpers.py
deleted file mode 100644
index 832cfa796..000000000
--- a/AlluraTest/alluratest/pytest_helpers.py
+++ /dev/null
@@ -1,49 +0,0 @@
-
-IS_NOSE = None
-
-
-def is_called_by_nose():
-    global IS_NOSE
-    if IS_NOSE is None:
-        import inspect
-        stack = inspect.stack()
-        IS_NOSE = any(x[0].f_globals['__name__'].startswith('nose.') for x in stack)
-    return IS_NOSE
-
-
-def with_nose_compatibility(test_class):
-
-    if not is_called_by_nose():
-        return test_class
-
-    def setUp_(self):
-        setup_method = hasattr(self, 'setup_method')
-        if setup_method:
-            self.setup_method(None)
-    if hasattr(test_class, 'setup_method'):
-        test_class.setUp = setUp_
-
-    def tearDown_(self):
-        teardown_method = hasattr(self, 'teardown_method')
-        if teardown_method:
-            self.teardown_method(None)
-    if hasattr(test_class, 'teardown_method'):
-        test_class.tearDown = tearDown_
-
-    @classmethod
-    def setUpClass_(cls):
-        setup_class = hasattr(cls, 'setup_class')
-        if setup_class:
-            cls.setup_class()
-    if hasattr(test_class, 'setup_class'):
-        test_class.setUpClass = setUpClass_
-
-    @classmethod
-    def tearDownClass_(cls):
-        teardown_class = hasattr(cls, 'teardown_class')
-        if teardown_class:
-            cls.teardown_class()
-    if hasattr(test_class, 'teardown_class'):
-        test_class.tearDownClass = tearDownClass_
-
-    return test_class
diff --git a/AlluraTest/alluratest/test_syntax.py b/AlluraTest/alluratest/test_syntax.py
index 658d676fc..0d93f4c0d 100644
--- a/AlluraTest/alluratest/test_syntax.py
+++ b/AlluraTest/alluratest/test_syntax.py
@@ -27,7 +27,7 @@ toplevel_dir = os.path.abspath(os.path.dirname(__file__) + "/../..")
 
 def run(cmd):
     proc = Popen(cmd, shell=True, cwd=toplevel_dir, stdout=PIPE, stderr=PIPE)
-    # must capture & reprint stdount, so that nosetests can capture it
+    # must capture & reprint stdount, so that pytest can capture it
     (stdout, stderr) = proc.communicate()
     sys.stdout.write(stdout.decode('utf-8'))
     sys.stderr.write(stderr.decode('utf-8'))


[allura] 07/14: [#8455] remove unused tox.ini

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 49ca10e197e73eda056b0d919a923e0f3ddfc790
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Nov 1 14:48:21 2022 +0000

    [#8455] remove unused tox.ini
---
 tox.ini | 32 --------------------------------
 1 file changed, 32 deletions(-)

diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 5eff60b89..000000000
--- a/tox.ini
+++ /dev/null
@@ -1,32 +0,0 @@
-;       Licensed to the Apache Software Foundation (ASF) under one
-;       or more contributor license agreements.  See the NOTICE file
-;       distributed with this work for additional information
-;       regarding copyright ownership.  The ASF licenses this file
-;       to you under the Apache License, Version 2.0 (the
-;       "License"); you may not use this file except in compliance
-;       with the License.  You may obtain a copy of the License at
-;
-;         http://www.apache.org/licenses/LICENSE-2.0
-;
-;       Unless required by applicable law or agreed to in writing,
-;       software distributed under the License is distributed on an
-;       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-;       KIND, either express or implied.  See the License for the
-;       specific language governing permissions and limitations
-;       under the License.
-
-# this is helpful for running individual nosetests like:
-#       tox -q allura.tests.functional.test_admin:TestInstallableTools.test_installable_tools_response
-# add -p2 if its safe to run in parallel (doesn't use filesystem path like a repo)
-# for full test suite runs, use ./run_tests within the virtualenv
-
-[tox]
-envlist = py{37}
-# since we don't have one top-level setup.py:
-skipsdist = True
-
-[testenv]
-deps = -rrequirements.txt
-# can comment out this line after venvs have been built the first time, for faster runs:
-commands_pre = ./rebuild-all.bash
-commands = nosetests {posargs}


[allura] 14/14: [#8455] change pytest from dev dep to full dep, add pytest-sugar dep

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 649503257335837f4e0b9677b146443569135967
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Mon Nov 7 20:35:44 2022 +0000

    [#8455] change pytest from dev dep to full dep, add pytest-sugar dep
---
 requirements-dev.txt | 2 --
 requirements.in      | 1 +
 requirements.txt     | 9 ++++++++-
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 71ace7eda..ec1568631 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -7,7 +7,5 @@ sphinx-argparse
 sphinx-rtd-theme
 sphinxcontrib-programoutput
 coverage
-pytest
-pytest-xdist
 pycodestyle
 pyflakes
diff --git a/requirements.in b/requirements.in
index 2fbf36b84..50e6726e7 100644
--- a/requirements.in
+++ b/requirements.in
@@ -56,6 +56,7 @@ testfixtures
 WebTest
 pytest
 pytest-xdist
+pytest-sugar
 
 # deployment
 gunicorn
diff --git a/requirements.txt b/requirements.txt
index 4d7392610..0d4820e57 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -102,7 +102,9 @@ oauthlib==3.2.2
     #   -r requirements.in
     #   requests-oauthlib
 packaging==21.3
-    # via pytest
+    # via
+    #   pytest
+    #   pytest-sugar
 paginate==0.5.6
     # via -r requirements.in
 paste==3.5.2
@@ -144,7 +146,10 @@ pysolr==3.9.0
 pytest==7.1.3
     # via
     #   -r requirements.in
+    #   pytest-sugar
     #   pytest-xdist
+pytest-sugar==0.9.5
+    # via -r requirements.in
 pytest-xdist==3.0.2
     # via -r requirements.in
 python-dateutil==2.8.2
@@ -198,6 +203,8 @@ smmap==5.0.0
     # via gitdb
 soupsieve==2.3.2.post1
     # via beautifulsoup4
+termcolor==2.1.0
+    # via pytest-sugar
 testfixtures==7.0.2
     # via -r requirements.in
 textile==4.0.2


[allura] 05/14: [#8445] pytest - remove alluratest.tools, dtadiff, nose asserts

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit bb53572e2a08f44c1291cf3d8015116f2d18d223
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Oct 25 13:58:59 2022 +0000

    [#8445] pytest - remove alluratest.tools, dtadiff, nose asserts
---
 Allura/allura/tests/decorators.py                  |  1 -
 Allura/allura/tests/functional/test_admin.py       |  4 +-
 Allura/allura/tests/functional/test_gravatar.py    |  1 -
 Allura/allura/tests/functional/test_home.py        |  1 -
 .../allura/tests/functional/test_neighborhood.py   |  1 -
 .../tests/functional/test_personal_dashboard.py    |  1 -
 Allura/allura/tests/functional/test_rest.py        |  1 -
 Allura/allura/tests/functional/test_root.py        |  4 +-
 Allura/allura/tests/functional/test_site_admin.py  |  2 -
 .../allura/tests/functional/test_trovecategory.py  |  1 -
 .../allura/tests/functional/test_user_profile.py   |  1 -
 Allura/allura/tests/model/test_filesystem.py       |  1 -
 Allura/allura/tests/model/test_neighborhood.py     |  1 -
 Allura/allura/tests/model/test_notification.py     |  1 -
 Allura/allura/tests/model/test_project.py          |  1 -
 Allura/allura/tests/model/test_repo.py             |  1 -
 Allura/allura/tests/model/test_timeline.py         |  2 -
 .../tests/scripts/test_create_sitemap_files.py     |  1 -
 .../allura/tests/scripts/test_delete_projects.py   |  1 -
 Allura/allura/tests/scripts/test_misc_scripts.py   |  1 -
 Allura/allura/tests/scripts/test_reindexes.py      |  1 -
 .../tests/templates/jinja_master/test_lib.py       |  4 +-
 Allura/allura/tests/test_app.py                    |  1 -
 Allura/allura/tests/test_commands.py               |  4 --
 Allura/allura/tests/test_decorators.py             |  1 -
 Allura/allura/tests/test_helpers.py                | 49 ++++++++++------------
 Allura/allura/tests/test_middlewares.py            |  2 -
 Allura/allura/tests/test_tasks.py                  |  5 ---
 Allura/allura/tests/test_webhooks.py               | 17 ++++----
 .../test_discussion_moderation_controller.py       |  1 -
 Allura/allura/tests/unit/phone/test_nexmo.py       |  2 -
 .../allura/tests/unit/phone/test_phone_service.py  |  3 --
 Allura/allura/tests/unit/spam/test_spam_filter.py  |  1 -
 .../allura/tests/unit/spam/test_stopforumspam.py   |  1 -
 Allura/allura/tests/unit/test_helpers/test_ago.py  |  1 -
 .../allura/tests/unit/test_ldap_auth_provider.py   |  3 +-
 Allura/allura/tests/unit/test_repo.py              | 20 ++++-----
 Allura/allura/tests/unit/test_solr.py              |  1 -
 .../forgeactivity/tests/functional/test_rest.py    |  2 -
 .../forgeactivity/tests/functional/test_root.py    |  1 -
 ForgeBlog/forgeblog/tests/functional/test_feeds.py |  1 -
 ForgeBlog/forgeblog/tests/functional/test_rest.py  |  1 -
 ForgeBlog/forgeblog/tests/functional/test_root.py  |  1 -
 ForgeBlog/forgeblog/tests/test_app.py              |  1 -
 ForgeBlog/forgeblog/tests/test_commands.py         |  2 -
 ForgeBlog/forgeblog/tests/unit/test_blog_post.py   |  1 -
 ForgeChat/forgechat/tests/functional/test_root.py  |  1 -
 .../tests/functional/test_import.py                |  5 ---
 ForgeDiscussion/forgediscussion/tests/test_app.py  |  1 -
 .../forgefeedback/tests/test_feedback_roles.py     |  4 +-
 .../forgefeedback/tests/unit/test_feedback.py      |  4 --
 .../tests/unit/test_root_controller.py             |  5 ---
 .../forgefiles/tests/functional/test_root.py       |  1 -
 ForgeFiles/forgefiles/tests/model/test_files.py    |  2 -
 ForgeFiles/forgefiles/tests/test_files_roles.py    |  1 -
 ForgeGit/forgegit/tests/functional/test_auth.py    |  1 -
 .../forgegit/tests/functional/test_controllers.py  |  6 +--
 ForgeGit/forgegit/tests/model/test_repository.py   |  2 -
 ForgeGit/forgegit/tests/test_git_app.py            |  1 -
 .../forgeimporters/github/tests/test_utils.py      |  2 -
 .../forgeimporters/github/tests/test_wiki.py       |  1 -
 .../tests/github/functional/test_github.py         |  1 -
 .../trac/tests/functional/test_trac.py             |  3 +-
 ForgeLink/forgelink/tests/functional/test_rest.py  |  1 -
 ForgeLink/forgelink/tests/functional/test_root.py  |  2 -
 ForgeLink/forgelink/tests/test_app.py              |  1 -
 ForgeSVN/forgesvn/tests/functional/test_auth.py    |  1 -
 .../forgesvn/tests/functional/test_controllers.py  |  3 --
 ForgeSVN/forgesvn/tests/model/test_repository.py   |  3 --
 .../forgesvn/tests/model/test_svnimplementation.py |  1 -
 ForgeSVN/forgesvn/tests/test_svn_app.py            |  1 -
 ForgeSVN/forgesvn/tests/test_tasks.py              |  1 -
 .../forgeshorturl/tests/functional/test_main.py    |  1 -
 .../tests/command/test_fix_discussion.py           |  1 -
 .../forgetracker/tests/functional/test_rest.py     |  2 -
 ForgeTracker/forgetracker/tests/test_app.py        |  2 -
 .../forgetracker/tests/unit/test_globals_model.py  |  1 -
 .../tests/unit/test_milestone_controller.py        |  1 -
 .../tests/unit/test_root_controller.py             |  1 -
 .../forgetracker/tests/unit/test_search.py         |  1 -
 ForgeWiki/forgewiki/tests/functional/test_rest.py  |  1 -
 ForgeWiki/forgewiki/tests/functional/test_root.py  |  1 -
 ForgeWiki/forgewiki/tests/test_app.py              |  1 -
 ForgeWiki/forgewiki/tests/test_wiki_roles.py       |  2 -
 requirements.in                                    |  1 -
 85 files changed, 46 insertions(+), 180 deletions(-)

diff --git a/Allura/allura/tests/decorators.py b/Allura/allura/tests/decorators.py
index 4e6711a4a..53aa5dff2 100644
--- a/Allura/allura/tests/decorators.py
+++ b/Allura/allura/tests/decorators.py
@@ -21,7 +21,6 @@ from functools import wraps
 import contextlib
 from six.moves.urllib.parse import parse_qs
 
-from alluratest.tools import assert_equal
 from ming.orm.ormsession import ThreadLocalORMSession
 from tg import tmpl_context as c
 from mock import patch
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index a140f1701..20fc7a554 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -23,18 +23,16 @@ import logging
 
 import tg
 import PIL
-from alluratest.tools import assert_equals, assert_in, assert_not_in, assert_is_not_none, assert_greater
 from ming.orm.ormsession import ThreadLocalORMSession
 from tg import expose
 from tg import tmpl_context as c, app_globals as g
 import mock
-import six
 
 import allura
 from allura.tests import TestController
 from alluratest.pytest_helpers import with_nose_compatibility
 from allura.tests import decorators as td
-from allura.tests.decorators import audits, out_audits
+from allura.tests.decorators import audits
 from alluratest.controller import TestRestApiBase, setup_trove_categories
 from allura import model as M
 from allura.app import SitemapEntry
diff --git a/Allura/allura/tests/functional/test_gravatar.py b/Allura/allura/tests/functional/test_gravatar.py
index 2886df357..b42f64a71 100644
--- a/Allura/allura/tests/functional/test_gravatar.py
+++ b/Allura/allura/tests/functional/test_gravatar.py
@@ -18,7 +18,6 @@
 from six.moves.urllib.parse import urlparse, parse_qs
 
 import tg
-from alluratest.tools import assert_equal
 from mock import patch
 
 from allura.tests import TestController
diff --git a/Allura/allura/tests/functional/test_home.py b/Allura/allura/tests/functional/test_home.py
index a27febbea..81c11a16a 100644
--- a/Allura/allura/tests/functional/test_home.py
+++ b/Allura/allura/tests/functional/test_home.py
@@ -20,7 +20,6 @@ import re
 import os
 
 from tg import tmpl_context as c
-from alluratest.tools import assert_equal, assert_not_in, assert_in
 from ming.orm import ThreadLocalORMSession
 
 import allura
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index b123cb7eb..7261e0ce0 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -25,7 +25,6 @@ import six.moves.urllib.error
 import PIL
 from mock import patch
 from tg import config
-from alluratest.tools import assert_equal, assert_in, assert_not_equal
 from ming.orm.ormsession import ThreadLocalORMSession, session
 from paste.httpexceptions import HTTPFound, HTTPMovedPermanently
 from tg import app_globals as g, tmpl_context as c
diff --git a/Allura/allura/tests/functional/test_personal_dashboard.py b/Allura/allura/tests/functional/test_personal_dashboard.py
index c1f7d3cb3..06c73dea7 100644
--- a/Allura/allura/tests/functional/test_personal_dashboard.py
+++ b/Allura/allura/tests/functional/test_personal_dashboard.py
@@ -20,7 +20,6 @@ import mock
 import tg
 
 from ming.orm import ThreadLocalORMSession, ThreadLocalODMSession
-from alluratest.tools import assert_equal, assert_in, assert_not_in
 from tg import tmpl_context as c
 
 from allura import model as M
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index c2a8cef8b..26d3b5620 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -22,7 +22,6 @@ import tg
 from bson import ObjectId
 from tg import app_globals as g
 import mock
-from alluratest.tools import assert_equal, assert_in, assert_not_in
 from ming.odm import ThreadLocalODMSession
 from tg import config
 
diff --git a/Allura/allura/tests/functional/test_root.py b/Allura/allura/tests/functional/test_root.py
index 633bdeb99..ff196261e 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -29,10 +29,8 @@ Please read http://pythonpaste.org/webtest/ for more information.
 import os
 from unittest import skipIf
 
-import six
-
 from tg import tmpl_context as c
-from alluratest.tools import assert_equal, module_not_available
+from alluratest.tools import module_not_available
 from ming.orm.ormsession import ThreadLocalORMSession
 import mock
 import tg
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index ec743bb1f..788fb4fed 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -20,7 +20,6 @@ import datetime as dt
 import bson
 
 from mock import patch, MagicMock
-from alluratest.tools import assert_equal, assert_in, assert_not_in
 from ming.odm import ThreadLocalORMSession
 from tg import tmpl_context as c
 from tg import config
@@ -32,7 +31,6 @@ from allura.tests import decorators as td
 from allura.lib import helpers as h
 from allura.lib.decorators import task
 from allura.lib.plugin import LocalAuthenticationProvider
-import six
 from alluratest.pytest_helpers import with_nose_compatibility
 
 
diff --git a/Allura/allura/tests/functional/test_trovecategory.py b/Allura/allura/tests/functional/test_trovecategory.py
index 76473d3d7..ae9057dd3 100644
--- a/Allura/allura/tests/functional/test_trovecategory.py
+++ b/Allura/allura/tests/functional/test_trovecategory.py
@@ -18,7 +18,6 @@ from bs4 import BeautifulSoup
 import mock
 
 from tg import config
-from alluratest.tools import assert_equals, assert_true, assert_in, assert_equal
 from ming.orm import session
 
 from allura import model as M
diff --git a/Allura/allura/tests/functional/test_user_profile.py b/Allura/allura/tests/functional/test_user_profile.py
index 353a99fa7..2a5e8674d 100644
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -17,7 +17,6 @@
 
 import mock
 import tg
-from alluratest.tools import assert_equal, assert_in, assert_not_in
 
 from alluratest.controller import TestRestApiBase
 from allura.model import Project, User
diff --git a/Allura/allura/tests/model/test_filesystem.py b/Allura/allura/tests/model/test_filesystem.py
index fe88d8aa7..5625df27d 100644
--- a/Allura/allura/tests/model/test_filesystem.py
+++ b/Allura/allura/tests/model/test_filesystem.py
@@ -22,7 +22,6 @@ from io import BytesIO
 import ming
 from tg import tmpl_context as c
 from ming.orm import session, Mapper
-from alluratest.tools import assert_equal
 from mock import patch
 from webob import Request, Response
 
diff --git a/Allura/allura/tests/model/test_neighborhood.py b/Allura/allura/tests/model/test_neighborhood.py
index 247370888..aaa6a963c 100644
--- a/Allura/allura/tests/model/test_neighborhood.py
+++ b/Allura/allura/tests/model/test_neighborhood.py
@@ -18,7 +18,6 @@
 """
 Model tests for neighborhood
 """
-from alluratest.tools import with_setup
 
 from allura import model as M
 from allura.tests import decorators as td
diff --git a/Allura/allura/tests/model/test_notification.py b/Allura/allura/tests/model/test_notification.py
index 33d2547fd..774c8dae7 100644
--- a/Allura/allura/tests/model/test_notification.py
+++ b/Allura/allura/tests/model/test_notification.py
@@ -20,7 +20,6 @@ from datetime import timedelta
 import collections
 
 from tg import tmpl_context as c, app_globals as g
-from alluratest.tools import assert_equal, assert_in
 from ming.orm import ThreadLocalORMSession
 import mock
 import bson
diff --git a/Allura/allura/tests/model/test_project.py b/Allura/allura/tests/model/test_project.py
index 6a1d44a8a..c4daf9474 100644
--- a/Allura/allura/tests/model/test_project.py
+++ b/Allura/allura/tests/model/test_project.py
@@ -18,7 +18,6 @@
 """
 Model tests for project
 """
-from alluratest.tools import with_setup, assert_equals, assert_in
 from tg import tmpl_context as c
 from ming.orm.ormsession import ThreadLocalORMSession
 from formencode import validators as fev
diff --git a/Allura/allura/tests/model/test_repo.py b/Allura/allura/tests/model/test_repo.py
index 84d26bdf2..b5fffdaa1 100644
--- a/Allura/allura/tests/model/test_repo.py
+++ b/Allura/allura/tests/model/test_repo.py
@@ -20,7 +20,6 @@ from collections import defaultdict, OrderedDict
 
 import unittest
 import mock
-from alluratest.tools import assert_equal
 from tg import tmpl_context as c
 from bson import ObjectId
 from ming.orm import session
diff --git a/Allura/allura/tests/model/test_timeline.py b/Allura/allura/tests/model/test_timeline.py
index 00e7ea069..f5af23bb1 100644
--- a/Allura/allura/tests/model/test_timeline.py
+++ b/Allura/allura/tests/model/test_timeline.py
@@ -15,8 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from alluratest.tools import assert_equal
-
 from allura import model as M
 from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, setup_global_objects
diff --git a/Allura/allura/tests/scripts/test_create_sitemap_files.py b/Allura/allura/tests/scripts/test_create_sitemap_files.py
index e9d40ec9d..906ff600b 100644
--- a/Allura/allura/tests/scripts/test_create_sitemap_files.py
+++ b/Allura/allura/tests/scripts/test_create_sitemap_files.py
@@ -20,7 +20,6 @@ from shutil import rmtree
 import xml.etree.ElementTree as ET
 
 from tg import tmpl_context as c
-from alluratest.tools import assert_in, assert_not_in
 from testfixtures import TempDirectory
 
 from alluratest.controller import setup_basic_test
diff --git a/Allura/allura/tests/scripts/test_delete_projects.py b/Allura/allura/tests/scripts/test_delete_projects.py
index d9414358f..12d6d69b5 100644
--- a/Allura/allura/tests/scripts/test_delete_projects.py
+++ b/Allura/allura/tests/scripts/test_delete_projects.py
@@ -18,7 +18,6 @@
 from ming.odm import session, Mapper, ThreadLocalODMSession
 from mock import patch
 from tg import app_globals as g
-from alluratest.tools import assert_equal
 
 from alluratest.controller import TestController
 from allura.tests.decorators import audits, out_audits, with_user_project
diff --git a/Allura/allura/tests/scripts/test_misc_scripts.py b/Allura/allura/tests/scripts/test_misc_scripts.py
index 83c8c145e..174bf9c52 100644
--- a/Allura/allura/tests/scripts/test_misc_scripts.py
+++ b/Allura/allura/tests/scripts/test_misc_scripts.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 from bson import ObjectId
-from alluratest.tools import assert_equal
 
 from allura.scripts.clear_old_notifications import ClearOldNotifications
 from alluratest.controller import setup_basic_test
diff --git a/Allura/allura/tests/scripts/test_reindexes.py b/Allura/allura/tests/scripts/test_reindexes.py
index c6da7ad62..e1f995078 100644
--- a/Allura/allura/tests/scripts/test_reindexes.py
+++ b/Allura/allura/tests/scripts/test_reindexes.py
@@ -14,7 +14,6 @@
 #       KIND, either express or implied.  See the License for the
 #       specific language governing permissions and limitations
 #       under the License.
-from alluratest.tools import assert_in, assert_equal
 from testfixtures import LogCapture
 
 from allura.scripts.reindex_projects import ReindexProjects
diff --git a/Allura/allura/tests/templates/jinja_master/test_lib.py b/Allura/allura/tests/templates/jinja_master/test_lib.py
index 8c46da06b..84108aaa5 100644
--- a/Allura/allura/tests/templates/jinja_master/test_lib.py
+++ b/Allura/allura/tests/templates/jinja_master/test_lib.py
@@ -17,10 +17,8 @@
 
 from tg import config, app_globals as g
 from mock import Mock
-from alluratest.tools import assert_equal
 
-import ming
-from allura.config.app_cfg import ForgeConfig, AlluraJinjaRenderer
+from allura.config.app_cfg import AlluraJinjaRenderer
 from alluratest.controller import setup_basic_test
 from alluratest.pytest_helpers import with_nose_compatibility
 
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index 8a6924ce2..997dd13f8 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -23,7 +23,6 @@ from formencode import validators as fev
 from textwrap import dedent
 
 from alluratest.controller import setup_unit_test
-from alluratest.tools import with_setup
 from allura import app
 from allura.lib.app_globals import Icon
 from allura.lib import mail_util
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index 18026cc5f..9e13f60cb 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -18,12 +18,8 @@
 
 import datetime
 
-import six
-from alluratest.tools import with_setup
 from testfixtures import OutputCapture
 
-from datadiff.tools import assert_equal
-
 from ming.base import Object
 from ming.orm import ThreadLocalORMSession
 from mock import Mock, call, patch
diff --git a/Allura/allura/tests/test_decorators.py b/Allura/allura/tests/test_decorators.py
index 71b70d019..2ef77cd8c 100644
--- a/Allura/allura/tests/test_decorators.py
+++ b/Allura/allura/tests/test_decorators.py
@@ -20,7 +20,6 @@ from mock import patch
 import random
 import gc
 
-from alluratest.tools import assert_equal, assert_not_equal
 from alluratest.pytest_helpers import with_nose_compatibility
 from allura.lib.decorators import task, memoize
 from alluratest.controller import setup_basic_test, setup_global_objects
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index 1e0229365..46703b142 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -23,8 +23,7 @@ import time
 import PIL
 from mock import Mock, patch
 from tg import tmpl_context as c
-from alluratest.tools import module_not_available, with_setup
-from datadiff import tools as dd
+from alluratest.tools import module_not_available
 from webob import Request
 from webob.exc import HTTPUnauthorized
 from ming.orm import ThreadLocalORMSession
@@ -392,22 +391,19 @@ M & Ms - doesn't get escaped
 http://blah.com/?x=y&a=b - not escaped either
 '''
 
-    dd.assert_equal(h.plain2markdown(text), expected)
+    assert h.plain2markdown(text) == expected
 
-    dd.assert_equal(
-        h.plain2markdown('a foo  bar\n\n    code here?',
-                         preserve_multiple_spaces=True),
-        'a foo&nbsp; bar\n\n&nbsp;&nbsp;&nbsp; code here?')
+    assert h.plain2markdown('a foo  bar\n\n    code here?',
+                            preserve_multiple_spaces=True) == \
+        'a foo&nbsp; bar\n\n&nbsp;&nbsp;&nbsp; code here?'
 
-    dd.assert_equal(
-        h.plain2markdown('\ttab before (stuff)',
-                         preserve_multiple_spaces=True),
-        r'&nbsp;&nbsp;&nbsp; tab before \(stuff\)')
+    assert h.plain2markdown('\ttab before (stuff)',
+                            preserve_multiple_spaces=True) == \
+        r'&nbsp;&nbsp;&nbsp; tab before \(stuff\)'
 
-    dd.assert_equal(
-        h.plain2markdown('\ttab before (stuff)',
-                         preserve_multiple_spaces=False),
-        r'tab before \(stuff\)')
+    assert h.plain2markdown('\ttab before (stuff)',
+                            preserve_multiple_spaces=False) == \
+        r'tab before \(stuff\)'
 
 
 @td.without_module('html2text')
@@ -443,22 +439,19 @@ http://blah\.com/?x=y&a=b \- not escaped either
 back\\\-slash escaped
 '''
 
-    dd.assert_equal(h.plain2markdown(text), expected)
+    assert h.plain2markdown(text) == expected
 
-    dd.assert_equal(
-        h.plain2markdown('a foo  bar\n\n    code here?',
-                         preserve_multiple_spaces=True),
-        'a foo&nbsp; bar\n\n&nbsp;&nbsp;&nbsp; code here?')
+    assert h.plain2markdown('a foo  bar\n\n    code here?',
+                            preserve_multiple_spaces=True) == \
+        'a foo&nbsp; bar\n\n&nbsp;&nbsp;&nbsp; code here?'
 
-    dd.assert_equal(
-        h.plain2markdown('\ttab before (stuff)',
-                         preserve_multiple_spaces=True),
-        r'&nbsp;&nbsp;&nbsp; tab before \(stuff\)')
+    assert h.plain2markdown('\ttab before (stuff)',
+                            preserve_multiple_spaces=True) == \
+        r'&nbsp;&nbsp;&nbsp; tab before \(stuff\)'
 
-    dd.assert_equal(
-        h.plain2markdown('\ttab before (stuff)',
-                         preserve_multiple_spaces=False),
-        r'tab before \(stuff\)')
+    assert h.plain2markdown('\ttab before (stuff)',
+                            preserve_multiple_spaces=False) == \
+        r'tab before \(stuff\)'
 
 
 @with_nose_compatibility
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index b48e27603..2435ba8d3 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -16,8 +16,6 @@
 #       under the License.
 
 from mock import MagicMock, patch
-from datadiff.tools import assert_equal
-from alluratest.tools import assert_not_equal
 from allura.lib.custom_middleware import CORSMiddleware
 from alluratest.pytest_helpers import with_nose_compatibility
 
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 2aeef658d..80fffbb8c 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -17,7 +17,6 @@
 
 import operator
 import shutil
-import sys
 from textwrap import dedent
 import unittest
 
@@ -30,8 +29,6 @@ import tg
 import mock
 from tg import tmpl_context as c, app_globals as g
 
-from datadiff.tools import assert_equal
-from nose.tools import assert_in, assert_less, assert_less_equal
 from ming.orm import FieldProperty, Mapper
 from ming.orm import ThreadLocalORMSession
 from testfixtures import LogCapture
@@ -41,8 +38,6 @@ from alluratest.controller import setup_basic_test, setup_global_objects, TestCo
 from allura import model as M
 from allura.command.taskd import TaskdCommand
 from allura.lib import helpers as h
-from allura.lib import search
-from allura.lib.exceptions import CompoundError
 from allura.lib.mail_util import MAX_MAIL_LINE_OCTETS
 from allura.tasks import event_tasks
 from allura.tasks import index_tasks
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index b2958d7d2..4494d9a6b 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -22,7 +22,6 @@ import datetime as dt
 
 from mock import Mock, MagicMock, patch, call
 import pytest
-from datadiff import tools as dd
 from formencode import Invalid
 from ming.odm import session
 from tg import tmpl_context as c
@@ -669,7 +668,7 @@ class TestModels(TestWebhookBase):
             'hook_url': 'http://httpbin.org/post',
             'mod_date': self.wh.mod_date,
         }
-        dd.assert_equal(self.wh.__json__(), expected)
+        assert self.wh.__json__() == expected
 
 
 @with_nose_compatibility
@@ -727,7 +726,7 @@ class TestWebhookRestController(TestRestApiBase):
             'webhooks': webhooks,
             'limits': {'repo-push': {'max': 3, 'used': 3}},
         }
-        dd.assert_equal(r.json, expected)
+        assert r.json == expected
 
     def test_webhook_GET_404(self):
         r = self.api_get(self.url + '/repo-push/invalid', status=404)
@@ -743,8 +742,8 @@ class TestWebhookRestController(TestRestApiBase):
             'hook_url': 'http://httpbin.org/post/0',
             'mod_date': str(webhook.mod_date),
         }
-        dd.assert_equal(r.status_int, 200)
-        dd.assert_equal(r.json, expected)
+        assert r.status_int == 200
+        assert r.json == expected
 
     def test_create_validation(self):
         assert M.Webhook.query.find().count() == len(self.webhooks)
@@ -787,7 +786,7 @@ class TestWebhookRestController(TestRestApiBase):
             'hook_url': data['url'],
             'mod_date': str(webhook.mod_date),
         }
-        dd.assert_equal(r.json, expected)
+        assert r.json == expected
         assert M.Webhook.query.find().count() == len(self.webhooks) + 1
 
     def test_create_duplicates(self):
@@ -847,7 +846,7 @@ class TestWebhookRestController(TestRestApiBase):
             'hook_url': data['url'],
             'mod_date': str(webhook.mod_date),
         }
-        dd.assert_equal(r.json, expected)
+        assert r.json == expected
 
         # change only secret
         data = {'secret': 'new-secret'}
@@ -867,7 +866,7 @@ class TestWebhookRestController(TestRestApiBase):
             'hook_url': 'http://hook.slack.com/abcd',
             'mod_date': str(webhook.mod_date),
         }
-        dd.assert_equal(r.json, expected)
+        assert r.json == expected
 
     def test_edit_duplicates(self):
         webhook = self.webhooks[0]
@@ -891,7 +890,7 @@ class TestWebhookRestController(TestRestApiBase):
             webhook.hook_url, self.git.config.url())
         with td.audits(msg):
             r = self.api_delete(url, status=200)
-        dd.assert_equal(r.json, {'result': 'ok'})
+        assert r.json == {'result': 'ok'}
         assert M.Webhook.query.find().count() == 2
         assert M.Webhook.query.get(_id=webhook._id) == None
 
diff --git a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
index 3ad4ed8df..133301f23 100644
--- a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
+++ b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
@@ -15,7 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from alluratest.tools import assert_equal
 from mock import Mock, patch
 from ming.orm import ThreadLocalORMSession, session
 
diff --git a/Allura/allura/tests/unit/phone/test_nexmo.py b/Allura/allura/tests/unit/phone/test_nexmo.py
index 290273946..1b6139fa8 100644
--- a/Allura/allura/tests/unit/phone/test_nexmo.py
+++ b/Allura/allura/tests/unit/phone/test_nexmo.py
@@ -17,8 +17,6 @@
 
 import json
 from mock import patch
-from datadiff.tools import assert_equal
-from alluratest.tools import assert_in, assert_not_in
 from alluratest.pytest_helpers import with_nose_compatibility
 
 from allura.lib.phone.nexmo import NexmoPhoneService
diff --git a/Allura/allura/tests/unit/phone/test_phone_service.py b/Allura/allura/tests/unit/phone/test_phone_service.py
index e4af19908..739594475 100644
--- a/Allura/allura/tests/unit/phone/test_phone_service.py
+++ b/Allura/allura/tests/unit/phone/test_phone_service.py
@@ -15,9 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from alluratest.tools import assert_true
-from datadiff.tools import assert_equal
-
 from allura.lib.phone import PhoneService
 from alluratest.pytest_helpers import with_nose_compatibility
 
diff --git a/Allura/allura/tests/unit/spam/test_spam_filter.py b/Allura/allura/tests/unit/spam/test_spam_filter.py
index f907b41ef..93b01b984 100644
--- a/Allura/allura/tests/unit/spam/test_spam_filter.py
+++ b/Allura/allura/tests/unit/spam/test_spam_filter.py
@@ -19,7 +19,6 @@ import mock
 import unittest
 
 from ming.odm import ThreadLocalORMSession
-from alluratest.tools import assert_equal
 
 from allura.lib.spam import SpamFilter, ChainedSpamFilter
 from allura import model as M
diff --git a/Allura/allura/tests/unit/spam/test_stopforumspam.py b/Allura/allura/tests/unit/spam/test_stopforumspam.py
index 7a1bd31d7..296f63a2c 100644
--- a/Allura/allura/tests/unit/spam/test_stopforumspam.py
+++ b/Allura/allura/tests/unit/spam/test_stopforumspam.py
@@ -19,7 +19,6 @@ import tempfile
 import mock
 
 from bson import ObjectId
-from alluratest.tools import assert_equal
 
 from allura.lib.spam.stopforumspamfilter import StopForumSpamSpamFilter
 from alluratest.pytest_helpers import with_nose_compatibility
diff --git a/Allura/allura/tests/unit/test_helpers/test_ago.py b/Allura/allura/tests/unit/test_helpers/test_ago.py
index a8e4c9044..ee4d0925f 100644
--- a/Allura/allura/tests/unit/test_helpers/test_ago.py
+++ b/Allura/allura/tests/unit/test_helpers/test_ago.py
@@ -18,7 +18,6 @@
 from datetime import datetime
 
 from mock import patch
-from alluratest.tools import assert_equal
 
 from allura.lib import helpers
 from alluratest.pytest_helpers import with_nose_compatibility
diff --git a/Allura/allura/tests/unit/test_ldap_auth_provider.py b/Allura/allura/tests/unit/test_ldap_auth_provider.py
index bc17ce9a5..c66da2b24 100644
--- a/Allura/allura/tests/unit/test_ldap_auth_provider.py
+++ b/Allura/allura/tests/unit/test_ldap_auth_provider.py
@@ -17,11 +17,10 @@
 
 import calendar
 import platform
-from datetime import datetime, timedelta
+from datetime import datetime
 
 from bson import ObjectId
 from mock import patch, Mock
-from alluratest.tools import assert_equal, assert_not_equal, assert_true
 from unittest import SkipTest
 from webob import Request
 from ming.orm.ormsession import ThreadLocalORMSession
diff --git a/Allura/allura/tests/unit/test_repo.py b/Allura/allura/tests/unit/test_repo.py
index a33d8c644..f67e69a96 100644
--- a/Allura/allura/tests/unit/test_repo.py
+++ b/Allura/allura/tests/unit/test_repo.py
@@ -20,8 +20,6 @@ import unittest
 
 import six
 from mock import patch, Mock, MagicMock, call
-from alluratest.tools import assert_equal
-from datadiff import tools as dd
 
 from tg import tmpl_context as c
 
@@ -320,8 +318,8 @@ class TestGroupCommits:
 
     def test_no_branches(self):
         b, t = _group_commits(self.repo, ['3', '2', '1'])
-        dd.assert_equal(b, {'__default__': ['3', '2', '1']})
-        dd.assert_equal(t, {})
+        assert b == {'__default__': ['3', '2', '1']}
+        assert t == {}
 
     def test_branches_and_tags(self):
         self.repo.symbolics_for_commit.side_effect = [
@@ -330,8 +328,8 @@ class TestGroupCommits:
             ([], []),
         ]
         b, t = _group_commits(self.repo, ['3', '2', '1'])
-        dd.assert_equal(b, {'master': ['3', '2', '1']})
-        dd.assert_equal(t, {'v1.1': ['3', '2', '1']})
+        assert b == {'master': ['3', '2', '1']}
+        assert t == {'v1.1': ['3', '2', '1']}
 
     def test_multiple_branches(self):
         self.repo.symbolics_for_commit.side_effect = [
@@ -340,8 +338,8 @@ class TestGroupCommits:
             (['test1', 'test2'], []),
         ]
         b, t = _group_commits(self.repo, ['3', '2', '1'])
-        dd.assert_equal(b, {'master': ['3', '2'],
-                            'test1': ['1'],
-                            'test2': ['1']})
-        dd.assert_equal(t, {'v1.1': ['3'],
-                            'v1.0': ['2', '1']})
+        assert b == {'master': ['3', '2'],
+                     'test1': ['1'],
+                     'test2': ['1']}
+        assert t == {'v1.1': ['3'],
+                     'v1.0': ['2', '1']}
diff --git a/Allura/allura/tests/unit/test_solr.py b/Allura/allura/tests/unit/test_solr.py
index fa23bc135..8d15e65ad 100644
--- a/Allura/allura/tests/unit/test_solr.py
+++ b/Allura/allura/tests/unit/test_solr.py
@@ -18,7 +18,6 @@
 import unittest
 
 import mock
-from datadiff.tools import assert_equal
 from markupsafe import Markup
 
 from allura.lib import helpers as h
diff --git a/ForgeActivity/forgeactivity/tests/functional/test_rest.py b/ForgeActivity/forgeactivity/tests/functional/test_rest.py
index 5ee7b6b0b..39db63a9c 100644
--- a/ForgeActivity/forgeactivity/tests/functional/test_rest.py
+++ b/ForgeActivity/forgeactivity/tests/functional/test_rest.py
@@ -15,8 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from datadiff.tools import assert_equal
-
 from tg import config
 from alluratest.controller import TestRestApiBase
 
diff --git a/ForgeActivity/forgeactivity/tests/functional/test_root.py b/ForgeActivity/forgeactivity/tests/functional/test_root.py
index 565576e05..5860ac7e2 100644
--- a/ForgeActivity/forgeactivity/tests/functional/test_root.py
+++ b/ForgeActivity/forgeactivity/tests/functional/test_root.py
@@ -21,7 +21,6 @@ from mock import patch
 from tg import config
 from bson import ObjectId
 import dateutil.parser
-from alluratest.tools import assert_equal
 from tg import app_globals as g
 from activitystream.storage.mingstorage import Activity
 from ming.odm import ThreadLocalODMSession
diff --git a/ForgeBlog/forgeblog/tests/functional/test_feeds.py b/ForgeBlog/forgeblog/tests/functional/test_feeds.py
index 530f8bdb4..dd68b0ea8 100644
--- a/ForgeBlog/forgeblog/tests/functional/test_feeds.py
+++ b/ForgeBlog/forgeblog/tests/functional/test_feeds.py
@@ -18,7 +18,6 @@
 
 import datetime
 
-from alluratest.tools import assert_in, assert_not_in
 from ming.orm.ormsession import ThreadLocalORMSession
 from tg import tmpl_context as c
 
diff --git a/ForgeBlog/forgeblog/tests/functional/test_rest.py b/ForgeBlog/forgeblog/tests/functional/test_rest.py
index 068a5e911..59c06d5ad 100644
--- a/ForgeBlog/forgeblog/tests/functional/test_rest.py
+++ b/ForgeBlog/forgeblog/tests/functional/test_rest.py
@@ -17,7 +17,6 @@
 from datetime import date
 
 import tg
-from alluratest.tools import assert_equal, assert_in
 
 from allura.lib import helpers as h
 from allura.tests import decorators as td
diff --git a/ForgeBlog/forgeblog/tests/functional/test_root.py b/ForgeBlog/forgeblog/tests/functional/test_root.py
index c0b18e42d..7e8bc735e 100644
--- a/ForgeBlog/forgeblog/tests/functional/test_root.py
+++ b/ForgeBlog/forgeblog/tests/functional/test_root.py
@@ -19,7 +19,6 @@ import datetime
 import json
 
 import tg
-from alluratest.tools import assert_equal, assert_in
 from mock import patch
 
 from allura.lib import helpers as h
diff --git a/ForgeBlog/forgeblog/tests/test_app.py b/ForgeBlog/forgeblog/tests/test_app.py
index c61b0a0e8..86ecba4e5 100644
--- a/ForgeBlog/forgeblog/tests/test_app.py
+++ b/ForgeBlog/forgeblog/tests/test_app.py
@@ -21,7 +21,6 @@ import os
 from cgi import FieldStorage
 from io import BytesIO
 
-from alluratest.tools import assert_equal
 from tg import tmpl_context as c
 from ming.orm import ThreadLocalORMSession
 
diff --git a/ForgeBlog/forgeblog/tests/test_commands.py b/ForgeBlog/forgeblog/tests/test_commands.py
index 3b82d6801..1728dbf3e 100644
--- a/ForgeBlog/forgeblog/tests/test_commands.py
+++ b/ForgeBlog/forgeblog/tests/test_commands.py
@@ -16,8 +16,6 @@
 #       under the License.
 
 from datetime import datetime, timedelta
-from tg import app_globals as g
-from datadiff.tools import assert_equal
 from unittest import skipIf
 import pkg_resources
 import mock
diff --git a/ForgeBlog/forgeblog/tests/unit/test_blog_post.py b/ForgeBlog/forgeblog/tests/unit/test_blog_post.py
index 56c5e2930..19559ff71 100644
--- a/ForgeBlog/forgeblog/tests/unit/test_blog_post.py
+++ b/ForgeBlog/forgeblog/tests/unit/test_blog_post.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 from datetime import datetime
-from alluratest.tools import assert_equal, assert_true
 from tg import tmpl_context as c
 
 from forgeblog import model as M
diff --git a/ForgeChat/forgechat/tests/functional/test_root.py b/ForgeChat/forgechat/tests/functional/test_root.py
index 024e3a460..5e7e73975 100644
--- a/ForgeChat/forgechat/tests/functional/test_root.py
+++ b/ForgeChat/forgechat/tests/functional/test_root.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import json
-from datadiff.tools import assert_equal
 
 from alluratest.controller import TestController
 from allura.tests.decorators import with_tool
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_import.py b/ForgeDiscussion/forgediscussion/tests/functional/test_import.py
index 1219d2a87..bf4cf6db8 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_import.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_import.py
@@ -17,14 +17,9 @@
 
 import os
 import json
-from datetime import datetime, timedelta
-from alluratest.tools import assert_equal
 
-import ming
 from tg import config
-from tg import tmpl_context as c
 
-from allura import model as M
 from allura.lib import helpers as h
 from alluratest.controller import TestRestApiBase
 
diff --git a/ForgeDiscussion/forgediscussion/tests/test_app.py b/ForgeDiscussion/forgediscussion/tests/test_app.py
index 0bf49aeca..434e4d2a8 100644
--- a/ForgeDiscussion/forgediscussion/tests/test_app.py
+++ b/ForgeDiscussion/forgediscussion/tests/test_app.py
@@ -23,7 +23,6 @@ from cgi import FieldStorage
 from io import BytesIO
 from alluratest.controller import setup_basic_test, setup_unit_test
 
-from alluratest.tools import assert_equal
 from tg import tmpl_context as c
 
 from forgediscussion.site_stats import posts_24hr
diff --git a/ForgeFeedback/forgefeedback/tests/test_feedback_roles.py b/ForgeFeedback/forgefeedback/tests/test_feedback_roles.py
index 64b626290..33faa8290 100644
--- a/ForgeFeedback/forgefeedback/tests/test_feedback_roles.py
+++ b/ForgeFeedback/forgefeedback/tests/test_feedback_roles.py
@@ -17,8 +17,6 @@
 
 from tg import tmpl_context as c, app_globals as g
 
-from alluratest.tools import assert_equal
-
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 from allura.lib import security
@@ -26,7 +24,7 @@ from allura.tests import decorators as td
 from allura.lib import helpers as h
 
 
-def setup_module(module):
+def setup_module():
     setup_basic_test()
     setup_with_tools()
 
diff --git a/ForgeFeedback/forgefeedback/tests/unit/test_feedback.py b/ForgeFeedback/forgefeedback/tests/unit/test_feedback.py
index a5863d3d4..dd885dfac 100644
--- a/ForgeFeedback/forgefeedback/tests/unit/test_feedback.py
+++ b/ForgeFeedback/forgefeedback/tests/unit/test_feedback.py
@@ -15,10 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from datetime import datetime
-from alluratest.tools import assert_equal, assert_true
-from tg import tmpl_context as c
-
 from forgefeedback.tests.unit import FeedbackTestWithModel
 from forgefeedback import model as M
 
diff --git a/ForgeFeedback/forgefeedback/tests/unit/test_root_controller.py b/ForgeFeedback/forgefeedback/tests/unit/test_root_controller.py
index f6b2b0ca8..bd86be46f 100644
--- a/ForgeFeedback/forgefeedback/tests/unit/test_root_controller.py
+++ b/ForgeFeedback/forgefeedback/tests/unit/test_root_controller.py
@@ -15,13 +15,8 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-import unittest
-
-from mock import Mock, patch
 from ming.orm.ormsession import session
 from tg import tmpl_context as c
-from tg import request
-from alluratest.tools import assert_equal
 
 from allura.lib import helpers as h
 from allura.model import User
diff --git a/ForgeFiles/forgefiles/tests/functional/test_root.py b/ForgeFiles/forgefiles/tests/functional/test_root.py
index 5c44760f1..75acf6fa2 100644
--- a/ForgeFiles/forgefiles/tests/functional/test_root.py
+++ b/ForgeFiles/forgefiles/tests/functional/test_root.py
@@ -15,7 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 from tg import tmpl_context as c
-from nose.tools import assert_true,assert_not_equal,assert_equals
 
 from allura import model as M
 from alluratest.controller import TestController
diff --git a/ForgeFiles/forgefiles/tests/model/test_files.py b/ForgeFiles/forgefiles/tests/model/test_files.py
index 38929e68e..d66415896 100644
--- a/ForgeFiles/forgefiles/tests/model/test_files.py
+++ b/ForgeFiles/forgefiles/tests/model/test_files.py
@@ -17,8 +17,6 @@
 
 '''This module is added for testing the files model '''
 
-from nose.tools import assert_equal, assert_true, assert_false
-
 from forgefiles.tests.model import FilesTestWithModel
 from forgefiles.model.files import UploadFolder
 from forgefiles.model.files import UploadFiles
diff --git a/ForgeFiles/forgefiles/tests/test_files_roles.py b/ForgeFiles/forgefiles/tests/test_files_roles.py
index 6df1ae701..1bce3c1fb 100644
--- a/ForgeFiles/forgefiles/tests/test_files_roles.py
+++ b/ForgeFiles/forgefiles/tests/test_files_roles.py
@@ -15,7 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from nose.tools import assert_equal
 from tg import tmpl_context as c, app_globals as g
 
 from alluratest.controller import setup_basic_test, setup_global_objects
diff --git a/ForgeGit/forgegit/tests/functional/test_auth.py b/ForgeGit/forgegit/tests/functional/test_auth.py
index d0bacfa6d..80955e077 100644
--- a/ForgeGit/forgegit/tests/functional/test_auth.py
+++ b/ForgeGit/forgegit/tests/functional/test_auth.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import json
-from datadiff.tools import assert_equal
 
 from allura.tests import TestController
 from allura.tests.decorators import with_tool
diff --git a/ForgeGit/forgegit/tests/functional/test_controllers.py b/ForgeGit/forgegit/tests/functional/test_controllers.py
index c681d5bbd..a906bea91 100644
--- a/ForgeGit/forgegit/tests/functional/test_controllers.py
+++ b/ForgeGit/forgegit/tests/functional/test_controllers.py
@@ -22,8 +22,6 @@ import shutil
 import tempfile
 import textwrap
 
-from datadiff.tools import assert_equal as dd_assert_equal
-from alluratest.tools import assert_equal, assert_in, assert_not_in, assert_not_equal
 import pkg_resources
 from alluratest.tools import assert_regexp_matches
 from tg import tmpl_context as c
@@ -892,7 +890,7 @@ class TestFork(_TestCase):
         assert '<p>changed description</p' in r
         assert 'Merge Request #1: changed summary (open)' in r
         changes = r.html.findAll('div', attrs={'class': 'markdown_content'})[-1]
-        dd_assert_equal(str(changes), """
+        assert str(changes) == """
 <div class="markdown_content"><ul>
 <li>
 <p><strong>Summary</strong>: summary --&gt; changed summary</p>
@@ -912,7 +910,7 @@ class TestFork(_TestCase):
 <span class="gi">+changed description</span><span class="w"></span>
 </code></pre></div>
 </div>
-""".strip())
+""".strip()
 
         r = self.app.get('/p/test/src-git/merge-requests').follow()
         assert '<a href="1/" rel="nofollow">changed summary</a>' in r
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index 7d29d7451..8011ef145 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -29,9 +29,7 @@ from tg import tmpl_context as c, app_globals as g
 import tg
 from ming.base import Object
 from ming.orm import ThreadLocalORMSession, session
-from alluratest.tools import assert_equal, assert_in, assert_less
 from testfixtures import TempDirectory
-from datadiff.tools import assert_equals
 
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura.lib import helpers as h
diff --git a/ForgeGit/forgegit/tests/test_git_app.py b/ForgeGit/forgegit/tests/test_git_app.py
index c4868448a..59c86012b 100644
--- a/ForgeGit/forgegit/tests/test_git_app.py
+++ b/ForgeGit/forgegit/tests/test_git_app.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import unittest
-from alluratest.tools import assert_equals
 
 from tg import tmpl_context as c
 from ming.orm import ThreadLocalORMSession
diff --git a/ForgeImporters/forgeimporters/github/tests/test_utils.py b/ForgeImporters/forgeimporters/github/tests/test_utils.py
index 5f067e8bc..f8c45cd02 100644
--- a/ForgeImporters/forgeimporters/github/tests/test_utils.py
+++ b/ForgeImporters/forgeimporters/github/tests/test_utils.py
@@ -15,8 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from alluratest.tools import assert_equal
-
 from forgeimporters.github.utils import GitHubMarkdownConverter
 
 
diff --git a/ForgeImporters/forgeimporters/github/tests/test_wiki.py b/ForgeImporters/forgeimporters/github/tests/test_wiki.py
index 20f01002b..1ddd56d6c 100644
--- a/ForgeImporters/forgeimporters/github/tests/test_wiki.py
+++ b/ForgeImporters/forgeimporters/github/tests/test_wiki.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 from unittest import TestCase, skipIf
-from alluratest.tools import assert_equal
 from mock import Mock, patch, call
 from ming.odm import ThreadLocalORMSession
 import git
diff --git a/ForgeImporters/forgeimporters/tests/github/functional/test_github.py b/ForgeImporters/forgeimporters/tests/github/functional/test_github.py
index 7945bb209..90f605522 100644
--- a/ForgeImporters/forgeimporters/tests/github/functional/test_github.py
+++ b/ForgeImporters/forgeimporters/tests/github/functional/test_github.py
@@ -17,7 +17,6 @@
 import requests
 import tg
 from mock import patch, call, Mock
-from alluratest.tools import assert_equal
 from unittest import TestCase
 
 from allura.tests import TestController
diff --git a/ForgeImporters/forgeimporters/trac/tests/functional/test_trac.py b/ForgeImporters/forgeimporters/trac/tests/functional/test_trac.py
index f31d5a96c..c12823fbf 100644
--- a/ForgeImporters/forgeimporters/trac/tests/functional/test_trac.py
+++ b/ForgeImporters/forgeimporters/trac/tests/functional/test_trac.py
@@ -15,8 +15,7 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from mock import patch, Mock
-from alluratest.tools import assert_equal
+from mock import patch
 from tg import config
 
 from allura.lib import helpers as h
diff --git a/ForgeLink/forgelink/tests/functional/test_rest.py b/ForgeLink/forgelink/tests/functional/test_rest.py
index 6b3e026d9..2be487255 100644
--- a/ForgeLink/forgelink/tests/functional/test_rest.py
+++ b/ForgeLink/forgelink/tests/functional/test_rest.py
@@ -15,7 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from alluratest.tools import assert_equal
 from allura.tests import decorators as td
 from alluratest.controller import TestRestApiBase
 from allura import model as M
diff --git a/ForgeLink/forgelink/tests/functional/test_root.py b/ForgeLink/forgelink/tests/functional/test_root.py
index 595c2aa46..ac2d716cb 100644
--- a/ForgeLink/forgelink/tests/functional/test_root.py
+++ b/ForgeLink/forgelink/tests/functional/test_root.py
@@ -17,8 +17,6 @@
 
 import json
 
-from alluratest.tools import assert_equal, assert_in
-
 from allura import model as M
 from allura.lib import helpers as h
 from allura.tests import decorators as td
diff --git a/ForgeLink/forgelink/tests/test_app.py b/ForgeLink/forgelink/tests/test_app.py
index d6e2fb4a6..a94901bd9 100644
--- a/ForgeLink/forgelink/tests/test_app.py
+++ b/ForgeLink/forgelink/tests/test_app.py
@@ -18,7 +18,6 @@
 import tempfile
 import json
 
-from alluratest.tools import assert_equal
 from tg import tmpl_context as c
 
 from allura.tests import decorators as td
diff --git a/ForgeSVN/forgesvn/tests/functional/test_auth.py b/ForgeSVN/forgesvn/tests/functional/test_auth.py
index 6ae98a74f..7a64dc229 100644
--- a/ForgeSVN/forgesvn/tests/functional/test_auth.py
+++ b/ForgeSVN/forgesvn/tests/functional/test_auth.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import json
-from datadiff.tools import assert_equal
 
 from allura.tests import TestController
 from forgesvn.tests import with_svn
diff --git a/ForgeSVN/forgesvn/tests/functional/test_controllers.py b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
index 27b58e047..99cc242ea 100644
--- a/ForgeSVN/forgesvn/tests/functional/test_controllers.py
+++ b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
@@ -16,18 +16,15 @@
 #       under the License.
 
 import json
-import re
 import shutil
 import os
 from unittest import skipUnless
 
-import six
 import tg
 import pkg_resources
 from tg import tmpl_context as c
 from ming.orm import ThreadLocalORMSession
 from mock import patch
-from alluratest.tools import assert_equal, assert_in
 
 from allura import model as M
 from allura.lib import helpers as h
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 885aaaff1..707d7b52a 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -29,8 +29,6 @@ from collections import defaultdict
 
 from tg import tmpl_context as c, app_globals as g
 import mock
-from alluratest.tools import assert_equal, assert_in
-from datadiff.tools import assert_equals
 import tg
 import ming
 from ming.base import Object
@@ -48,7 +46,6 @@ from forgesvn import model as SM
 from forgesvn.model.svn import svn_path_exists
 from forgesvn.tests import with_svn
 from allura.tests.decorators import with_tool
-import six
 
 
 class TestNewRepo(unittest.TestCase):
diff --git a/ForgeSVN/forgesvn/tests/model/test_svnimplementation.py b/ForgeSVN/forgesvn/tests/model/test_svnimplementation.py
index 0313e822b..bb60f1324 100644
--- a/ForgeSVN/forgesvn/tests/model/test_svnimplementation.py
+++ b/ForgeSVN/forgesvn/tests/model/test_svnimplementation.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 from mock import Mock, patch
-from alluratest.tools import assert_equal
 from tg import app_globals as g
 
 from alluratest.controller import setup_unit_test
diff --git a/ForgeSVN/forgesvn/tests/test_svn_app.py b/ForgeSVN/forgesvn/tests/test_svn_app.py
index d5c8770c1..80d3357bc 100644
--- a/ForgeSVN/forgesvn/tests/test_svn_app.py
+++ b/ForgeSVN/forgesvn/tests/test_svn_app.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import unittest
-from alluratest.tools import assert_equals
 
 from tg import tmpl_context as c
 from ming.orm import ThreadLocalORMSession
diff --git a/ForgeSVN/forgesvn/tests/test_tasks.py b/ForgeSVN/forgesvn/tests/test_tasks.py
index b3e388e98..b4d939eaa 100644
--- a/ForgeSVN/forgesvn/tests/test_tasks.py
+++ b/ForgeSVN/forgesvn/tests/test_tasks.py
@@ -23,7 +23,6 @@ import tg
 import mock
 from tg import tmpl_context as c
 from paste.deploy.converters import asbool
-from alluratest.tools import assert_equal
 
 from alluratest.controller import setup_basic_test
 
diff --git a/ForgeShortUrl/forgeshorturl/tests/functional/test_main.py b/ForgeShortUrl/forgeshorturl/tests/functional/test_main.py
index 1d3471d06..4aa193815 100644
--- a/ForgeShortUrl/forgeshorturl/tests/functional/test_main.py
+++ b/ForgeShortUrl/forgeshorturl/tests/functional/test_main.py
@@ -17,7 +17,6 @@
 
 from tg import tmpl_context as c
 from tg import config
-from alluratest.tools import assert_equal
 import mock
 
 from allura.lib import helpers as h
diff --git a/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py b/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py
index 1f3479d20..5d51ad120 100644
--- a/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py
+++ b/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 from ming.orm import session
-from alluratest.tools import assert_equal, assert_not_equal
 import pkg_resources
 
 from alluratest.controller import setup_basic_test, setup_global_objects
diff --git a/ForgeTracker/forgetracker/tests/functional/test_rest.py b/ForgeTracker/forgetracker/tests/functional/test_rest.py
index 7db63dfa2..6dfa4c9eb 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_rest.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_rest.py
@@ -17,8 +17,6 @@
 
 from tg import tmpl_context as c
 
-from datadiff.tools import assert_equal
-from alluratest.tools import assert_not_equal
 from mock import patch
 from tg import config
 
diff --git a/ForgeTracker/forgetracker/tests/test_app.py b/ForgeTracker/forgetracker/tests/test_app.py
index a688e8d01..f5542b602 100644
--- a/ForgeTracker/forgetracker/tests/test_app.py
+++ b/ForgeTracker/forgetracker/tests/test_app.py
@@ -21,7 +21,6 @@ import operator
 import os
 from io import BytesIO
 
-from alluratest.tools import assert_equal, assert_true
 from tg import tmpl_context as c
 from cgi import FieldStorage
 
@@ -33,7 +32,6 @@ from allura.tests import decorators as td
 from forgetracker import model as TM
 from forgetracker.site_stats import tickets_stats_24hr
 from forgetracker.tests.functional.test_root import TrackerTestController
-from alluratest.pytest_helpers import with_nose_compatibility
 
 
 class TestApp:
diff --git a/ForgeTracker/forgetracker/tests/unit/test_globals_model.py b/ForgeTracker/forgetracker/tests/unit/test_globals_model.py
index bb22df47a..2e7c591a3 100644
--- a/ForgeTracker/forgetracker/tests/unit/test_globals_model.py
+++ b/ForgeTracker/forgetracker/tests/unit/test_globals_model.py
@@ -18,7 +18,6 @@
 from datetime import datetime, timedelta
 
 import mock
-from alluratest.tools import assert_equal
 from tg import tmpl_context as c
 from ming.orm.ormsession import ThreadLocalORMSession
 
diff --git a/ForgeTracker/forgetracker/tests/unit/test_milestone_controller.py b/ForgeTracker/forgetracker/tests/unit/test_milestone_controller.py
index 3bc0edd50..9634eb62d 100644
--- a/ForgeTracker/forgetracker/tests/unit/test_milestone_controller.py
+++ b/ForgeTracker/forgetracker/tests/unit/test_milestone_controller.py
@@ -17,7 +17,6 @@
 
 
 from mock import Mock
-from alluratest.tools import assert_equal
 
 from allura.lib import helpers as h
 from tg import tmpl_context as c
diff --git a/ForgeTracker/forgetracker/tests/unit/test_root_controller.py b/ForgeTracker/forgetracker/tests/unit/test_root_controller.py
index fd40593bc..ac0157381 100644
--- a/ForgeTracker/forgetracker/tests/unit/test_root_controller.py
+++ b/ForgeTracker/forgetracker/tests/unit/test_root_controller.py
@@ -20,7 +20,6 @@ import unittest
 from mock import Mock, patch
 from ming.orm.ormsession import session
 from tg import tmpl_context as c
-from alluratest.tools import assert_equal
 
 from allura.lib import helpers as h
 from allura.model import User
diff --git a/ForgeTracker/forgetracker/tests/unit/test_search.py b/ForgeTracker/forgetracker/tests/unit/test_search.py
index c9bb4bdbf..4c3548b2c 100644
--- a/ForgeTracker/forgetracker/tests/unit/test_search.py
+++ b/ForgeTracker/forgetracker/tests/unit/test_search.py
@@ -16,7 +16,6 @@
 #       under the License.
 
 import mock
-from alluratest.tools import assert_equal
 from forgetracker.search import get_facets, query_filter_choices
 
 
diff --git a/ForgeWiki/forgewiki/tests/functional/test_rest.py b/ForgeWiki/forgewiki/tests/functional/test_rest.py
index 4eb87d999..0703f7e39 100644
--- a/ForgeWiki/forgewiki/tests/functional/test_rest.py
+++ b/ForgeWiki/forgewiki/tests/functional/test_rest.py
@@ -17,7 +17,6 @@
 
 import json
 
-from alluratest.tools import assert_equal, assert_in, assert_not_equal
 import tg
 
 from allura.lib import helpers as h
diff --git a/ForgeWiki/forgewiki/tests/functional/test_root.py b/ForgeWiki/forgewiki/tests/functional/test_root.py
index 927f17dc4..8a5e5eee2 100644
--- a/ForgeWiki/forgewiki/tests/functional/test_root.py
+++ b/ForgeWiki/forgewiki/tests/functional/test_root.py
@@ -21,7 +21,6 @@ import allura
 import json
 
 import PIL
-from alluratest.tools import assert_true, assert_equal, assert_in, assert_not_equal, assert_not_in
 from ming.orm.ormsession import ThreadLocalORMSession
 from mock import patch
 from tg import config
diff --git a/ForgeWiki/forgewiki/tests/test_app.py b/ForgeWiki/forgewiki/tests/test_app.py
index 482288dc5..ca01c8291 100644
--- a/ForgeWiki/forgewiki/tests/test_app.py
+++ b/ForgeWiki/forgewiki/tests/test_app.py
@@ -22,7 +22,6 @@ import operator
 import os
 from io import BytesIO
 
-from alluratest.tools import assert_equal
 from tg import tmpl_context as c
 from ming.orm import ThreadLocalORMSession
 
diff --git a/ForgeWiki/forgewiki/tests/test_wiki_roles.py b/ForgeWiki/forgewiki/tests/test_wiki_roles.py
index f42f76618..56a964ddc 100644
--- a/ForgeWiki/forgewiki/tests/test_wiki_roles.py
+++ b/ForgeWiki/forgewiki/tests/test_wiki_roles.py
@@ -17,8 +17,6 @@
 
 from tg import tmpl_context as c, app_globals as g
 
-from alluratest.tools import assert_equal
-
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 from allura.lib import security
diff --git a/requirements.in b/requirements.in
index 57c4c5328..0226b62f9 100644
--- a/requirements.in
+++ b/requirements.in
@@ -49,7 +49,6 @@ werkzeug
 wrapt
 importlib-metadata<5.0
 # testing
-datadiff
 mock
 pyflakes
 #pylint -- disabled due to [#8346]  (also requires diff versions on py2 vs 3, including transitive deps which gets tricky with pip-compile)


[allura] 12/14: [#8455] remove 'test_suite' and 'tests_require' from setup.py as they are deprecated

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit ebd0d3b345cde629e0f246dd0c732d198097e754
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Nov 1 16:27:42 2022 +0000

    [#8455] remove 'test_suite' and 'tests_require' from setup.py as they are deprecated
---
 Allura/setup.py          | 2 --
 ForgeDiscussion/setup.py | 2 --
 2 files changed, 4 deletions(-)

diff --git a/Allura/setup.py b/Allura/setup.py
index cb89a44ff..aa5f79113 100644
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -50,8 +50,6 @@ setup(
     paster_plugins=['PasteScript', 'TurboGears2', 'Ming'],
     packages=find_packages(exclude=['ez_setup']),
     include_package_data=True,
-    test_suite='nose.collector',
-    tests_require=['WebTest >= 1.2', 'BeautifulSoup', 'nose'],
     package_data={'allura': ['i18n/*/LC_MESSAGES/*.mo',
                              'templates/**.html',
                              'templates/**.py',
diff --git a/ForgeDiscussion/setup.py b/ForgeDiscussion/setup.py
index 165ce4b2f..a8993785d 100644
--- a/ForgeDiscussion/setup.py
+++ b/ForgeDiscussion/setup.py
@@ -38,8 +38,6 @@ setup(name='ForgeDiscussion',
           # -*- Extra requirements: -*-
           'Allura',
       ],
-      test_suite='nose.collector',
-      tests_require=['WebTest', 'BeautifulSoup'],
       entry_points="""
       # -*- Entry points: -*-
       [allura]


[allura] 10/14: [#8455] updated test docs, removed various old references to nose and replaced with pytest

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 0cc46ae4d2ba345493eced86d10d8bdc26bdc5cf
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Nov 1 15:30:08 2022 +0000

    [#8455] updated test docs, removed various old references to nose and replaced with pytest
---
 .travis.yml                                                  |  1 -
 Allura/allura/tests/test_commands.py                         |  1 -
 Allura/allura/tests/test_helpers.py                          |  1 -
 Allura/docs/development/contributing.rst                     |  9 +++------
 Allura/docs/getting_started/installation.rst                 |  5 ++---
 AlluraTest/alluratest/test_syntax.py                         |  2 +-
 CHANGES                                                      | 12 ++++++++++++
 .../forgetracker/tests/command/test_fix_discussion.py        |  1 -
 README.markdown                                              |  2 +-
 rat-excludes.txt                                             |  1 +
 10 files changed, 20 insertions(+), 15 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index e8511f751..14a74ff89 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,7 +27,6 @@ jobs:
 install:
   - sudo apt-get install -qq libjpeg8-dev zlib1g-dev
   - pip install --upgrade setuptools pip
-  - pip install nose
   - pip install -r requirements.txt --no-deps --upgrade --upgrade-strategy=only-if-needed
   - npm ci
 script:
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index fadd8ad09..db9ae0296 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -44,7 +44,6 @@ class EmptyClass:
 
 
 def setup_module():
-    """Method called by nose before running each test"""
     setup_basic_test()
     setup_global_objects()
     setup_unit_test()
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index 571d33fea..21926356c 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -42,7 +42,6 @@ import six
 
 
 def setup_module():
-    """Method called by nose before running each test"""
     setup_basic_test()
 
 
diff --git a/Allura/docs/development/contributing.rst b/Allura/docs/development/contributing.rst
index bd9fcbf87..29ac70776 100644
--- a/Allura/docs/development/contributing.rst
+++ b/Allura/docs/development/contributing.rst
@@ -206,16 +206,13 @@ as ``pudb`` are also available.
 
 Testing
 -------
-First, install :code:`nose` (not bundled installed by default, since it is LGPL and deprecated)
-:code:`docker-compose run web pip install nose`
-
 To run all the tests, execute ``./run_tests`` in the repo root. To run tests
 for a single package, for example ``forgetracker``::
 
-  cd ForgeTracker && nosetests
+  cd ForgeTracker && pytest
 
-To learn more about the ``nose`` test runner, consult the `documentation
-<http://nose.readthedocs.org/en/latest/>`_.
+To learn more about the ``pytest`` test runner, consult the `documentation
+<https://docs.pytest.org/en/latest/contents.html>`_.
 
 When writing code for Allura, don't forget that you'll need to also create
 tests that cover behaviour that you've added or changed. You may find this
diff --git a/Allura/docs/getting_started/installation.rst b/Allura/docs/getting_started/installation.rst
index a89d2dbe2..c519c42cb 100644
--- a/Allura/docs/getting_started/installation.rst
+++ b/Allura/docs/getting_started/installation.rst
@@ -174,8 +174,7 @@ Update requirements and reinstall apps:
 You may want to restart at least "taskd" container after that in order for it to
 pick up changes.  Run :code:`docker-compose restart taskd`
 
-Running all tests.  First, install :code:`nose` (not bundled installed by default, since it is LGPL and deprecated)
-:code:`docker-compose run web pip install nose` then:
+Run all tests:
 
 .. code-block:: bash
 
@@ -185,7 +184,7 @@ Running subset of tests:
 
 .. code-block:: bash
 
-    docker-compose run web bash -c 'cd ForgeGit && nosetests forgegit.tests.functional.test_controllers:TestFork'
+    docker-compose run web bash -c 'cd ForgeGit && pytest forgegit/tests/functional/test_controllers.py::TestFork'
 
 Connecting to mongo using a container:
 
diff --git a/AlluraTest/alluratest/test_syntax.py b/AlluraTest/alluratest/test_syntax.py
index 0d93f4c0d..bd2ea0701 100644
--- a/AlluraTest/alluratest/test_syntax.py
+++ b/AlluraTest/alluratest/test_syntax.py
@@ -27,7 +27,7 @@ toplevel_dir = os.path.abspath(os.path.dirname(__file__) + "/../..")
 
 def run(cmd):
     proc = Popen(cmd, shell=True, cwd=toplevel_dir, stdout=PIPE, stderr=PIPE)
-    # must capture & reprint stdount, so that pytest can capture it
+    # must capture & reprint stdout, so that pytest can capture it
     (stdout, stderr) = proc.communicate()
     sys.stdout.write(stdout.decode('utf-8'))
     sys.stderr.write(stderr.decode('utf-8'))
diff --git a/CHANGES b/CHANGES
index bc2e43ded..672c16e40 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,15 @@
+Next Version
+
+Upgrade Instructions
+
+  More to come!
+
+General
+ * [#8445] Switched all tests from nose to pytest
+
+
+
+
 Version 1.14.0  (September 2022)
 
 Upgrade Instructions
diff --git a/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py b/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py
index 5d51ad120..e31609b77 100644
--- a/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py
+++ b/ForgeTracker/forgetracker/tests/command/test_fix_discussion.py
@@ -30,7 +30,6 @@ test_config = pkg_resources.resource_filename(
 
 
 def setup_module(self):
-    """Method called by nose before running each test"""
     setup_basic_test()
     setup_global_objects()
 
diff --git a/README.markdown b/README.markdown
index b0bbc4b2e..4c1fdef30 100644
--- a/README.markdown
+++ b/README.markdown
@@ -25,7 +25,7 @@
 
 Allura is an open source implementation of a software "forge", a web site that manages source code repositories, bug reports, discussions, mailing lists, wiki pages, blogs and more for any number of individual projects.
 
-Allura is written in Python and leverages a great many existing Python packages (see requirements.txt and friends).  It comes with tests which we run with [nose](https://nose.readthedocs.org/en/latest/).  It is extensible in several ways, most importantly via the notion of "tools" based on `allura.app.Application`; but also with [themes, authentication, and various other pluggable-APIs](https://forge-allura.apache.org/docs/extending.html).
+Allura is written in Python and leverages a great many existing Python packages (see requirements.txt and friends).  It comes with tests which we run with [pytest](https://docs.pytest.org/en/latest/contents.html).  It is extensible in several ways, most importantly via the notion of "tools" based on `allura.app.Application`; but also with [themes, authentication, and various other pluggable-APIs](https://forge-allura.apache.org/docs/extending.html).
 
 Website: <https://allura.apache.org/>
 
diff --git a/rat-excludes.txt b/rat-excludes.txt
index c33bf1001..7c702e624 100644
--- a/rat-excludes.txt
+++ b/rat-excludes.txt
@@ -7,6 +7,7 @@
 **/.pytest_cache/
 **/MANIFEST.in
 **/nosetests.xml
+**/pytest.junit.xml
 **/setup.cfg
 .eslintignore-es5
 .eslintignore-es6


[allura] 01/14: [#8455] added pytest.ini

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 8f2e1003f150c58cc28e1bad962f186d07be4f8d
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Fri Sep 23 17:21:15 2022 +0000

    [#8455] added pytest.ini
---
 pytest.ini | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 000000000..92478cd93
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,17 @@
+[pytest]
+# see python -W and pytest filterwarnings
+# https://docs.python.org/3/using/cmdline.html#cmdoption-w
+# https://docs.pytest.org/en/6.2.x/reference.html#ini-options-ref
+filterwarnings =
+    ignore::DeprecationWarning
+
+addopts = --pyargs -p no:flaky
+
+# our patterns are listed first:  then ".*" and following are defaults from https://github.com/pytest-dev/pytest/blob/main/src/_pytest/main.py#L52
+norecursedirs = templates_responsive resources images js data docs public *.egg-info __pycache__ .* *.egg _darcs build CVS dist node_modules venv {arch}
+
+# legacy|xunit1|xunit2
+#junit_family = legacy
+
+# no|log|system-out|system-err|out-err|all
+junit_logging = all
\ No newline at end of file


[allura] 08/14: [#8455] update test runner to use pytest and pytest-xdist for parallelization

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit a92536f9de4ca1531e401790c7e0b8d157a3161b
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Tue Nov 1 14:53:18 2022 +0000

    [#8455] update test runner to use pytest and pytest-xdist for parallelization
---
 requirements-dev.txt |  4 ++--
 requirements.in      |  1 +
 requirements.txt     |  6 ++++++
 run_tests            | 35 +++++++++++++++++++----------------
 4 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index de987cb82..71ace7eda 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -7,7 +7,7 @@ sphinx-argparse
 sphinx-rtd-theme
 sphinxcontrib-programoutput
 coverage
-nose
-nose-xunitmp
+pytest
+pytest-xdist
 pycodestyle
 pyflakes
diff --git a/requirements.in b/requirements.in
index 0226b62f9..2fbf36b84 100644
--- a/requirements.in
+++ b/requirements.in
@@ -55,6 +55,7 @@ pyflakes
 testfixtures
 WebTest
 pytest
+pytest-xdist
 
 # deployment
 gunicorn
diff --git a/requirements.txt b/requirements.txt
index 54e502cb9..4d7392610 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -40,6 +40,8 @@ easywidgets==0.4.1
     # via -r requirements.in
 emoji==2.2.0
     # via -r requirements.in
+execnet==1.9.0
+    # via pytest-xdist
 feedgenerator==2.0.0
     # via -r requirements.in
 feedparser==6.0.10
@@ -140,6 +142,10 @@ pypeline[creole,markdown,rst,textile]==0.6.1
 pysolr==3.9.0
     # via -r requirements.in
 pytest==7.1.3
+    # via
+    #   -r requirements.in
+    #   pytest-xdist
+pytest-xdist==3.0.2
     # via -r requirements.in
 python-dateutil==2.8.2
     # via
diff --git a/run_tests b/run_tests
index 23b8d3d4f..813314b20 100755
--- a/run_tests
+++ b/run_tests
@@ -31,7 +31,6 @@ import six
 CPUS = multiprocessing.cpu_count()
 CONCURRENT_SUITES = (CPUS // 4) or CPUS
 CONCURRENT_TESTS = CPUS // CONCURRENT_SUITES
-PROC_TIMEOUT = 360
 
 ALT_PKG_PATHS = {
     'Allura': 'allura/tests/',
@@ -68,7 +67,7 @@ def run_one(cmd, **popen_kwargs):
     out_remainder, _ = proc.communicate()
     sys.stdout.write(print_ensured(out_remainder))
     sys.stdout.flush()
-    print('finished {}'.format(cmd_to_show))
+    print('finished {}, with returncode: {}'.format(cmd_to_show, proc.returncode))
     sys.stdout.flush()
     return proc
 
@@ -117,17 +116,21 @@ def check_packages(packages):
             yield pkg
 
 
-def run_tests_in_parallel(options, nosetests_args):
+def run_tests_in_parallel(options, runner_args):
+    default_args = [
+        # '-c /dev/null',  # pytest's equivalent of nose's NOSE_IGNORE_CONFIG_FILES='1' is '-c /dev/null/'
+        '--disable-warnings',
+    ]
+
     def get_pkg_path(pkg):
         return ALT_PKG_PATHS.get(pkg, '')
 
     def get_multiproc_args(pkg):
         if options.concurrent_tests == 1:
             return ''
-        return ('--processes={procs_per_suite} --process-timeout={proc_timeout}'.format(
-            procs_per_suite=options.concurrent_tests,
-            proc_timeout=PROC_TIMEOUT)
-            if pkg not in NOT_MULTIPROC_SAFE else '')
+        return '-n {procs_per_suite} --dist loadfile'.format(
+            procs_per_suite=options.concurrent_tests
+        ) if pkg not in NOT_MULTIPROC_SAFE else ''
 
     def get_concurrent_suites():
         if '-n' in sys.argv:
@@ -135,13 +138,12 @@ def run_tests_in_parallel(options, nosetests_args):
         return CPUS
 
     cmds = []
-    env = dict(os.environ,
-               NOSE_IGNORE_CONFIG_FILES='1')
+    env = dict(os.environ)
     for package in check_packages(options.packages):
-        runner = 'nosetests'
+        runner = 'pytest'
         if options.coverage:
-            # This is the recommended way to run coverage + nose  https://coverage.readthedocs.io/en/latest/faq.html
-            runner = 'coverage run $(which nosetests)'
+            # This is the recommended way to run coverage + pytest  https://coverage.readthedocs.io/en/6.5.0/
+            runner = f'coverage run -m {runner}'
             """
             And using config settings in setup.cfg seems to work well with parallel processes
             Otherwise need to run with a complex setup like:
@@ -154,10 +156,10 @@ def run_tests_in_parallel(options, nosetests_args):
             """
 
         multiproc_args = get_multiproc_args(package)
-        cmd = "{runner} {pkg_path} {nosetests_args} {multiproc_args}".format(
+        cmd = "{runner} {pkg_path} {args} {multiproc_args}".format(
             runner=runner,
             pkg_path=get_pkg_path(package),
-            nosetests_args=' '.join(nosetests_args),
+            args=' '.join(default_args + runner_args),
             multiproc_args=multiproc_args,
         )
         if options.coverage:
@@ -184,12 +186,13 @@ def run_tests_in_parallel(options, nosetests_args):
 def parse_args():
     parser = argparse.ArgumentParser(
         formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog='''All additional arguments are passed along to nosetests (e.g. -v)''')
+        epilog='''All additional arguments are passed along to pytest (e.g. -v)''')
     parser.add_argument('-n', help='Number of test suites to run concurrently in separate '
                                    'processes. Default: # CPUs / 4',
                         dest='concurrent_suites', type=int, default=CONCURRENT_SUITES)
     parser.add_argument('-m', help='Number of tests to run concurrently in separate '
-                                   'processes, per suite. Default: # CPUs / # concurrent suites',
+                                   'processes, per suite. Default: # CPUs / # concurrent suites. '
+                                   '(equivalent to pytest-xdist\'s -n option)',
                         dest='concurrent_tests', type=int, default=CONCURRENT_TESTS)
     parser.add_argument('--coverage', action='store_true',
                         help='Collect code coverage details, and report')


[allura] 13/14: [#8455] misc fixes to tests from pytest conversion

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 0c064dc0c67d84f7404569265edb7b4225276302
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Wed Nov 2 20:44:44 2022 +0000

    [#8455] misc fixes to tests from pytest conversion
---
 Allura/allura/tests/test_globals.py                | 28 ++++++++++++----------
 ForgeBlog/forgeblog/tests/unit/__init__.py         |  2 +-
 ForgeFeedback/forgefeedback/tests/unit/__init__.py |  2 +-
 ForgeFiles/forgefiles/tests/model/__init__.py      |  2 +-
 ForgeTracker/forgetracker/tests/unit/__init__.py   |  2 +-
 5 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index 01a48daf1..9b16328bc 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -48,6 +48,21 @@ from forgewiki import model as WM
 from forgeblog import model as BM
 
 
+def setup():
+    setup_basic_test()
+    setup_unit_test()
+    setup_with_tools()
+
+
+def teardown():
+    setup()
+
+
+@td.with_wiki
+def setup_with_tools():
+    setup_global_objects()
+
+
 def squish_spaces(text):
     # \s is whitespace
     # \xa0 is &nbsp; in unicode form
@@ -78,13 +93,8 @@ def get_projects_property_in_the_same_order(names, prop):
 
 class Test():
 
-    @classmethod
-    def setup_class(cls):
-        setup_basic_test()
-        setup_global_objects()
-
     def setup_method(self, method):
-        setup_global_objects()
+        setup()
         p_nbhd = M.Neighborhood.query.get(name='Projects')
         p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
         self.acl_bak = p_test.acl.copy()
@@ -273,7 +283,6 @@ class Test():
         assert '</a><br/><br/><a href=' not in r, r
         assert '</a></li><li><a href=' in r, r
 
-    @td.with_wiki
     def test_macro_include_no_extra_br(self):
         p_nbhd = M.Neighborhood.query.get(name='Projects')
         p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
@@ -304,7 +313,6 @@ class Test():
     <p></p></div>'''
         assert squish_spaces(html) == squish_spaces(expected_html)
 
-    @td.with_wiki
     @td.with_tool('test', 'Wiki', 'wiki2')
     def test_macro_include_permissions(self):
         p_nbhd = M.Neighborhood.query.get(name='Projects')
@@ -386,7 +394,6 @@ class Test():
             </li>
             </ul>''') in r
 
-    @td.with_wiki
     def test_wiki_artifact_links(self):
         text = g.markdown.convert('See [18:13:49]')
         assert 'See <span>[18:13:49]</span>' in text, text
@@ -436,7 +443,6 @@ class Test():
         assert g.markdown.convert(text) == '<pre>%s</pre>' % text
         assert g.markdown_wiki.convert(text) == '<pre>%s</pre>' % text
 
-    @td.with_wiki
     def test_markdown_basics(self):
         with h.push_context('test', 'wiki', neighborhood='Projects'):
             text = g.markdown.convert('# Foo!\n[Home]')
@@ -629,7 +635,6 @@ class Test():
             assert f'<span>[{charSuperLong}]</span>(Home)' in text, text  # current limitation, not a link
             # assert f'href="/p/test/wiki-len/Home/">{charSuperLong}</a>' in text, text  # ideal output
 
-    @td.with_wiki
     def test_macro_include(self):
         r = g.markdown.convert('[[include ref=Home id=foo]]')
         assert '<div id="foo">' in r, r
@@ -721,7 +726,6 @@ class Test():
             proj_title = f'<h2><a href="{p.url()}">{p.name}</a></h2>'
             assert proj_title in r
 
-    @td.with_wiki
     def test_hideawards_macro(self):
         p_nbhd = M.Neighborhood.query.get(name='Projects')
 
diff --git a/ForgeBlog/forgeblog/tests/unit/__init__.py b/ForgeBlog/forgeblog/tests/unit/__init__.py
index 648c054e8..59d93c01e 100644
--- a/ForgeBlog/forgeblog/tests/unit/__init__.py
+++ b/ForgeBlog/forgeblog/tests/unit/__init__.py
@@ -25,7 +25,7 @@ from allura import model as M
 from alluratest.controller import setup_basic_test
 
 
-def setUp():
+def setup_module():
     setup_basic_test()
 
 
diff --git a/ForgeFeedback/forgefeedback/tests/unit/__init__.py b/ForgeFeedback/forgefeedback/tests/unit/__init__.py
index 1bd8052c5..757dcf782 100644
--- a/ForgeFeedback/forgefeedback/tests/unit/__init__.py
+++ b/ForgeFeedback/forgefeedback/tests/unit/__init__.py
@@ -25,7 +25,7 @@ from allura import model as M
 from alluratest.controller import setup_basic_test
 
 
-def setUp():
+def setup_module():
     setup_basic_test()
 
 
diff --git a/ForgeFiles/forgefiles/tests/model/__init__.py b/ForgeFiles/forgefiles/tests/model/__init__.py
index 72fc7a79b..2b562f8dd 100644
--- a/ForgeFiles/forgefiles/tests/model/__init__.py
+++ b/ForgeFiles/forgefiles/tests/model/__init__.py
@@ -25,7 +25,7 @@ from allura import model as M
 from alluratest.controller import setup_basic_test
 
 
-def setUp():
+def setup_module():
     setup_basic_test()
 
 
diff --git a/ForgeTracker/forgetracker/tests/unit/__init__.py b/ForgeTracker/forgetracker/tests/unit/__init__.py
index 3783fc20f..3a23685cf 100644
--- a/ForgeTracker/forgetracker/tests/unit/__init__.py
+++ b/ForgeTracker/forgetracker/tests/unit/__init__.py
@@ -27,7 +27,7 @@ from allura import model as M
 from alluratest.controller import setup_basic_test
 
 
-def setUp():
+def setup_module():
     setup_basic_test()
 
 


[allura] 02/14: [#8455] converted yield test to pytest.mark.parametrize

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch pytest-finalize
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 8709bdc106405a4597e95d4b8d4fc067e4383e22
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Fri Sep 23 18:43:16 2022 +0000

    [#8455] converted yield test to pytest.mark.parametrize
---
 Allura/allura/tests/functional/test_auth.py | 52 +++++++++++++++--------------
 1 file changed, 27 insertions(+), 25 deletions(-)

diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 06a3daf9d..c016a39b1 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -32,6 +32,7 @@ from ming.orm.ormsession import ThreadLocalORMSession, session
 from tg import config, expose
 from mock import patch, Mock
 import mock
+import pytest
 from tg import tmpl_context as c, app_globals as g
 
 from allura.tests import TestController
@@ -562,36 +563,37 @@ class TestAuth(TestController):
         assert hash_expiry == '04-08-2020'
         return user
 
-    def test_token_generator(self):
+    @pytest.mark.parametrize(['change_params'], [
+        pytest.param({'new_addr.addr': 'test_abcd@domain.net',  # Change primary address
+                      'primary_addr': 'test@example.com'},
+                     id='change_primary'),
+        pytest.param({'new_addr.addr': 'test@example.com',  # Claim new address
+                      'new_addr.claim': 'Claim Address',
+                      'primary_addr': 'test-admin@users.localhost',
+                      'password': 'foo',
+                      'preferences.email_format': 'plain'},
+                     id='claim_new'),
+        pytest.param({'addr-1.ord': '1',  # remove test-admin@users.localhost
+                      'addr-1.delete': 'on',
+                      'addr-2.ord': '2',
+                      'new_addr.addr': '',
+                      'primary_addr': 'test-admin@users.localhost',
+                      'password': 'foo',
+                      'preferences.email_format': 'plain'},
+                     id='remove_one'),
+        pytest.param({'addr-1.ord': '1',  # Remove email
+                      'addr-2.ord': '2',
+                      'addr-2.delete': 'on',
+                      'new_addr.addr': '',
+                      'primary_addr': 'test-admin@users.localhost'},
+                     id='remove_all'),
+    ])
+    def test_email_change_invalidates_token(self, change_params):
         """ Generates new token invalidation tests.
 
         The tests cover: changing, claiming, updating, removing email addresses.
         :returns: email_change_invalidates_token
         """
-        _params = [{'new_addr.addr': 'test_abcd@domain.net',  # Change primary address
-                    'primary_addr': 'test@example.com', },
-                   {'new_addr.addr': 'test@example.com',  # Claim new address
-                    'new_addr.claim': 'Claim Address',
-                    'primary_addr': 'test-admin@users.localhost',
-                    'password': 'foo',
-                    'preferences.email_format': 'plain'},
-                   {'addr-1.ord': '1',  # remove test-admin@users.localhost
-                    'addr-1.delete': 'on',
-                    'addr-2.ord': '2',
-                    'new_addr.addr': '',
-                    'primary_addr': 'test-admin@users.localhost',
-                    'password': 'foo',
-                    'preferences.email_format': 'plain'},
-                   {'addr-1.ord': '1',  # Remove email
-                    'addr-2.ord': '2',
-                    'addr-2.delete': 'on',
-                    'new_addr.addr': '',
-                    'primary_addr': 'test-admin@users.localhost'}]
-
-        for param in _params:
-            yield self.email_change_invalidates_token, param
-
-    def email_change_invalidates_token(self, change_params):
         user = self._create_password_reset_hash()
         session(user).flush(user)