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/09/15 18:36:50 UTC

[allura] 04/05: all trivial failures resolved for ./Allura, only legit failures remain

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

dill0wn pushed a commit to branch dw/8455
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 822573fb17a8c1fd4a0b444e284b4f4cef03b22f
Author: Kenton Taylor <kt...@slashdotmedia.com>
AuthorDate: Tue Aug 23 20:19:46 2022 +0000

    all trivial failures resolved for ./Allura, only legit failures remain
---
 Allura/allura/tests/functional/test_admin.py       |    2 +-
 Allura/allura/tests/functional/test_discuss.py     |    2 +-
 Allura/allura/tests/functional/test_feeds.py       |    4 +-
 Allura/allura/tests/functional/test_nav.py         |    2 +-
 .../tests/functional/test_personal_dashboard.py    |    2 +-
 Allura/allura/tests/functional/test_rest.py        |    2 +-
 Allura/allura/tests/functional/test_root.py        |    4 +-
 Allura/allura/tests/functional/test_site_admin.py  |    4 +-
 .../allura/tests/functional/test_user_profile.py   |    3 -
 Allura/allura/tests/model/test_artifact.py         |    5 +-
 Allura/allura/tests/model/test_auth.py             |    2 +-
 Allura/allura/tests/model/test_discussion.py       |    4 +-
 Allura/allura/tests/model/test_filesystem.py       |    2 +-
 Allura/allura/tests/model/test_monq.py             |    2 +-
 Allura/allura/tests/model/test_neighborhood.py     |    2 +-
 Allura/allura/tests/model/test_notification.py     |   12 +-
 Allura/allura/tests/model/test_oauth.py            |    2 +-
 Allura/allura/tests/model/test_project.py          |    3 +-
 Allura/allura/tests/model/test_repo.py             |    8 +-
 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      |    4 +-
 .../tests/templates/jinja_master/test_lib.py       |    2 +-
 Allura/allura/tests/test_app.py                    |   12 +-
 Allura/allura/tests/test_commands.py               |   19 +-
 Allura/allura/tests/test_decorators.py             |    4 +
 Allura/allura/tests/test_diff.py                   |    2 +-
 Allura/allura/tests/test_globals.py                | 1452 ++++++++++----------
 Allura/allura/tests/test_helpers.py                |   19 +-
 Allura/allura/tests/test_mail_util.py              |    6 +-
 Allura/allura/tests/test_middlewares.py            |    2 +-
 Allura/allura/tests/test_multifactor.py            |    8 +-
 Allura/allura/tests/test_plugin.py                 |   11 +-
 Allura/allura/tests/test_scripttask.py             |    2 +-
 Allura/allura/tests/test_tasks.py                  |   16 +-
 Allura/allura/tests/test_utils.py                  |    8 +-
 Allura/allura/tests/test_validators.py             |   37 +-
 Allura/allura/tests/test_webhooks.py               |   10 +-
 Allura/allura/tests/unit/__init__.py               |    6 +-
 .../test_discussion_moderation_controller.py       |    4 +-
 Allura/allura/tests/unit/phone/test_nexmo.py       |    2 +-
 Allura/allura/tests/unit/spam/test_spam_filter.py  |    2 +-
 .../allura/tests/unit/spam/test_stopforumspam.py   |    2 +-
 Allura/allura/tests/unit/test_app.py               |    6 +-
 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    |    8 +-
 .../allura/tests/unit/test_ldap_auth_provider.py   |    2 +-
 Allura/allura/tests/unit/test_mixins.py            |    2 +-
 Allura/allura/tests/unit/test_post_model.py        |    2 +-
 Allura/allura/tests/unit/test_repo.py              |    2 +-
 Allura/allura/tests/unit/test_session.py           |    6 +-
 Allura/allura/tests/unit/test_solr.py              |    8 +-
 .../forgetracker/tests/functional/test_root.py     |    4 +-
 56 files changed, 880 insertions(+), 869 deletions(-)

diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 8c1caab3a..b5f39a393 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -964,7 +964,7 @@ class TestProjectAdmin(TestController):
 @with_nose_compatibility
 class TestExport(TestController):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.setup_with_tools()
 
diff --git a/Allura/allura/tests/functional/test_discuss.py b/Allura/allura/tests/functional/test_discuss.py
index 5164142d3..a2442074a 100644
--- a/Allura/allura/tests/functional/test_discuss.py
+++ b/Allura/allura/tests/functional/test_discuss.py
@@ -402,7 +402,7 @@ class TestDiscuss(TestDiscussBase):
 @with_nose_compatibility
 class TestAttachment(TestDiscussBase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.thread_link = self._thread_link()
         thread = self.app.get(self.thread_link)
diff --git a/Allura/allura/tests/functional/test_feeds.py b/Allura/allura/tests/functional/test_feeds.py
index de32fa04c..4baf5f9d0 100644
--- a/Allura/allura/tests/functional/test_feeds.py
+++ b/Allura/allura/tests/functional/test_feeds.py
@@ -26,8 +26,8 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestFeeds(TestController):
 
-    def setup_class(self, method):
-        TestController.setUp(self)
+    def setup_method(self, method):
+        TestController.setup_method(self, method)
         self._setUp()
 
     @td.with_wiki
diff --git a/Allura/allura/tests/functional/test_nav.py b/Allura/allura/tests/functional/test_nav.py
index f865d8f32..840a97969 100644
--- a/Allura/allura/tests/functional/test_nav.py
+++ b/Allura/allura/tests/functional/test_nav.py
@@ -33,7 +33,7 @@ class TestNavigation(TestController):
     - Test of logo.
     """
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.logo_pattern = ('div', {'class': 'nav-logo'})
         self.global_nav_pattern = ('nav', {'class': 'nav-left'})
diff --git a/Allura/allura/tests/functional/test_personal_dashboard.py b/Allura/allura/tests/functional/test_personal_dashboard.py
index ea3244123..428afb029 100644
--- a/Allura/allura/tests/functional/test_personal_dashboard.py
+++ b/Allura/allura/tests/functional/test_personal_dashboard.py
@@ -89,7 +89,7 @@ class TestTicketsSection(TrackerTestController):
 @with_nose_compatibility
 class TestMergeRequestsSection(TestController):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         setup_unit_test()
         self.setup_with_tools()
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index a24ff59e4..0be71b396 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -420,7 +420,7 @@ class TestRestHome(TestRestApiBase):
 @with_nose_compatibility
 class TestRestNbhdAddProject(TestRestApiBase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         # create some troves we'll need
         M.TroveCategory(fullname="Root", trove_cat_id=1, trove_parent_id=0)
diff --git a/Allura/allura/tests/functional/test_root.py b/Allura/allura/tests/functional/test_root.py
index 47105f8d9..95a1ea15b 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -47,7 +47,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestRootController(TestController):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         n_adobe = M.Neighborhood.query.get(name='Adobe')
         assert n_adobe
@@ -192,7 +192,7 @@ class TestRootController(TestController):
 
 @with_nose_compatibility
 class TestRootWithSSLPattern(TestController):
-    def setup_class(self, method):
+    def setup_method(self, method):
         with td.patch_middleware_config({'force_ssl.pattern': '^/auth'}):
             super().setup_method(method)
 
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index fce6abe29..7ca541362 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -361,7 +361,7 @@ class TestProjectsSearch(TestController):
         'id': 'allura/model/project/Project#53ccf6e8100d2b0741746e9f',
     }])
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         # Create project that matches TEST_HIT id
         _id = ObjectId('53ccf6e8100d2b0741746e9f')
@@ -419,7 +419,7 @@ class TestUsersSearch(TestController):
         'user_registration_date_dt': '2014-09-09T13:17:38Z',
         'username_s': 'darth'}])
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         # Create user that matches TEST_HIT id
         _id = ObjectId('540efdf2100d2b1483155d39')
diff --git a/Allura/allura/tests/functional/test_user_profile.py b/Allura/allura/tests/functional/test_user_profile.py
index 9ec08f518..4a54ffa69 100644
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -269,9 +269,6 @@ class TestUserProfile(TestController):
         assert 'content="noindex, follow"' not in r.text
 
 
-
-
-
 @with_nose_compatibility
 class TestUserProfileHasAccessAPI(TestRestApiBase):
 
diff --git a/Allura/allura/tests/model/test_artifact.py b/Allura/allura/tests/model/test_artifact.py
index 43ac4828d..8f0100e82 100644
--- a/Allura/allura/tests/model/test_artifact.py
+++ b/Allura/allura/tests/model/test_artifact.py
@@ -54,7 +54,10 @@ class Checkmessage(M.Message):
 Mapper.compile_all()
 
 
-def setup_method(self, method):
+# def setup_method_wrapper(fn):
+#     fn(None)
+
+def setup_method():
     setup_basic_test()
     setup_unit_test()
     setup_with_tools()
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index 870fcfa21..2d6aa75f7 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -45,7 +45,7 @@ from alluratest.controller import setup_basic_test, setup_global_objects, setup_
 from allura.tests.pytest_helpers import with_nose_compatibility
 
 
-def setup_method(self, method):
+def setup_method():
     setup_basic_test()
     ThreadLocalORMSession.close_all()
     setup_global_objects()
diff --git a/Allura/allura/tests/model/test_discussion.py b/Allura/allura/tests/model/test_discussion.py
index 86f3467e0..1bfc445b9 100644
--- a/Allura/allura/tests/model/test_discussion.py
+++ b/Allura/allura/tests/model/test_discussion.py
@@ -38,9 +38,9 @@ from allura.tests import TestController
 from alluratest.controller import setup_global_objects
 
 
-def setup_method(self, method):
+def setup_method():
     controller = TestController()
-    controller.setup_method(method)
+    controller.setup_method(None)
     controller.app.get('/wiki/Home/')
     setup_global_objects()
     ThreadLocalORMSession.close_all()
diff --git a/Allura/allura/tests/model/test_filesystem.py b/Allura/allura/tests/model/test_filesystem.py
index 29a32b5d8..df0e5b7d5 100644
--- a/Allura/allura/tests/model/test_filesystem.py
+++ b/Allura/allura/tests/model/test_filesystem.py
@@ -41,7 +41,7 @@ Mapper.compile_all()
 @with_nose_compatibility
 class TestFile(TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         config = {
             'ming.main.uri': 'mim://host/allura',
             'ming.project.uri': 'mim://host/project-data',
diff --git a/Allura/allura/tests/model/test_monq.py b/Allura/allura/tests/model/test_monq.py
index f2fd7ad29..9dc682149 100644
--- a/Allura/allura/tests/model/test_monq.py
+++ b/Allura/allura/tests/model/test_monq.py
@@ -24,7 +24,7 @@ from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 
 
-def setup_method(self, method):
+def setup_method():
     setup_basic_test()
     ThreadLocalORMSession.close_all()
     setup_global_objects()
diff --git a/Allura/allura/tests/model/test_neighborhood.py b/Allura/allura/tests/model/test_neighborhood.py
index 28b85dca1..26a773331 100644
--- a/Allura/allura/tests/model/test_neighborhood.py
+++ b/Allura/allura/tests/model/test_neighborhood.py
@@ -25,7 +25,7 @@ from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-def setup_method(self, method):
+def setup_method():
     setup_basic_test()
     setup_with_tools()
 
diff --git a/Allura/allura/tests/model/test_notification.py b/Allura/allura/tests/model/test_notification.py
index 5df6a54df..7e0b8ed3d 100644
--- a/Allura/allura/tests/model/test_notification.py
+++ b/Allura/allura/tests/model/test_notification.py
@@ -37,7 +37,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestNotification(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.setup_with_tools()
 
@@ -169,7 +169,7 @@ class TestNotification(unittest.TestCase):
 @with_nose_compatibility
 class TestPostNotifications(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.setup_with_tools()
 
@@ -308,7 +308,7 @@ class TestPostNotifications(unittest.TestCase):
 @with_nose_compatibility
 class TestSubscriptionTypes(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.setup_with_tools()
 
@@ -346,10 +346,10 @@ class TestSubscriptionTypes(unittest.TestCase):
     def test_message(self):
         self._test_message()
 
-        self.setup_method(method)
+        self.setup_method(None)
         self._test_message()
 
-        self.setup_method(method)
+        self.setup_method(None)
         M.notification.MAILBOX_QUIESCENT = timedelta(minutes=1)
         # will raise "assert msg is not None" since the new message is not 1
         # min old:
@@ -481,7 +481,7 @@ class TestSubscriptionTypes(unittest.TestCase):
 
 @with_nose_compatibility
 class TestSiteNotification(unittest.TestCase):
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.note = M.SiteNotification(
             active=True,
             impressions=0,
diff --git a/Allura/allura/tests/model/test_oauth.py b/Allura/allura/tests/model/test_oauth.py
index 2a8b82ad6..67d12b094 100644
--- a/Allura/allura/tests/model/test_oauth.py
+++ b/Allura/allura/tests/model/test_oauth.py
@@ -24,7 +24,7 @@ from allura import model as M
 from alluratest.controller import setup_basic_test, setup_global_objects
 
 
-def setup_method(self, method):
+def setup_method():
     setup_basic_test()
     ThreadLocalORMSession.close_all()
     setup_global_objects()
diff --git a/Allura/allura/tests/model/test_project.py b/Allura/allura/tests/model/test_project.py
index 339168f15..524754bc3 100644
--- a/Allura/allura/tests/model/test_project.py
+++ b/Allura/allura/tests/model/test_project.py
@@ -31,7 +31,7 @@ from allura.lib.exceptions import ToolError, Invalid
 from mock import MagicMock, patch
 
 
-def setup_method(self, method):
+def setup_method():
     setup_basic_test()
     setup_with_tools()
 
@@ -41,6 +41,7 @@ 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()
diff --git a/Allura/allura/tests/model/test_repo.py b/Allura/allura/tests/model/test_repo.py
index 60d5487c7..873b411c8 100644
--- a/Allura/allura/tests/model/test_repo.py
+++ b/Allura/allura/tests/model/test_repo.py
@@ -74,7 +74,7 @@ class RepoImplTestBase:
 
 
 class RepoTestBase(unittest.TestCase):
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     @mock.patch('allura.model.repository.Repository.url')
@@ -132,7 +132,7 @@ class RepoTestBase(unittest.TestCase):
 
 @with_nose_compatibility
 class TestLastCommit(unittest.TestCase):
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
         self.repo = mock.Mock(
@@ -405,7 +405,7 @@ class TestLastCommit(unittest.TestCase):
 
 @with_nose_compatibility
 class TestModelCache(unittest.TestCase):
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.cache = M.repository.ModelCache()
 
     def test_normalize_query(self):
@@ -685,7 +685,7 @@ class TestModelCache(unittest.TestCase):
 @with_nose_compatibility
 class TestMergeRequest:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
         self.mr = M.MergeRequest(
diff --git a/Allura/allura/tests/model/test_timeline.py b/Allura/allura/tests/model/test_timeline.py
index f1e32434b..3d8bc26e7 100644
--- a/Allura/allura/tests/model/test_timeline.py
+++ b/Allura/allura/tests/model/test_timeline.py
@@ -27,7 +27,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 class TestActivityObject_Functional:
     # NOTE not for unit tests, this class sets up all the junk
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         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 6df1d6f3a..c5835109f 100644
--- a/Allura/allura/tests/scripts/test_create_sitemap_files.py
+++ b/Allura/allura/tests/scripts/test_create_sitemap_files.py
@@ -33,7 +33,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestCreateSitemapFiles:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     def run_script(self, options):
diff --git a/Allura/allura/tests/scripts/test_delete_projects.py b/Allura/allura/tests/scripts/test_delete_projects.py
index 29a983f8c..57c219f54 100644
--- a/Allura/allura/tests/scripts/test_delete_projects.py
+++ b/Allura/allura/tests/scripts/test_delete_projects.py
@@ -31,7 +31,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestDeleteProjects(TestController):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         n = M.Neighborhood.query.get(name='Projects')
         admin = M.User.by_username('test-admin')
diff --git a/Allura/allura/tests/scripts/test_misc_scripts.py b/Allura/allura/tests/scripts/test_misc_scripts.py
index c36a064b6..8ebbba848 100644
--- a/Allura/allura/tests/scripts/test_misc_scripts.py
+++ b/Allura/allura/tests/scripts/test_misc_scripts.py
@@ -28,7 +28,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestClearOldNotifications:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     def run_script(self, options):
diff --git a/Allura/allura/tests/scripts/test_reindexes.py b/Allura/allura/tests/scripts/test_reindexes.py
index eaa4eb7bc..53a51f04a 100644
--- a/Allura/allura/tests/scripts/test_reindexes.py
+++ b/Allura/allura/tests/scripts/test_reindexes.py
@@ -28,7 +28,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestReindexProjects:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     def run_script(self, options):
@@ -53,7 +53,7 @@ class TestReindexProjects:
 @with_nose_compatibility
 class TestReindexUsers:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     def run_script(self, options):
diff --git a/Allura/allura/tests/templates/jinja_master/test_lib.py b/Allura/allura/tests/templates/jinja_master/test_lib.py
index 87cc5fe87..f3eea9edd 100644
--- a/Allura/allura/tests/templates/jinja_master/test_lib.py
+++ b/Allura/allura/tests/templates/jinja_master/test_lib.py
@@ -30,7 +30,7 @@ def strip_space(s):
 
 
 class TemplateTest:
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.jinja2_env = AlluraJinjaRenderer.create(config, g)['jinja'].jinja2_env
 
diff --git a/Allura/allura/tests/test_app.py b/Allura/allura/tests/test_app.py
index eeeb9ecea..b93170a8e 100644
--- a/Allura/allura/tests/test_app.py
+++ b/Allura/allura/tests/test_app.py
@@ -18,17 +18,18 @@
 from tg import tmpl_context as c
 import mock
 from ming.base import Object
-from alluratest.tools import assert_equal, assert_raises
+from alluratest.tools import assert_raises
 from formencode import validators as fev
 
 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 allura.tests.pytest_helpers import with_nose_compatibility
 
 
-def setup_method(self, method):
+def setup_method():
     setup_unit_test()
     c.user._id = None
     c.project = mock.Mock()
@@ -48,7 +49,6 @@ def setup_method(self, method):
     c.app.url = c.app.config.url()
     c.app.__version__ = '0.0'
 
-
 def test_config_options():
     options = [
         app.ConfigOption('test1', str, 'MyTestValue'),
@@ -77,11 +77,13 @@ def test_config_option_with_validator():
     assert_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)]
@@ -94,7 +96,7 @@ def test_options_on_install():
     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
@@ -110,6 +112,7 @@ def test_main_menu():
     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/'),
@@ -125,6 +128,7 @@ def test_sitemap():
     assert len(sm.children) == 3
 
 
+@with_setup(setup_method)
 @mock.patch('allura.app.Application.PostClass.query.get')
 def test_handle_artifact_unicode(qg):
     """
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index 03b3fb954..84bac7a10 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -19,7 +19,7 @@
 import datetime
 
 import six
-from alluratest.tools import assert_raises, assert_in
+from alluratest.tools import assert_raises, assert_in, with_setup
 from testfixtures import OutputCapture
 
 from datadiff.tools import assert_equal
@@ -30,7 +30,7 @@ from mock import Mock, call, patch
 import pymongo
 import pkg_resources
 
-from alluratest.controller import setup_basic_test, setup_global_objects
+from alluratest.controller import setup_basic_test, setup_global_objects, setup_unit_test
 from allura.command import base, script, set_neighborhood_features, \
     create_neighborhood, show_models, taskd_cleanup, taskd
 from allura import model as M
@@ -47,12 +47,13 @@ class EmptyClass:
     pass
 
 
-def setup_class(self, method):
+def setup_method():
     """Method called by nose before running each test"""
     setup_basic_test()
     setup_global_objects()
+    setup_unit_test()
 
-
+@with_setup(setup_method)
 def test_script():
     cmd = script.ScriptCommand('script')
     cmd.run(
@@ -61,6 +62,7 @@ def test_script():
                   [test_config, pkg_resources.resource_filename('allura', 'tests/tscript_error.py')])
 
 
+@with_setup(setup_method)
 def test_set_neighborhood_max_projects():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -84,6 +86,7 @@ def test_set_neighborhood_max_projects():
                   [test_config, str(n_id), 'max_projects', '2.8'])
 
 
+@with_setup(setup_method)
 def test_set_neighborhood_private():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -109,6 +112,7 @@ def test_set_neighborhood_private():
                   [test_config, str(n_id), 'private_projects', '2.8'])
 
 
+@with_setup(setup_method)
 def test_set_neighborhood_google_analytics():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -134,6 +138,7 @@ def test_set_neighborhood_google_analytics():
                   [test_config, str(n_id), 'google_analytics', '2.8'])
 
 
+@with_setup(setup_method)
 def test_set_neighborhood_css():
     neighborhood = M.Neighborhood.query.find().first()
     n_id = neighborhood._id
@@ -168,6 +173,7 @@ def test_set_neighborhood_css():
                   [test_config, str(n_id), 'css', 'True'])
 
 
+@with_setup(setup_method)
 def test_update_neighborhood():
     cmd = create_neighborhood.UpdateNeighborhoodCommand('update-neighborhood')
     cmd.run([test_config, 'Projects', 'True'])
@@ -341,7 +347,7 @@ class TestTaskCommand:
 @with_nose_compatibility
 class TestTaskdCleanupCommand:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.cmd_class = taskd_cleanup.TaskdCleanupCommand
         self.old_check_taskd_status = self.cmd_class._check_taskd_status
         self.cmd_class._check_taskd_status = lambda x, p: 'OK'
@@ -503,6 +509,9 @@ class TestReindexAsTask:
 @with_nose_compatibility
 class TestReindexCommand:
 
+    def setup_method(self, method):
+        setup_method()
+
     @patch('allura.command.show_models.g')
     def test_skip_solr_delete(self, g):
         cmd = show_models.ReindexCommand('reindex')
diff --git a/Allura/allura/tests/test_decorators.py b/Allura/allura/tests/test_decorators.py
index 5bdf5cf70..033e5c41a 100644
--- a/Allura/allura/tests/test_decorators.py
+++ b/Allura/allura/tests/test_decorators.py
@@ -23,11 +23,15 @@ import gc
 from alluratest.tools import assert_equal, assert_not_equal
 from allura.tests.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):
+        setup_basic_test()
+
     def test_no_params(self):
         @task
         def func():
diff --git a/Allura/allura/tests/test_diff.py b/Allura/allura/tests/test_diff.py
index 505b95c33..d128386b0 100644
--- a/Allura/allura/tests/test_diff.py
+++ b/Allura/allura/tests/test_diff.py
@@ -24,7 +24,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestHtmlSideBySideDiff(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.diff = HtmlSideBySideDiff()
 
     def test_render_change(self):
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index b1f67003b..4d4b3b592 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -25,7 +25,6 @@ import six
 from mock import patch, Mock
 
 from bson import ObjectId
-from alluratest.tools import with_setup, assert_equal, assert_in, assert_not_in
 from tg import tmpl_context as c, app_globals as g
 import tg
 from oembed import OEmbedError
@@ -49,759 +48,15 @@ from forgewiki import model as WM
 from forgeblog import model as BM
 
 
-def squish_spaces(text):
-    # \s is whitespace
-    # \xa0 is &nbsp; in unicode form
-    return re.sub(r'[\s\xa0]+', ' ', text)
-
-
-def setup_method(self, method):
-    """Method called by nose once before running the package.  Some functions need it run again to reset data"""
+def setup_module(module):
     setup_basic_test()
     setup_unit_test()
-    setup_with_tools()
-
-
-def teadown_method():
-    setup_method(None)
-
-
-@td.with_wiki
-def setup_with_tools():
-    setup_global_objects()
-
-
-@td.with_wiki
-def test_app_globals():
-    with h.push_context('test', 'wiki', neighborhood='Projects'):
-        assert g.app_static(
-            'css/wiki.css') == '/nf/_static_/wiki/css/wiki.css', g.app_static('css/wiki.css')
-
-
-@with_setup(setup_method)
-def test_macro_projects():
-    file_name = 'neo-icon-set-454545-256x350.png'
-    file_path = os.path.join(
-        allura.__path__[0], 'nf', 'allura', 'images', file_name)
-
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
-    c.project = p_test
-    icon_file = open(file_path, 'rb')
-    M.ProjectFile.save_image(
-        file_name, icon_file, content_type='image/png',
-        square=True, thumbnail_size=(48, 48),
-        thumbnail_meta=dict(project_id=c.project._id, category='icon'))
-    icon_file.close()
-    p_test2 = M.Project.query.get(
-        shortname='test2', neighborhood_id=p_nbhd._id)
-    c.project = p_test2
-    icon_file = open(file_path, 'rb')
-    M.ProjectFile.save_image(
-        file_name, icon_file, content_type='image/png',
-        square=True, thumbnail_size=(48, 48),
-        thumbnail_meta=dict(project_id=c.project._id, category='icon'))
-    icon_file.close()
-    p_sub1 = M.Project.query.get(
-        shortname='test/sub1', neighborhood_id=p_nbhd._id)
-    c.project = p_sub1
-    icon_file = open(file_path, 'rb')
-    M.ProjectFile.save_image(
-        file_name, icon_file, content_type='image/png',
-        square=True, thumbnail_size=(48, 48),
-        thumbnail_meta=dict(project_id=c.project._id, category='icon'))
-    icon_file.close()
-    p_test.labels = ['test', 'root']
-    p_sub1.labels = ['test', 'sub1']
-    # Make one project private
-    p_test.private = False
-    p_sub1.private = False
-    p_test2.private = True
-
-    ThreadLocalORMSession.flush_all()
-
-    with h.push_config(c,
-                       project=p_nbhd.neighborhood_project,
-                       user=M.User.by_username('test-admin')):
-        r = g.markdown_wiki.convert('[[projects]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' in r, r
-        r = g.markdown_wiki.convert('[[projects labels=root]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' not in r, r
-        r = g.markdown_wiki.convert('[[projects labels=sub1]]')
-        assert 'alt="Test Project Logo"' not in r, r
-        assert 'alt="A Subproject Logo"' in r, r
-        r = g.markdown_wiki.convert('[[projects labels=test]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' in r, r
-        r = g.markdown_wiki.convert('[[projects labels=test,root]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' not in r, r
-        r = g.markdown_wiki.convert('[[projects labels=test,sub1]]')
-        assert 'alt="Test Project Logo"' not in r, r
-        assert 'alt="A Subproject Logo"' in r, r
-        r = g.markdown_wiki.convert('[[projects labels=root|sub1]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' in r, r
-        r = g.markdown_wiki.convert('[[projects labels=test,root|root,sub1]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' not in r, r
-        r = g.markdown_wiki.convert('[[projects labels=test,root|test,sub1]]')
-        assert 'alt="Test Project Logo"' in r, r
-        assert 'alt="A Subproject Logo"' in r, r
-        r = g.markdown_wiki.convert('[[projects show_total=True sort=random]]')
-        assert '<p class="macro_projects_total">3 Projects' in r, r
-        r = g.markdown_wiki.convert(
-            '[[projects show_total=True private=True sort=random]]')
-        assert '<p class="macro_projects_total">1 Projects' in r, r
-        assert 'alt="Test 2 Logo"' in r, r
-        assert 'alt="Test Project Logo"' not in r, r
-        assert 'alt="A Subproject Logo"' not in r, r
-
-        r = g.markdown_wiki.convert('[[projects show_proj_icon=True]]')
-        assert 'alt="Test Project Logo"' in r
-        r = g.markdown_wiki.convert('[[projects show_proj_icon=False]]')
-        assert 'alt="Test Project Logo"' not in r
-
-
-def test_macro_neighborhood_feeds():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
-    with h.push_context('--init--', 'wiki', neighborhood='Projects'):
-        r = g.markdown_wiki.convert('[[neighborhood_feeds tool_name=wiki]]')
-        assert 'Home modified by' in r, r
-        r = re.sub(r'<small>.*? ago</small>', '', r)  # remove "less than 1 second ago" etc
-        orig_len = len(r)
-        # Make project private & verify we don't see its new feed items
-        anon = M.User.anonymous()
-        p_test.acl.insert(0, M.ACE.deny(
-            M.ProjectRole.anonymous(p_test)._id, 'read'))
-        ThreadLocalORMSession.flush_all()
-        pg = WM.Page.query.get(title='Home', app_config_id=c.app.config._id)
-        pg.text = 'Change'
-        with h.push_config(c, user=M.User.by_username('test-admin')):
-            pg.commit()
-        r = g.markdown_wiki.convert('[[neighborhood_feeds tool_name=wiki]]')
-        r = re.sub(r'<small>.*? ago</small>', '', r)  # remove "less than 1 second ago" etc
-        new_len = len(r)
-        assert new_len == orig_len
-        p = BM.BlogPost(title='test me',
-                        neighborhood_id=p_test.neighborhood_id)
-        p.text = 'test content'
-        p.state = 'published'
-        p.make_slug()
-        with h.push_config(c, user=M.User.by_username('test-admin')):
-            p.commit()
-        ThreadLocalORMSession.flush_all()
-        with h.push_config(c, user=anon):
-            r = g.markdown_wiki.convert('[[neighborhood_blog_posts]]')
-        assert 'test content' in r
-
-
-@with_setup(setup_method)
-def test_macro_members():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
-    p_test.add_user(M.User.by_username('test-user'), ['Developer'])
-    p_test.add_user(M.User.by_username('test-user-0'), ['Member'])
-    ThreadLocalORMSession.flush_all()
-    r = g.markdown_wiki.convert('[[members limit=2]]').replace('\t', '').replace('\n', '')
-    assert (r ==
-                 '<div class="markdown_content"><h6>Project Members:</h6>'
-                 '<ul class="md-users-list">'
-                 '<li><a href="/u/test-admin/">Test Admin</a> (admin)</li>'
-                 '<li><a href="/u/test-user/">Test User</a></li>'
-                 '<li class="md-users-list-more"><a href="/p/test/_members">All Members</a></li>'
-                 '</ul>'
-                 '</div>')
-
-
-@with_setup(setup_method)
-def test_macro_members_escaping():
-    user = M.User.by_username('test-admin')
-    user.display_name = 'Test Admin <script>'
-    r = g.markdown_wiki.convert('[[members]]')
-    assert (r.replace('\n', '').replace('\t', '') ==
-                 '<div class="markdown_content"><h6>Project Members:</h6>'
-                 '<ul class="md-users-list">'
-                 '<li><a href="/u/test-admin/">Test Admin &lt;script&gt;</a> (admin)</li>'
-                 '</ul></div>')
-
-
-@with_setup(setup_method)
-def test_macro_project_admins():
-    user = M.User.by_username('test-admin')
-    user.display_name = 'Test Ådmin <script>'
-    with h.push_context('test', neighborhood='Projects'):
-        r = g.markdown_wiki.convert('[[project_admins]]')
-    assert (r.replace('\n', '') ==
-                 '<div class="markdown_content"><h6>Project Admins:</h6>'
-                 '<ul class="md-users-list">'
-                 '    <li><a href="/u/test-admin/">Test \xc5dmin &lt;script&gt;</a></li>'
-                 '</ul></div>')
-
-
-@with_setup(setup_method)
-def test_macro_project_admins_one_br():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
-    p_test.add_user(M.User.by_username('test-user'), ['Admin'])
-    ThreadLocalORMSession.flush_all()
-    with h.push_config(c, project=p_test):
-        r = g.markdown_wiki.convert('[[project_admins]]\n[[download_button]]')
-
-    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():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
-    wiki = p_test.app_instance('wiki')
-    with h.push_context(p_test._id, app_config_id=wiki.config._id):
-        p = WM.Page.upsert(title='Include_1')
-        p.text = 'included page 1'
-        p.commit()
-        p = WM.Page.upsert(title='Include_2')
-        p.text = 'included page 2'
-        p.commit()
-        p = WM.Page.upsert(title='Include_3')
-        p.text = 'included page 3'
-        p.commit()
-        ThreadLocalORMSession.flush_all()
-        md = '[[include ref=Include_1]]\n[[include ref=Include_2]]\n[[include ref=Include_3]]'
-        html = g.markdown_wiki.convert(md)
-
-    expected_html = '''<div class="markdown_content"><p></p><div>
-<div class="markdown_content"><p>included page 1</p></div>
-</div>
-<div>
-<div class="markdown_content"><p>included page 2</p></div>
-</div>
-<div>
-<div class="markdown_content"><p>included page 3</p></div>
-</div>
-<p></p></div>'''
-    assert squish_spaces(html) == squish_spaces(expected_html)
-
-
-@with_setup(setup_method, teadown_method)
-@td.with_wiki
-@td.with_tool('test', 'Wiki', 'wiki2')
-def test_macro_include_permissions():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
-    wiki = p_test.app_instance('wiki')
-    wiki2 = p_test.app_instance('wiki2')
-    with h.push_context(p_test._id, app_config_id=wiki.config._id):
-        p = WM.Page.upsert(title='CanRead')
-        p.text = 'Can see this!'
-        p.commit()
-        ThreadLocalORMSession.flush_all()
-
-    with h.push_context(p_test._id, app_config_id=wiki2.config._id):
-        role = M.ProjectRole.by_name('*anonymous')._id
-        read_perm = M.ACE.allow(role, 'read')
-        acl = c.app.config.acl
-        if read_perm in acl:
-            acl.remove(read_perm)
-        p = WM.Page.upsert(title='CanNotRead')
-        p.text = 'Can not see this!'
-        p.commit()
-        ThreadLocalORMSession.flush_all()
-
-    with h.push_context(p_test._id, app_config_id=wiki.config._id):
-        c.user = M.User.anonymous()
-        md = '[[include ref=CanRead]]\n[[include ref=wiki2:CanNotRead]]'
-        html = g.markdown_wiki.convert(md)
-        assert 'Can see this!' in html
-        assert 'Can not see this!' not in html
-        assert "[[include: you don't have a read permission for wiki2:CanNotRead]]" in html
-
-
-@patch('oembed.OEmbedEndpoint.fetch')
-def test_macro_embed(oembed_fetch):
-    oembed_fetch.return_value = {
-        "html": '<iframe width="480" height="270" src="http://www.youtube.com/embed/kOLpSPEA72U?feature=oembed" '
-                'frameborder="0" allowfullscreen></iframe>)',
-        "title": "Nature's 3D Printer: MIND BLOWING Cocoon in Rainforest - Smarter Every Day 94",
-    }
-    r = g.markdown_wiki.convert('[[embed url=http://www.youtube.com/watch?v=kOLpSPEA72U]]')
-    assert ('<p><iframe height="270" '
-              'src="https://www.youtube-nocookie.com/embed/kOLpSPEA72U?feature=oembed" width="480"></iframe></p>' in
-              r.replace('\n', ''))
-
-
-def test_macro_embed_video_gone():
-    # this does a real fetch
-    r = g.markdown_wiki.convert('[[embed url=https://www.youtube.com/watch?v=OWsFqPZ3v-0]]')
-    r = str(r)  # convert away from Markup, to get better assertion diff output
-    # either of these could happen depending on the mood of youtube's oembed API:
-    assert r in [
-        '<div class="markdown_content"><p>Video not available</p></div>',
-        '<div class="markdown_content"><p>Could not embed: https://www.youtube.com/watch?v=OWsFqPZ3v-0</p></div>',
-    ]
-
-
-@patch('oembed.OEmbedEndpoint.fetch')
-def test_macro_embed_video_error(oembed_fetch):
-    oembed_fetch.side_effect = OEmbedError('Invalid mime-type in response...')
-    r = g.markdown_wiki.convert('[[embed url=http://www.youtube.com/watch?v=6YbBmqUnoQM]]')
-    assert (r == '<div class="markdown_content"><p>Could not embed: '
-                    'http://www.youtube.com/watch?v=6YbBmqUnoQM</p></div>')
-
-
-def test_macro_embed_notsupported():
-    r = g.markdown_wiki.convert('[[embed url=http://vimeo.com/46163090]]')
-    assert (
-        r == '<div class="markdown_content"><p>[[embed url=http://vimeo.com/46163090]]</p></div>')
-
-
-def test_markdown_toc():
-    with h.push_context('test', neighborhood='Projects'):
-        r = g.markdown_wiki.convert("""[TOC]
-
-# Header 1
-
-## Header 2""")
-    assert '''<ul>
-<li><a href="#header-1">Header 1</a><ul>
-<li><a href="#header-2">Header 2</a></li>
-</ul>
-</li>
-</ul>''' in r, r
-
-
-@td.with_wiki
-def test_wiki_artifact_links():
-    text = g.markdown.convert('See [18:13:49]')
-    assert 'See <span>[18:13:49]</span>' in text, text
-    with h.push_context('test', 'wiki', neighborhood='Projects'):
-        text = g.markdown.convert('Read [here](Home) about our project')
-        assert '<a class="" href="/p/test/wiki/Home/">here</a>' in text, text
-        text = g.markdown.convert('[Go home](test:wiki:Home)')
-        assert '<a class="" href="/p/test/wiki/Home/">Go home</a>' in text, text
-        text = g.markdown.convert('See [test:wiki:Home]')
-        assert '<a class="alink" href="/p/test/wiki/Home/">[test:wiki:Home]</a>' in text, text
-
-
-def test_markdown_links():
-    with patch.dict(tg.config, {'nofollow_exempt_domains': 'foobar.net'}):
-        text = g.markdown.convert('Read [here](http://foobar.net/) about our project')
-        assert 'class="" href="http://foobar.net/">here</a> about' in text
-
-    text = g.markdown.convert('Read [here](http://foobar.net/) about our project')
-    assert 'class="" href="http://foobar.net/" rel="nofollow">here</a> about' in text
-
-    text = g.markdown.convert('Read [here](/p/foobar/blah) about our project')
-    assert 'class="" href="/p/foobar/blah">here</a> about' in text
-
-    text = g.markdown.convert('Read [here](/p/foobar/blah/) about our project')
-    assert 'class="" href="/p/foobar/blah/">here</a> about' in text
-
-    text = g.markdown.convert('Read <http://foobar.net/> about our project')
-    assert 'href="http://foobar.net/" rel="nofollow">http://foobar.net/</a> about' in text
-
-
-def test_markdown_and_html():
-    with h.push_context('test', neighborhood='Projects'):
-        r = g.markdown_wiki.convert('<div style="float:left">blah</div>')
-    assert '<div style="float: left;">blah</div>' in r, r
-
-
-def test_markdown_within_html():
-    with h.push_context('test', neighborhood='Projects'):
-        r = g.markdown_wiki.convert('<div style="float:left" markdown>**blah**</div>')
-    assert ('<div style="float: left;"><p><strong>blah</strong></p></div>' in
-              r.replace('\n', ''))
-
-
-def test_markdown_with_html_comments():
-    text = g.markdown.convert('test <!-- comment -->')
-    assert '<div class="markdown_content"><p>test </p></div>' == text, text
-
-
-def test_markdown_big_text():
-    '''If text is too big g.markdown.convert should return plain text'''
-    text = 'a' * 40001
-    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():
-    with h.push_context('test', 'wiki', neighborhood='Projects'):
-        text = g.markdown.convert('# Foo!\n[Home]')
-        assert (text ==
-                     '<div class="markdown_content"><h1 id="foo">Foo!</h1>\n'
-                     '<p><a class="alink" href="/p/test/wiki/Home/">[Home]</a></p></div>')
-        text = g.markdown.convert('# Foo!\n[Rooted]')
-        assert (text ==
-                     '<div class="markdown_content"><h1 id="foo">Foo!</h1>\n'
-                     '<p><span>[Rooted]</span></p></div>')
-
-    assert (
-        g.markdown.convert('Multi\nLine') ==
-        '<div class="markdown_content"><p>Multi<br/>\n'
-        'Line</p></div>')
-    assert (
-        g.markdown.convert('Multi\n\nLine') ==
-        '<div class="markdown_content"><p>Multi</p>\n'
-        '<p>Line</p></div>')
-
-    # should not raise an exception:
-    assert (
-        g.markdown.convert("<class 'foo'>") ==
-        '''<div class="markdown_content"><p>&lt;class 'foo'=""&gt;&lt;/class&gt;</p></div>''')
-
-    assert (
-        g.markdown.convert('''# Header
-
-Some text in a regular paragraph
-
-    :::python
-    for i in range(10):
-        print i
-''') ==
-        # no <br
-        '<div class="markdown_content"><h1 id="header">Header</h1>\n'
-        '<p>Some text in a regular paragraph</p>\n'
-        '<div class="codehilite"><pre><span></span><code><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>\n'
-        '    <span class="nb">print</span> <span class="n">i</span>\n'
-        '</code></pre></div>\n'
-        '</div>')
-    assert (
-        g.forge_markdown(email=True).convert('[Home]') ==
-        # uses localhost:
-        '<div class="markdown_content"><p><a class="alink" href="http://localhost/p/test/wiki/Home/">[Home]</a></p></div>')
-    assert (
-        g.markdown.convert('''
-~~~~
-def foo(): pass
-~~~~''') ==
-        '<div class="markdown_content"><div class="codehilite"><pre><span></span><code>def foo(): pass\n'
-        '</code></pre></div>\n'
-        '</div>')
-
-
-def test_markdown_list_without_break():
-    # this is not a valid way to make a list in original Markdown or python-markdown
-    #   https://github.com/Python-Markdown/markdown/issues/874
-    # it is valid in the CommonMark spec https://spec.commonmark.org/0.30/#lists
-    # TODO: try https://github.com/adamb70/mdx-breakless-lists
-    #       or https://gitlab.com/ayblaq/prependnewline
-    assert (
-        g.markdown.convert('''\
-Regular text
-* first item
-* second item''') ==
-        '<div class="markdown_content"><p>Regular text\n'  # no <br>
-        '* first item\n'  # no <br>
-        '* second item</p></div>')
-
-    assert (
-        g.markdown.convert('''\
-Regular text
-- first item
-- second item''') ==
-        '<div class="markdown_content"><p>Regular text<br/>\n'
-        '- first item<br/>\n'
-        '- second item</p></div>')
-
-    assert (
-        g.markdown.convert('''\
-Regular text
-+ first item
-+ second item''') ==
-        '<div class="markdown_content"><p>Regular text<br/>\n'
-        '+ first item<br/>\n'
-        '+ second item</p></div>')
-
-    assert (
-        g.markdown.convert('''\
-Regular text
-1. first item
-2. second item''') ==
-        '<div class="markdown_content"><p>Regular text<br/>\n'
-        '1. first item<br/>\n'
-        '2. second item</p></div>')
-
-
-def test_markdown_autolink():
-    tgt = 'http://everything2.com/?node=nate+oostendorp'
-    s = g.markdown.convert('This is %s' % tgt)
-    assert (
-        s == f'<div class="markdown_content"><p>This is <a href="{tgt}" rel="nofollow">{tgt}</a></p></div>')
-    assert '<a href=' in g.markdown.convert('This is http://domain.net')
-    # beginning of doc
-    assert '<a href=' in g.markdown.convert('http://domain.net abc')
-    # beginning of a line
-    assert ('<br/>\n<a href="http://' in
-              g.markdown.convert('foobar\nhttp://domain.net abc'))
-    # no conversion of these urls:
-    assert ('a blahttp://sdf.com z' in
-              g.markdown.convert('a blahttp://sdf.com z'))
-    assert ('literal <code>http://domain.net</code> literal' in
-              g.markdown.convert('literal `http://domain.net` literal'))
-    assert ('<pre><span></span><code>preformatted http://domain.net\n</code></pre>' in
-              g.markdown.convert('    :::text\n'
-                                 '    preformatted http://domain.net'))
-
-
-def test_markdown_autolink_with_escape():
-    # \_ is unnecessary but valid markdown escaping and should be considered as a regular underscore
-    # (it occurs during html2text conversion during project migrations)
-    r = g.markdown.convert(r'a http://www.phpmyadmin.net/home\_page/security/\#target b')
-    assert 'href="http://www.phpmyadmin.net/home_page/security/#target"' in r, r
-
-
-def test_markdown_invalid_script():
-    r = g.markdown.convert('<script>alert(document.cookies)</script>')
-    assert '<div class="markdown_content">&lt;script&gt;alert(document.cookies)&lt;/script&gt;\n</div>' == r
-
-
-def test_markdown_invalid_onerror():
-    r = g.markdown.convert('<img src=x onerror=alert(document.cookie)>')
-    assert 'onerror' not in r
-
-
-def test_markdown_invalid_tagslash():
-    r = g.markdown.convert('<div/onload><img src=x onerror=alert(document.cookie)>')
-    assert 'onerror' not in r
-
-
-def test_markdown_invalid_script_in_link():
-    r = g.markdown.convert('[xss](http://"><a onmouseover=prompt(document.domain)>xss</a>)')
-    assert ('<div class="markdown_content"><p><a class="" '
-                 '''href='http://"&gt;&lt;a%20onmouseover=prompt(document.domain)&gt;xss&lt;/a&gt;' '''
-                 'rel="nofollow">xss</a></p></div>' == r)
-
-
-def test_markdown_invalid_script_in_link2():
-    r = g.markdown.convert('[xss](http://"><img src=x onerror=alert(document.cookie)>)')
-    assert ('<div class="markdown_content"><p><a class="" '
-                 '''href='http://"&gt;&lt;img%20src=x%20onerror=alert(document.cookie)&gt;' '''
-                 'rel="nofollow">xss</a></p></div>' == r)
-
-
-def test_markdown_extremely_slow():
-    r = g.markdown.convert('''bonjour, voila ce que j'obtient en voulant ajouter un utilisateur a un groupe de sécurite, que ce soit sur un groupe pre-existant, ou sur un groupe crée.
-message d'erreur:
-
-ERROR: Could not complete the Add UserLogin To SecurityGroup [file:/C:/neogia/ofbizNeogia/applications/securityext/script/org/ofbiz/securityext/securitygroup/SecurityGroupServices.xml#addUserLoginToSecurityGroup] process [problem creating the newEntity value: Exception while inserting the following entity: [GenericEntity:UserLoginSecurityGroup][createdStamp,2006-01-23 17:42:39.312(java.sql.Timestamp)][createdTxStamp,2006-01-23 17:42:38.875(java.sql.Timestamp)][fromDate,2006-01-23 17:42:3 [...]
-
-à priori les données du formulaire ne sont pas traitées : VALUES (?, ?, ?, ?, ?, ?, ?, ?) ce qui entraine l'echec du traitement SQL.
-
-
-Si une idée vous vient à l'esprit, merci de me tenir au courant.
-
-cordialement, julien.''')
-    assert True   # finished!
-
-
-@td.with_tool('test', 'Wiki', 'wiki-len')
-def test_markdown_link_length_limits():
-    with h.push_context('test', 'wiki-len', neighborhood='Projects'):
-        # these are always ok, no matter the NOBRACKET length
-        WM.Page.upsert(title='12345678901').commit()
-        text = g.markdown.convert('See [12345678901]')
-        assert 'href="/p/test/wiki-len/12345678901/">[12345678901]</a>' in text, text
-        WM.Page.upsert(title='this is 26 characters long').commit()
-        text = g.markdown.convert('See [this is 26 characters long]')
-        assert 'href="/p/test/wiki-len/this%20is%2026%20characters%20long/">[this is 26 characters long]</a>' in text, text
-
-        # NOBRACKET regex length impacts standard markdown links
-        text = g.markdown.convert('See [short](http://a.de)')
-        assert 'href="http://a.de" rel="nofollow">short</a>' in text, text
-        text = g.markdown.convert('See [this is 26 characters long](http://a.de)')
-        assert 'href="http://a.de" rel="nofollow">this is 26 characters long</a>' in text, text  # {0,12} fails {0,13} ok
-
-        # NOBRACKET regex length impacts our custom artifact links
-        text = g.markdown.convert('See [short](Home)')
-        assert 'href="/p/test/wiki-len/Home/">short</a>' in text, text
-        text = g.markdown.convert('See [123456789](Home)')
-        assert 'href="/p/test/wiki-len/Home/">123456789</a>' in text, text
-        text = g.markdown.convert('See [12345678901](Home)')
-        assert 'href="/p/test/wiki-len/Home/">12345678901</a>' in text, text  # {0,5} fails, {0,6} ok
-        text = g.markdown.convert('See [this is 16 chars](Home)')
-        assert 'href="/p/test/wiki-len/Home/">this is 16 chars</a>' in text, text  # {0,7} fails {0,8} ok
-        text = g.markdown.convert('See [this is 26 characters long](Home)')
-        assert 'href="/p/test/wiki-len/Home/">this is 26 characters long</a>' in text, text  # {0,12} fails {0,13} ok
-
-        # limit, currently
-        charSuperLong = '1234567890'*21
-        text = g.markdown.convert(f'See [{charSuperLong}](Home)')
-        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():
-    r = g.markdown.convert('[[include ref=Home id=foo]]')
-    assert '<div id="foo">' in r, r
-    assert 'href="../foo"' in g.markdown.convert('[My foo](foo)')
-    assert 'href="..' not in g.markdown.convert('[My foo](./foo)')
-
-
-def test_macro_nbhd_feeds():
-    with h.push_context('--init--', 'wiki', neighborhood='Projects'):
-        r = g.markdown_wiki.convert('[[neighborhood_feeds tool_name=wiki]]')
-        assert 'Home modified by ' in r, r
-        assert '&lt;div class="markdown_content"&gt;' not in r
-
-
-def test_sort_alpha():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-
-    with h.push_context(p_nbhd.neighborhood_project._id):
-        r = g.markdown_wiki.convert('[[projects sort=alpha]]')
-        project_list = get_project_names(r)
-        assert project_list == sorted(project_list)
-
-
-def test_sort_registered():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-
-    with h.push_context(p_nbhd.neighborhood_project._id):
-        r = g.markdown_wiki.convert('[[projects sort=last_registered]]')
-        project_names = get_project_names(r)
-        ids = get_projects_property_in_the_same_order(project_names, '_id')
-        assert ids == sorted(ids, reverse=True)
-
-
-def test_sort_updated():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-
-    with h.push_context(p_nbhd.neighborhood_project._id):
-        r = g.markdown_wiki.convert('[[projects sort=last_updated]]')
-        project_names = get_project_names(r)
-        updated_at = get_projects_property_in_the_same_order(
-            project_names, 'last_updated')
-        assert updated_at == sorted(updated_at, reverse=True)
-
-
-@with_setup(setup_functional_test)
-def test_filtering():
-    # set up for test
-    from random import choice
-    setup_trove_categories()
-    random_trove = choice(M.TroveCategory.query.find().all())
-    test_project = M.Project.query.get(shortname='test')
-    test_project_troves = getattr(test_project, 'trove_' + random_trove.type)
-    test_project_troves.append(random_trove._id)
-    ThreadLocalORMSession.flush_all()
-
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    with h.push_config(c,
-                       project=p_nbhd.neighborhood_project,
-                       user=M.User.by_username('test-admin')):
-        r = g.markdown_wiki.convert(
-            '[[projects category="%s"]]' % random_trove.fullpath)
-        project_names = get_project_names(r)
-        assert [test_project.name] == project_names
-
-
-def test_projects_macro():
-    two_column_style = 'width: 330px;'
-
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-    with h.push_config(c,
-                       project=p_nbhd.neighborhood_project,
-                       user=M.User.anonymous()):
-        # test columns
-        r = g.markdown_wiki.convert('[[projects display_mode=list columns=2]]')
-        assert two_column_style in r
-        r = g.markdown_wiki.convert('[[projects display_mode=list columns=3]]')
-        assert two_column_style not in r
-
-
-@td.with_user_project('test-admin')
-@td.with_user_project('test-user-1')
-def test_myprojects_macro():
-    h.set_context('u/%s' % (c.user.username), 'wiki', neighborhood='Users')
-    r = g.markdown_wiki.convert('[[my_projects]]')
-    for p in c.user.my_projects():
-        if p.deleted or p.is_nbhd_project:
-            continue
-        proj_title = f'<h2><a href="{p.url()}">{p.name}</a></h2>'
-        assert proj_title in r
-
-    h.set_context('u/test-user-1', 'wiki', neighborhood='Users')
-    user = M.User.query.get(username='test-user-1')
-    r = g.markdown_wiki.convert('[[my_projects]]')
-    for p in user.my_projects():
-        if p.deleted or p.is_nbhd_project:
-            continue
-        proj_title = f'<h2><a href="{p.url()}">{p.name}</a></h2>'
-        assert proj_title in r
-
-
-@td.with_wiki
-def test_hideawards_macro():
-    p_nbhd = M.Neighborhood.query.get(name='Projects')
-
-    app_config_id = ObjectId()
-    award = M.Award(app_config_id=app_config_id)
-    award.short = 'Award short'
-    award.full = 'Award full'
-    award.created_by_neighborhood_id = p_nbhd._id
-
-    project = M.Project.query.get(
-        neighborhood_id=p_nbhd._id, shortname='test')
-
-    M.AwardGrant(
-        award=award,
-        award_url='http://award.org',
-        comment='Winner!',
-        granted_by_neighborhood=p_nbhd,
-        granted_to_project=project)
-
-    ThreadLocalORMSession.flush_all()
-
-    with h.push_context(p_nbhd.neighborhood_project._id):
-        r = g.markdown_wiki.convert('[[projects]]')
-        assert ('<div class="feature"> <a href="http://award.org" rel="nofollow" title="Winner!">'
-                  'Award short</a> </div>' in
-                  squish_spaces(r))
-
-        r = g.markdown_wiki.convert('[[projects show_awards_banner=False]]')
-        assert 'Award short' not in r
-
-
-@td.with_tool('test', 'Blog', 'blog')
-def test_project_blog_posts_macro():
-    from forgeblog import model as BM
-    with h.push_context('test', 'blog', neighborhood='Projects'):
-        BM.BlogPost.new(
-            title='Test title',
-            text='test post',
-            state='published',
-        )
-        BM.BlogPost.new(
-            title='Test title2',
-            text='test post2',
-            state='published',
-        )
-
-        r = g.markdown_wiki.convert('[[project_blog_posts]]')
-        assert 'Test title</a></h3>' in r
-        assert 'Test title2</a></h3>' in r
-        assert '<div class="markdown_content"><p>test post</p></div>' in r
-        assert '<div class="markdown_content"><p>test post2</p></div>' in r
-        assert 'by <em>Test Admin</em>' in r
-
-
-def test_project_screenshots_macro():
-    with h.push_context('test', neighborhood='Projects'):
-        M.ProjectFile(project_id=c.project._id, category='screenshot', caption='caption', filename='test_file.jpg')
-        ThreadLocalORMSession.flush_all()
 
-        r = g.markdown_wiki.convert('[[project_screenshots]]')
 
-        assert 'href="/p/test/screenshot/test_file.jpg"' in r
-        assert 'src="/p/test/screenshot/test_file.jpg/thumb"' in r
+def squish_spaces(text):
+    # \s is whitespace
+    # \xa0 is &nbsp; in unicode form
+    return re.sub(r'[\s\xa0]+', ' ', text)
 
 
 def get_project_names(r):
@@ -826,10 +81,701 @@ def get_projects_property_in_the_same_order(names, prop):
     return [projects_dict[name] for name in names]
 
 
+@with_nose_compatibility
+class Test():
+
+    def setup_method(self, method):
+        setup_global_objects()
+
+    @td.with_wiki
+    def test_app_globals(self):
+        with h.push_context('test', 'wiki', neighborhood='Projects'):
+            assert g.app_static(
+                'css/wiki.css') == '/nf/_static_/wiki/css/wiki.css', g.app_static('css/wiki.css')
+
+    def test_macro_projects(self):
+        file_name = 'neo-icon-set-454545-256x350.png'
+        file_path = os.path.join(
+            allura.__path__[0], 'nf', 'allura', 'images', file_name)
+
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        c.project = p_test
+        icon_file = open(file_path, 'rb')
+        M.ProjectFile.save_image(
+            file_name, icon_file, content_type='image/png',
+            square=True, thumbnail_size=(48, 48),
+            thumbnail_meta=dict(project_id=c.project._id, category='icon'))
+        icon_file.close()
+        p_test2 = M.Project.query.get(
+            shortname='test2', neighborhood_id=p_nbhd._id)
+        c.project = p_test2
+        icon_file = open(file_path, 'rb')
+        M.ProjectFile.save_image(
+            file_name, icon_file, content_type='image/png',
+            square=True, thumbnail_size=(48, 48),
+            thumbnail_meta=dict(project_id=c.project._id, category='icon'))
+        icon_file.close()
+        p_sub1 = M.Project.query.get(
+            shortname='test/sub1', neighborhood_id=p_nbhd._id)
+        c.project = p_sub1
+        icon_file = open(file_path, 'rb')
+        M.ProjectFile.save_image(
+            file_name, icon_file, content_type='image/png',
+            square=True, thumbnail_size=(48, 48),
+            thumbnail_meta=dict(project_id=c.project._id, category='icon'))
+        icon_file.close()
+        p_test.labels = ['test', 'root']
+        p_sub1.labels = ['test', 'sub1']
+        # Make one project private
+        p_test.private = False
+        p_sub1.private = False
+        p_test2.private = True
+
+        ThreadLocalORMSession.flush_all()
+
+        with h.push_config(c,
+                        project=p_nbhd.neighborhood_project,
+                        user=M.User.by_username('test-admin')):
+            r = g.markdown_wiki.convert('[[projects]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' in r, r
+            r = g.markdown_wiki.convert('[[projects labels=root]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' not in r, r
+            r = g.markdown_wiki.convert('[[projects labels=sub1]]')
+            assert 'alt="Test Project Logo"' not in r, r
+            assert 'alt="A Subproject Logo"' in r, r
+            r = g.markdown_wiki.convert('[[projects labels=test]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' in r, r
+            r = g.markdown_wiki.convert('[[projects labels=test,root]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' not in r, r
+            r = g.markdown_wiki.convert('[[projects labels=test,sub1]]')
+            assert 'alt="Test Project Logo"' not in r, r
+            assert 'alt="A Subproject Logo"' in r, r
+            r = g.markdown_wiki.convert('[[projects labels=root|sub1]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' in r, r
+            r = g.markdown_wiki.convert('[[projects labels=test,root|root,sub1]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' not in r, r
+            r = g.markdown_wiki.convert('[[projects labels=test,root|test,sub1]]')
+            assert 'alt="Test Project Logo"' in r, r
+            assert 'alt="A Subproject Logo"' in r, r
+            r = g.markdown_wiki.convert('[[projects show_total=True sort=random]]')
+            assert '<p class="macro_projects_total">3 Projects' in r, r
+            r = g.markdown_wiki.convert(
+                '[[projects show_total=True private=True sort=random]]')
+            assert '<p class="macro_projects_total">1 Projects' in r, r
+            assert 'alt="Test 2 Logo"' in r, r
+            assert 'alt="Test Project Logo"' not in r, r
+            assert 'alt="A Subproject Logo"' not in r, r
+
+            r = g.markdown_wiki.convert('[[projects show_proj_icon=True]]')
+            assert 'alt="Test Project Logo"' in r
+            r = g.markdown_wiki.convert('[[projects show_proj_icon=False]]')
+            assert 'alt="Test Project Logo"' not in r
+
+    def test_macro_neighborhood_feeds(self):
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        with h.push_context('--init--', 'wiki', neighborhood='Projects'):
+            r = g.markdown_wiki.convert('[[neighborhood_feeds tool_name=wiki]]')
+            assert 'Home modified by' in r, r
+            r = re.sub(r'<small>.*? ago</small>', '', r)  # remove "less than 1 second ago" etc
+            orig_len = len(r)
+            # Make project private & verify we don't see its new feed items
+            anon = M.User.anonymous()
+            p_test.acl.insert(0, M.ACE.deny(
+                M.ProjectRole.anonymous(p_test)._id, 'read'))
+            ThreadLocalORMSession.flush_all()
+            pg = WM.Page.query.get(title='Home', app_config_id=c.app.config._id)
+            pg.text = 'Change'
+            with h.push_config(c, user=M.User.by_username('test-admin')):
+                pg.commit()
+            r = g.markdown_wiki.convert('[[neighborhood_feeds tool_name=wiki]]')
+            r = re.sub(r'<small>.*? ago</small>', '', r)  # remove "less than 1 second ago" etc
+            new_len = len(r)
+            assert new_len == orig_len
+            p = BM.BlogPost(title='test me',
+                            neighborhood_id=p_test.neighborhood_id)
+            p.text = 'test content'
+            p.state = 'published'
+            p.make_slug()
+            with h.push_config(c, user=M.User.by_username('test-admin')):
+                p.commit()
+            ThreadLocalORMSession.flush_all()
+            with h.push_config(c, user=anon):
+                r = g.markdown_wiki.convert('[[neighborhood_blog_posts]]')
+            assert 'test content' in r
+
+    def test_macro_members(self):
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        p_test.add_user(M.User.by_username('test-user'), ['Developer'])
+        p_test.add_user(M.User.by_username('test-user-0'), ['Member'])
+        ThreadLocalORMSession.flush_all()
+        r = g.markdown_wiki.convert('[[members limit=2]]').replace('\t', '').replace('\n', '')
+        assert (r ==
+                    '<div class="markdown_content"><h6>Project Members:</h6>'
+                    '<ul class="md-users-list">'
+                    '<li><a href="/u/test-admin/">Test Admin</a> (admin)</li>'
+                    '<li><a href="/u/test-user/">Test User</a></li>'
+                    '<li class="md-users-list-more"><a href="/p/test/_members">All Members</a></li>'
+                    '</ul>'
+                    '</div>')
+
+    def test_macro_members_escaping(self):
+        user = M.User.by_username('test-admin')
+        user.display_name = 'Test Admin <script>'
+        r = g.markdown_wiki.convert('[[members]]')
+        assert (r.replace('\n', '').replace('\t', '') ==
+                    '<div class="markdown_content"><h6>Project Members:</h6>'
+                    '<ul class="md-users-list">'
+                    '<li><a href="/u/test-admin/">Test Admin &lt;script&gt;</a> (admin)</li>'
+                    '</ul></div>')
+
+    def test_macro_project_admins(self):
+        user = M.User.by_username('test-admin')
+        user.display_name = 'Test Ådmin <script>'
+        with h.push_context('test', neighborhood='Projects'):
+            r = g.markdown_wiki.convert('[[project_admins]]')
+        assert (r.replace('\n', '') ==
+                    '<div class="markdown_content"><h6>Project Admins:</h6>'
+                    '<ul class="md-users-list">'
+                    '    <li><a href="/u/test-admin/">Test \xc5dmin &lt;script&gt;</a></li>'
+                    '</ul></div>')
+
+    def test_macro_project_admins_one_br(self):
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        p_test.add_user(M.User.by_username('test-user'), ['Admin'])
+        ThreadLocalORMSession.flush_all()
+        with h.push_config(c, project=p_test):
+            r = g.markdown_wiki.convert('[[project_admins]]\n[[download_button]]')
+
+        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)
+        wiki = p_test.app_instance('wiki')
+        with h.push_context(p_test._id, app_config_id=wiki.config._id):
+            p = WM.Page.upsert(title='Include_1')
+            p.text = 'included page 1'
+            p.commit()
+            p = WM.Page.upsert(title='Include_2')
+            p.text = 'included page 2'
+            p.commit()
+            p = WM.Page.upsert(title='Include_3')
+            p.text = 'included page 3'
+            p.commit()
+            ThreadLocalORMSession.flush_all()
+            md = '[[include ref=Include_1]]\n[[include ref=Include_2]]\n[[include ref=Include_3]]'
+            html = g.markdown_wiki.convert(md)
+
+        expected_html = '''<div class="markdown_content"><p></p><div>
+    <div class="markdown_content"><p>included page 1</p></div>
+    </div>
+    <div>
+    <div class="markdown_content"><p>included page 2</p></div>
+    </div>
+    <div>
+    <div class="markdown_content"><p>included page 3</p></div>
+    </div>
+    <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')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        wiki = p_test.app_instance('wiki')
+        wiki2 = p_test.app_instance('wiki2')
+        with h.push_context(p_test._id, app_config_id=wiki.config._id):
+            p = WM.Page.upsert(title='CanRead')
+            p.text = 'Can see this!'
+            p.commit()
+            ThreadLocalORMSession.flush_all()
+
+        with h.push_context(p_test._id, app_config_id=wiki2.config._id):
+            role = M.ProjectRole.by_name('*anonymous')._id
+            read_perm = M.ACE.allow(role, 'read')
+            acl = c.app.config.acl
+            if read_perm in acl:
+                acl.remove(read_perm)
+            p = WM.Page.upsert(title='CanNotRead')
+            p.text = 'Can not see this!'
+            p.commit()
+            ThreadLocalORMSession.flush_all()
+
+        with h.push_context(p_test._id, app_config_id=wiki.config._id):
+            c.user = M.User.anonymous()
+            md = '[[include ref=CanRead]]\n[[include ref=wiki2:CanNotRead]]'
+            html = g.markdown_wiki.convert(md)
+            assert 'Can see this!' in html
+            assert 'Can not see this!' not in html
+            assert "[[include: you don't have a read permission for wiki2:CanNotRead]]" in html
+
+    @patch('oembed.OEmbedEndpoint.fetch')
+    def test_macro_embed(self, oembed_fetch):
+        oembed_fetch.return_value = {
+            "html": '<iframe width="480" height="270" src="http://www.youtube.com/embed/kOLpSPEA72U?feature=oembed" '
+                    'frameborder="0" allowfullscreen></iframe>)',
+            "title": "Nature's 3D Printer: MIND BLOWING Cocoon in Rainforest - Smarter Every Day 94",
+        }
+        r = g.markdown_wiki.convert('[[embed url=http://www.youtube.com/watch?v=kOLpSPEA72U]]')
+        assert ('<p><iframe height="270" '
+                'src="https://www.youtube-nocookie.com/embed/kOLpSPEA72U?feature=oembed" width="480"></iframe></p>' in
+                r.replace('\n', ''))
+
+    def test_macro_embed_video_gone(self):
+        # this does a real fetch
+        r = g.markdown_wiki.convert('[[embed url=https://www.youtube.com/watch?v=OWsFqPZ3v-0]]')
+        r = str(r)  # convert away from Markup, to get better assertion diff output
+        # either of these could happen depending on the mood of youtube's oembed API:
+        assert r in [
+            '<div class="markdown_content"><p>Video not available</p></div>',
+            '<div class="markdown_content"><p>Could not embed: https://www.youtube.com/watch?v=OWsFqPZ3v-0</p></div>',
+        ]
+
+    @patch('oembed.OEmbedEndpoint.fetch')
+    def test_macro_embed_video_error(self, oembed_fetch):
+        oembed_fetch.side_effect = OEmbedError('Invalid mime-type in response...')
+        r = g.markdown_wiki.convert('[[embed url=http://www.youtube.com/watch?v=6YbBmqUnoQM]]')
+        assert (r == '<div class="markdown_content"><p>Could not embed: '
+                        'http://www.youtube.com/watch?v=6YbBmqUnoQM</p></div>')
+
+    def test_macro_embed_notsupported(self):
+        r = g.markdown_wiki.convert('[[embed url=http://vimeo.com/46163090]]')
+        assert (
+            r == '<div class="markdown_content"><p>[[embed url=http://vimeo.com/46163090]]</p></div>')
+
+    def test_markdown_toc(self):
+        with h.push_context('test', neighborhood='Projects'):
+            r = g.markdown_wiki.convert("""[TOC]
+
+    # Header 1
+
+    ## Header 2""")
+        assert '''<ul>
+    <li><a href="#header-1">Header 1</a><ul>
+    <li><a href="#header-2">Header 2</a></li>
+    </ul>
+    </li>
+    </ul>''' in r, 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
+        with h.push_context('test', 'wiki', neighborhood='Projects'):
+            text = g.markdown.convert('Read [here](Home) about our project')
+            assert '<a class="" href="/p/test/wiki/Home/">here</a>' in text, text
+            text = g.markdown.convert('[Go home](test:wiki:Home)')
+            assert '<a class="" href="/p/test/wiki/Home/">Go home</a>' in text, text
+            text = g.markdown.convert('See [test:wiki:Home]')
+            assert '<a class="alink" href="/p/test/wiki/Home/">[test:wiki:Home]</a>' in text, text
+
+    def test_markdown_links(self):
+        with patch.dict(tg.config, {'nofollow_exempt_domains': 'foobar.net'}):
+            text = g.markdown.convert('Read [here](http://foobar.net/) about our project')
+            assert 'class="" href="http://foobar.net/">here</a> about' in text
+
+        text = g.markdown.convert('Read [here](http://foobar.net/) about our project')
+        assert 'class="" href="http://foobar.net/" rel="nofollow">here</a> about' in text
+
+        text = g.markdown.convert('Read [here](/p/foobar/blah) about our project')
+        assert 'class="" href="/p/foobar/blah">here</a> about' in text
+
+        text = g.markdown.convert('Read [here](/p/foobar/blah/) about our project')
+        assert 'class="" href="/p/foobar/blah/">here</a> about' in text
+
+        text = g.markdown.convert('Read <http://foobar.net/> about our project')
+        assert 'href="http://foobar.net/" rel="nofollow">http://foobar.net/</a> about' in text
+
+    def test_markdown_and_html(self):
+        with h.push_context('test', neighborhood='Projects'):
+            r = g.markdown_wiki.convert('<div style="float:left">blah</div>')
+        assert '<div style="float: left;">blah</div>' in r, r
+
+    def test_markdown_within_html(self):
+        with h.push_context('test', neighborhood='Projects'):
+            r = g.markdown_wiki.convert('<div style="float:left" markdown>**blah**</div>')
+        assert ('<div style="float: left;"><p><strong>blah</strong></p></div>' in
+                r.replace('\n', ''))
+
+    def test_markdown_with_html_comments(self):
+        text = g.markdown.convert('test <!-- comment -->')
+        assert '<div class="markdown_content"><p>test </p></div>' == text, text
+
+    def test_markdown_big_text(self):
+        '''If text is too big g.markdown.convert should return plain text'''
+        text = 'a' * 40001
+        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]')
+            assert (text ==
+                        '<div class="markdown_content"><h1 id="foo">Foo!</h1>\n'
+                        '<p><a class="alink" href="/p/test/wiki/Home/">[Home]</a></p></div>')
+            text = g.markdown.convert('# Foo!\n[Rooted]')
+            assert (text ==
+                        '<div class="markdown_content"><h1 id="foo">Foo!</h1>\n'
+                        '<p><span>[Rooted]</span></p></div>')
+
+        assert (
+            g.markdown.convert('Multi\nLine') ==
+            '<div class="markdown_content"><p>Multi<br/>\n'
+            'Line</p></div>')
+        assert (
+            g.markdown.convert('Multi\n\nLine') ==
+            '<div class="markdown_content"><p>Multi</p>\n'
+            '<p>Line</p></div>')
+
+        # should not raise an exception:
+        assert (
+            g.markdown.convert("<class 'foo'>") ==
+            '''<div class="markdown_content"><p>&lt;class 'foo'=""&gt;&lt;/class&gt;</p></div>''')
+
+        assert (
+            g.markdown.convert('''# Header
+
+    Some text in a regular paragraph
+
+        :::python
+        for i in range(10):
+            print i
+    ''') ==
+            # no <br
+            '<div class="markdown_content"><h1 id="header">Header</h1>\n'
+            '<p>Some text in a regular paragraph</p>\n'
+            '<div class="codehilite"><pre><span></span><code><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>\n'
+            '    <span class="nb">print</span> <span class="n">i</span>\n'
+            '</code></pre></div>\n'
+            '</div>')
+        assert (
+            g.forge_markdown(email=True).convert('[Home]') ==
+            # uses localhost:
+            '<div class="markdown_content"><p><a class="alink" href="http://localhost/p/test/wiki/Home/">[Home]</a></p></div>')
+        assert (
+            g.markdown.convert('''
+    ~~~~
+    def foo(): pass
+    ~~~~''') ==
+            '<div class="markdown_content"><div class="codehilite"><pre><span></span><code>def foo(): pass\n'
+            '</code></pre></div>\n'
+            '</div>')
+
+    def test_markdown_list_without_break(self):
+        # this is not a valid way to make a list in original Markdown or python-markdown
+        #   https://github.com/Python-Markdown/markdown/issues/874
+        # it is valid in the CommonMark spec https://spec.commonmark.org/0.30/#lists
+        # TODO: try https://github.com/adamb70/mdx-breakless-lists
+        #       or https://gitlab.com/ayblaq/prependnewline
+        assert (
+            g.markdown.convert('''\
+    Regular text
+    * first item
+    * second item''') ==
+            '<div class="markdown_content"><p>Regular text\n'  # no <br>
+            '* first item\n'  # no <br>
+            '* second item</p></div>')
+
+        assert (
+            g.markdown.convert('''\
+    Regular text
+    - first item
+    - second item''') ==
+            '<div class="markdown_content"><p>Regular text<br/>\n'
+            '- first item<br/>\n'
+            '- second item</p></div>')
+
+        assert (
+            g.markdown.convert('''\
+    Regular text
+    + first item
+    + second item''') ==
+            '<div class="markdown_content"><p>Regular text<br/>\n'
+            '+ first item<br/>\n'
+            '+ second item</p></div>')
+
+        assert (
+            g.markdown.convert('''\
+    Regular text
+    1. first item
+    2. second item''') ==
+            '<div class="markdown_content"><p>Regular text<br/>\n'
+            '1. first item<br/>\n'
+            '2. second item</p></div>')
+
+    def test_markdown_autolink(self):
+        tgt = 'http://everything2.com/?node=nate+oostendorp'
+        s = g.markdown.convert('This is %s' % tgt)
+        assert (
+            s == f'<div class="markdown_content"><p>This is <a href="{tgt}" rel="nofollow">{tgt}</a></p></div>')
+        assert '<a href=' in g.markdown.convert('This is http://domain.net')
+        # beginning of doc
+        assert '<a href=' in g.markdown.convert('http://domain.net abc')
+        # beginning of a line
+        assert ('<br/>\n<a href="http://' in
+                g.markdown.convert('foobar\nhttp://domain.net abc'))
+        # no conversion of these urls:
+        assert ('a blahttp://sdf.com z' in
+                g.markdown.convert('a blahttp://sdf.com z'))
+        assert ('literal <code>http://domain.net</code> literal' in
+                g.markdown.convert('literal `http://domain.net` literal'))
+        assert ('<pre><span></span><code>preformatted http://domain.net\n</code></pre>' in
+                g.markdown.convert('    :::text\n'
+                                    '    preformatted http://domain.net'))
+
+    def test_markdown_autolink_with_escape(self):
+        # \_ is unnecessary but valid markdown escaping and should be considered as a regular underscore
+        # (it occurs during html2text conversion during project migrations)
+        r = g.markdown.convert(r'a http://www.phpmyadmin.net/home\_page/security/\#target b')
+        assert 'href="http://www.phpmyadmin.net/home_page/security/#target"' in r, r
+
+    def test_markdown_invalid_script(self):
+        r = g.markdown.convert('<script>alert(document.cookies)</script>')
+        assert '<div class="markdown_content">&lt;script&gt;alert(document.cookies)&lt;/script&gt;\n</div>' == r
+
+    def test_markdown_invalid_onerror(self):
+        r = g.markdown.convert('<img src=x onerror=alert(document.cookie)>')
+        assert 'onerror' not in r
+
+    def test_markdown_invalid_tagslash(self):
+        r = g.markdown.convert('<div/onload><img src=x onerror=alert(document.cookie)>')
+        assert 'onerror' not in r
+
+    def test_markdown_invalid_script_in_link(self):
+        r = g.markdown.convert('[xss](http://"><a onmouseover=prompt(document.domain)>xss</a>)')
+        assert ('<div class="markdown_content"><p><a class="" '
+                    '''href='http://"&gt;&lt;a%20onmouseover=prompt(document.domain)&gt;xss&lt;/a&gt;' '''
+                    'rel="nofollow">xss</a></p></div>' == r)
+
+    def test_markdown_invalid_script_in_link2(self):
+        r = g.markdown.convert('[xss](http://"><img src=x onerror=alert(document.cookie)>)')
+        assert ('<div class="markdown_content"><p><a class="" '
+                    '''href='http://"&gt;&lt;img%20src=x%20onerror=alert(document.cookie)&gt;' '''
+                    'rel="nofollow">xss</a></p></div>' == r)
+
+    def test_markdown_extremely_slow(self):
+        r = g.markdown.convert('''bonjour, voila ce que j'obtient en voulant ajouter un utilisateur a un groupe de sécurite, que ce soit sur un groupe pre-existant, ou sur un groupe crée.
+    message d'erreur:
+
+    ERROR: Could not complete the Add UserLogin To SecurityGroup [file:/C:/neogia/ofbizNeogia/applications/securityext/script/org/ofbiz/securityext/securitygroup/SecurityGroupServices.xml#addUserLoginToSecurityGroup] process [problem creating the newEntity value: Exception while inserting the following entity: [GenericEntity:UserLoginSecurityGroup][createdStamp,2006-01-23 17:42:39.312(java.sql.Timestamp)][createdTxStamp,2006-01-23 17:42:38.875(java.sql.Timestamp)][fromDate,2006-01-23 17: [...]
+
+    à priori les données du formulaire ne sont pas traitées : VALUES (?, ?, ?, ?, ?, ?, ?, ?) ce qui entraine l'echec du traitement SQL.
+
+
+    Si une idée vous vient à l'esprit, merci de me tenir au courant.
+
+    cordialement, julien.''')
+        assert True   # finished!
+
+    @td.with_tool('test', 'Wiki', 'wiki-len')
+    def test_markdown_link_length_limits(self):
+        with h.push_context('test', 'wiki-len', neighborhood='Projects'):
+            # these are always ok, no matter the NOBRACKET length
+            WM.Page.upsert(title='12345678901').commit()
+            text = g.markdown.convert('See [12345678901]')
+            assert 'href="/p/test/wiki-len/12345678901/">[12345678901]</a>' in text, text
+            WM.Page.upsert(title='this is 26 characters long').commit()
+            text = g.markdown.convert('See [this is 26 characters long]')
+            assert 'href="/p/test/wiki-len/this%20is%2026%20characters%20long/">[this is 26 characters long]</a>' in text, text
+
+            # NOBRACKET regex length impacts standard markdown links
+            text = g.markdown.convert('See [short](http://a.de)')
+            assert 'href="http://a.de" rel="nofollow">short</a>' in text, text
+            text = g.markdown.convert('See [this is 26 characters long](http://a.de)')
+            assert 'href="http://a.de" rel="nofollow">this is 26 characters long</a>' in text, text  # {0,12} fails {0,13} ok
+
+            # NOBRACKET regex length impacts our custom artifact links
+            text = g.markdown.convert('See [short](Home)')
+            assert 'href="/p/test/wiki-len/Home/">short</a>' in text, text
+            text = g.markdown.convert('See [123456789](Home)')
+            assert 'href="/p/test/wiki-len/Home/">123456789</a>' in text, text
+            text = g.markdown.convert('See [12345678901](Home)')
+            assert 'href="/p/test/wiki-len/Home/">12345678901</a>' in text, text  # {0,5} fails, {0,6} ok
+            text = g.markdown.convert('See [this is 16 chars](Home)')
+            assert 'href="/p/test/wiki-len/Home/">this is 16 chars</a>' in text, text  # {0,7} fails {0,8} ok
+            text = g.markdown.convert('See [this is 26 characters long](Home)')
+            assert 'href="/p/test/wiki-len/Home/">this is 26 characters long</a>' in text, text  # {0,12} fails {0,13} ok
+
+            # limit, currently
+            charSuperLong = '1234567890'*21
+            text = g.markdown.convert(f'See [{charSuperLong}](Home)')
+            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
+        assert 'href="../foo"' in g.markdown.convert('[My foo](foo)')
+        assert 'href="..' not in g.markdown.convert('[My foo](./foo)')
+
+    def test_macro_nbhd_feeds(self):
+        with h.push_context('--init--', 'wiki', neighborhood='Projects'):
+            r = g.markdown_wiki.convert('[[neighborhood_feeds tool_name=wiki]]')
+            assert 'Home modified by ' in r, r
+            assert '&lt;div class="markdown_content"&gt;' not in r
+
+    def test_sort_alpha(self):
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+
+        with h.push_context(p_nbhd.neighborhood_project._id):
+            r = g.markdown_wiki.convert('[[projects sort=alpha]]')
+            project_list = get_project_names(r)
+            assert project_list == sorted(project_list)
+
+    def test_sort_registered(self):
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+
+        with h.push_context(p_nbhd.neighborhood_project._id):
+            r = g.markdown_wiki.convert('[[projects sort=last_registered]]')
+            project_names = get_project_names(r)
+            ids = get_projects_property_in_the_same_order(project_names, '_id')
+            assert ids == sorted(ids, reverse=True)
+
+    def test_sort_updated(self):
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+
+        with h.push_context(p_nbhd.neighborhood_project._id):
+            r = g.markdown_wiki.convert('[[projects sort=last_updated]]')
+            project_names = get_project_names(r)
+            updated_at = get_projects_property_in_the_same_order(
+                project_names, 'last_updated')
+            assert updated_at == sorted(updated_at, reverse=True)
+
+    def test_filtering(self):
+        # set up for test
+        from random import choice
+        setup_trove_categories()
+        random_trove = choice(M.TroveCategory.query.find().all())
+        test_project = M.Project.query.get(shortname='test')
+        test_project_troves = getattr(test_project, 'trove_' + random_trove.type)
+        test_project_troves.append(random_trove._id)
+        ThreadLocalORMSession.flush_all()
+
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        with h.push_config(c,
+                        project=p_nbhd.neighborhood_project,
+                        user=M.User.by_username('test-admin')):
+            r = g.markdown_wiki.convert(
+                '[[projects category="%s"]]' % random_trove.fullpath)
+            project_names = get_project_names(r)
+            assert [test_project.name] == project_names
+
+    def test_projects_macro(self):
+        two_column_style = 'width: 330px;'
+
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        with h.push_config(c,
+                        project=p_nbhd.neighborhood_project,
+                        user=M.User.anonymous()):
+            # test columns
+            r = g.markdown_wiki.convert('[[projects display_mode=list columns=2]]')
+            assert two_column_style in r
+            r = g.markdown_wiki.convert('[[projects display_mode=list columns=3]]')
+            assert two_column_style not in r
+
+    @td.with_user_project('test-admin')
+    @td.with_user_project('test-user-1')
+    def test_myprojects_macro(self):
+        h.set_context('u/%s' % (c.user.username), 'wiki', neighborhood='Users')
+        r = g.markdown_wiki.convert('[[my_projects]]')
+        for p in c.user.my_projects():
+            if p.deleted or p.is_nbhd_project:
+                continue
+            proj_title = f'<h2><a href="{p.url()}">{p.name}</a></h2>'
+            assert proj_title in r
+
+        h.set_context('u/test-user-1', 'wiki', neighborhood='Users')
+        user = M.User.query.get(username='test-user-1')
+        r = g.markdown_wiki.convert('[[my_projects]]')
+        for p in user.my_projects():
+            if p.deleted or p.is_nbhd_project:
+                continue
+            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')
+
+        app_config_id = ObjectId()
+        award = M.Award(app_config_id=app_config_id)
+        award.short = 'Award short'
+        award.full = 'Award full'
+        award.created_by_neighborhood_id = p_nbhd._id
+
+        project = M.Project.query.get(
+            neighborhood_id=p_nbhd._id, shortname='test')
+
+        M.AwardGrant(
+            award=award,
+            award_url='http://award.org',
+            comment='Winner!',
+            granted_by_neighborhood=p_nbhd,
+            granted_to_project=project)
+
+        ThreadLocalORMSession.flush_all()
+
+        with h.push_context(p_nbhd.neighborhood_project._id):
+            r = g.markdown_wiki.convert('[[projects]]')
+            assert ('<div class="feature"> <a href="http://award.org" rel="nofollow" title="Winner!">'
+                    'Award short</a> </div>' in
+                    squish_spaces(r))
+
+            r = g.markdown_wiki.convert('[[projects show_awards_banner=False]]')
+            assert 'Award short' not in r
+
+    @td.with_tool('test', 'Blog', 'blog')
+    def test_project_blog_posts_macro(self):
+        from forgeblog import model as BM
+        with h.push_context('test', 'blog', neighborhood='Projects'):
+            BM.BlogPost.new(
+                title='Test title',
+                text='test post',
+                state='published',
+            )
+            BM.BlogPost.new(
+                title='Test title2',
+                text='test post2',
+                state='published',
+            )
+
+            r = g.markdown_wiki.convert('[[project_blog_posts]]')
+            assert 'Test title</a></h3>' in r
+            assert 'Test title2</a></h3>' in r
+            assert '<div class="markdown_content"><p>test post</p></div>' in r
+            assert '<div class="markdown_content"><p>test post2</p></div>' in r
+            assert 'by <em>Test Admin</em>' in r
+
+    def test_project_screenshots_macro(self):
+        with h.push_context('test', neighborhood='Projects'):
+            M.ProjectFile(project_id=c.project._id, category='screenshot', caption='caption', filename='test_file.jpg')
+            ThreadLocalORMSession.flush_all()
+
+            r = g.markdown_wiki.convert('[[project_screenshots]]')
+
+            assert 'href="/p/test/screenshot/test_file.jpg"' in r
+            assert 'src="/p/test/screenshot/test_file.jpg/thumb"' in r
+
+
 @with_nose_compatibility
 class TestCachedMarkdown(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.md = ForgeMarkdown()
         self.post = M.Post()
         self.post.text = '**bold**'
@@ -1025,7 +971,7 @@ class TestUserMentions(unittest.TestCase):
 @with_nose_compatibility
 class TestHandlePaging(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         prefs = {}
         c.user = Mock()
 
@@ -1086,7 +1032,7 @@ class TestHandlePaging(unittest.TestCase):
 @with_nose_compatibility
 class TestIconRender:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.i = g.icons['edit']
 
     def test_default(self):
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index ea0c50a2b..dbd359e33 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -23,7 +23,7 @@ import time
 import PIL
 from mock import Mock, patch
 from tg import tmpl_context as c
-from alluratest.tools import assert_equals, assert_raises, module_not_available
+from alluratest.tools import assert_equals, assert_raises, module_not_available, with_setup
 from datadiff import tools as dd
 from webob import Request
 from webob.exc import HTTPUnauthorized
@@ -42,7 +42,7 @@ from alluratest.controller import setup_basic_test
 import six
 
 
-def setup_class(self, method):
+def setup_method():
     """Method called by nose before running each test"""
     setup_basic_test()
 
@@ -50,7 +50,7 @@ def setup_class(self, method):
 @with_nose_compatibility
 class TestMakeSafePathPortion(TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.f = h.make_safe_path_portion
 
     def test_no_ascii_chars(self):
@@ -118,7 +118,7 @@ def test_really_unicode():
     assert isinstance(s, Markup)
     assert s == '<b>test</b>'
 
-
+@with_setup(setup_method)
 def test_find_project():
     proj, rest = h.find_project('/p/test/foo')
     assert proj.shortname == 'test'
@@ -127,12 +127,14 @@ def test_find_project():
     assert proj is None
 
 
+@with_setup(setup_method)
 def test_make_roles():
     h.set_context('test', 'wiki', neighborhood='Projects')
     pr = M.ProjectRole.anonymous()
     assert next(h.make_roles([pr._id])) == pr
 
 
+@with_setup(setup_method)
 @td.with_wiki
 def test_make_app_admin_only():
     h.set_context('test', 'wiki', neighborhood='Projects')
@@ -165,6 +167,7 @@ def test_make_app_admin_only():
     assert c.app.is_visible_to(admin)
 
 
+@with_setup(setup_method)
 @td.with_wiki
 def test_context_setters():
     h.set_context('test', 'wiki', neighborhood='Projects')
@@ -259,6 +262,7 @@ def test_render_any_markup_empty():
     assert h.render_any_markup('foo', '') == '<p><em>Empty File</em></p>'
 
 
+@with_setup(setup_method)
 def test_render_any_markup_plain():
     assert (
         h.render_any_markup(
@@ -266,6 +270,7 @@ def test_render_any_markup_plain():
         '<pre>&lt;b&gt;blah&lt;/b&gt;\n&lt;script&gt;alert(1)&lt;/script&gt;\nfoo</pre>')
 
 
+@with_setup(setup_method)
 def test_render_any_markup_formatting():
     assert (str(h.render_any_markup('README.md', '### foo\n'
                                           '    <script>alert(1)</script> bar')) ==
@@ -275,6 +280,7 @@ def test_render_any_markup_formatting():
                   '&lt;/script&gt;</span> bar\n</code></pre></div>\n</div>')
 
 
+@with_setup(setup_method)
 def test_render_any_markdown_encoding():
     # send encoded content in, make sure it converts it to actual unicode object which Markdown lib needs
     assert (h.render_any_markup('README.md', 'Müller'.encode()) ==
@@ -306,6 +312,7 @@ def test_log_if_changed():
     assert AuditLogMock.logs[0] == 'updated value'
 
 
+@with_setup(setup_method)
 def test_get_tool_packages():
     assert h.get_tool_packages('tickets') == ['forgetracker']
     assert h.get_tool_packages('Tickets') == ['forgetracker']
@@ -321,6 +328,7 @@ def test_get_first():
     assert h.get_first({'title': ['Value']}, 'title') == 'Value'
 
 
+@with_setup(setup_method)
 @patch('allura.lib.search.c')
 def test_inject_user(context):
     user = Mock(username='user01')
@@ -515,6 +523,7 @@ class TestUrlOpen(TestCase):
         self.assertEqual(urlopen.call_count, 1)
 
 
+@with_setup(setup_method)
 def test_absurl():
     assert h.absurl('/p/test/foobar') == 'http://localhost/p/test/foobar'
 
@@ -525,6 +534,7 @@ def test_daterange():
         [datetime(2013, 1, 1), datetime(2013, 1, 2), datetime(2013, 1, 3)])
 
 
+@with_setup(setup_method)
 @patch.object(h, 'request',
               new=Request.blank('/p/test/foobar', base_url='https://www.mysite.com/p/test/foobar'))
 def test_login_overlay():
@@ -591,6 +601,7 @@ class TestIterEntryPoints(TestCase):
                                 list, h.iter_entry_points('allura'))
 
 
+@with_setup(setup_method)
 def test_get_user_status():
     user = M.User.by_username('test-admin')
     assert h.get_user_status(user) == 'enabled'
diff --git a/Allura/allura/tests/test_mail_util.py b/Allura/allura/tests/test_mail_util.py
index 4a3970c82..f1ad21d09 100644
--- a/Allura/allura/tests/test_mail_util.py
+++ b/Allura/allura/tests/test_mail_util.py
@@ -49,7 +49,7 @@ config = ConfigProxy(
 @with_nose_compatibility
 class TestReactor(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
         ThreadLocalORMSession.flush_all()
@@ -236,7 +236,7 @@ class TestHeader:
 @with_nose_compatibility
 class TestIsAutoreply:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.msg = {'headers': {}}
 
     def test_empty(self):
@@ -333,7 +333,7 @@ def test_parse_message_id():
 @with_nose_compatibility
 class TestMailServer:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     @mock.patch('allura.command.base.log', autospec=True)
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index fa15b7fa3..1f8e78a79 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -25,7 +25,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestCORSMiddleware:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.app = MagicMock()
         self.allowed_methods = ['GET', 'POST', 'DELETE']
         self.allowed_headers = ['Authorization', 'Accept']
diff --git a/Allura/allura/tests/test_multifactor.py b/Allura/allura/tests/test_multifactor.py
index de6208c3b..e007b0adf 100644
--- a/Allura/allura/tests/test_multifactor.py
+++ b/Allura/allura/tests/test_multifactor.py
@@ -181,7 +181,7 @@ class TestMongodbTotpService(TestAnyTotpServiceImplementation):
     __test__ = True
     Service = MongodbTotpService
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         config = {
             'ming.main.uri': 'mim://host/allura_test',
         }
@@ -191,7 +191,7 @@ class TestMongodbTotpService(TestAnyTotpServiceImplementation):
 @with_nose_compatibility
 class TestGoogleAuthenticatorPamFilesystemMixin:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.totp_basedir = tempfile.mkdtemp(prefix='totp-test', dir=os.getenv('TMPDIR', '/tmp'))
         config['auth.multifactor.totp.filesystem.basedir'] = self.totp_basedir
 
@@ -312,7 +312,7 @@ class TestMongodbRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation):
 
     Service = MongodbRecoveryCodeService
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         config = {
             'ming.main.uri': 'mim://host/allura_test',
         }
@@ -327,7 +327,7 @@ class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCod
 
     Service = GoogleAuthenticatorPamFilesystemRecoveryCodeService
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
 
         # make a regular .google-authenticator file first, so recovery keys have somewhere to go
diff --git a/Allura/allura/tests/test_plugin.py b/Allura/allura/tests/test_plugin.py
index c0498de69..22a7ed8bf 100644
--- a/Allura/allura/tests/test_plugin.py
+++ b/Allura/allura/tests/test_plugin.py
@@ -51,7 +51,8 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestProjectRegistrationProvider:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
+        setup_basic_test()
         self.provider = ProjectRegistrationProvider()
 
     @patch('allura.lib.security.has_access')
@@ -89,7 +90,7 @@ class TestProjectRegistrationProvider:
 @with_nose_compatibility
 class TestProjectRegistrationProviderParseProjectFromUrl:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         ThreadLocalORMSession.close_all()
         setup_global_objects()
@@ -164,7 +165,7 @@ class UserMock:
 @with_nose_compatibility
 class TestProjectRegistrationProviderPhoneVerification:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.p = ProjectRegistrationProvider()
         self.user = UserMock()
         self.nbhd = MagicMock()
@@ -637,7 +638,7 @@ class TestThemeProvider_notifications:
 @with_nose_compatibility
 class TestLocalAuthenticationProvider:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         ThreadLocalORMSession.close_all()
         setup_global_objects()
@@ -752,7 +753,7 @@ class TestLocalAuthenticationProvider:
 @with_nose_compatibility
 class TestAuthenticationProvider:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.provider = plugin.AuthenticationProvider(Request.blank('/'))
         self.pwd_updated = dt.datetime.utcnow() - dt.timedelta(days=100)
diff --git a/Allura/allura/tests/test_scripttask.py b/Allura/allura/tests/test_scripttask.py
index d66bf2684..f8576bbf8 100644
--- a/Allura/allura/tests/test_scripttask.py
+++ b/Allura/allura/tests/test_scripttask.py
@@ -25,7 +25,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestScriptTask(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         class TestScriptTask(ScriptTask):
             _parser = mock.Mock()
 
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 0547b1954..735b3294b 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -58,6 +58,10 @@ from allura.lib.decorators import event_handler, task
 @with_nose_compatibility
 class TestRepoTasks(unittest.TestCase):
 
+    def setup_method(self, method):
+        setup_basic_test()
+        setup_global_objects()
+
     @mock.patch('allura.tasks.repo_tasks.c.app')
     @mock.patch('allura.tasks.repo_tasks.g.post_event')
     def test_clone_posts_event_on_failure(self, post_event, app):
@@ -103,7 +107,7 @@ def _task_that_creates_event(event_name,):
 @with_nose_compatibility
 class TestEventTasks(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
         self.called_with = []
@@ -162,7 +166,7 @@ class TestEventTasks(unittest.TestCase):
 @with_nose_compatibility
 class TestIndexTasks(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
 
@@ -248,7 +252,7 @@ class TestIndexTasks(unittest.TestCase):
 @with_nose_compatibility
 class TestMailTasks(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
 
@@ -543,7 +547,7 @@ I'm not here'''
 
 @with_nose_compatibility
 class TestUserNotificationTasks(TestController):
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.setup_with_tools()
 
@@ -576,7 +580,7 @@ class TestUserNotificationTasks(TestController):
 @with_nose_compatibility
 class TestNotificationTasks(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
 
@@ -623,7 +627,7 @@ class _TestArtifact(M.Artifact):
 @with_nose_compatibility
 class TestExportTasks(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         setup_global_objects()
         project = M.Project.query.get(shortname='test')
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index ac408a586..6e7fca1cc 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -51,7 +51,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestConfigProxy(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.cp = utils.ConfigProxy(mybaz="baz")
 
     def test_getattr(self):
@@ -72,7 +72,7 @@ class TestConfigProxy(unittest.TestCase):
 @with_nose_compatibility
 class TestChunkedIterator(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_unit_test()
         config = {
             'ming.main.uri': 'mim://host/allura_test',
@@ -115,7 +115,7 @@ class TestChunkedList(unittest.TestCase):
 @with_nose_compatibility
 class TestAntispam(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_unit_test()
         self.a = utils.AntiSpam()
 
@@ -256,7 +256,7 @@ class TestIsTextFile(unittest.TestCase):
 @with_nose_compatibility
 class TestCodeStats(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_unit_test()
 
     def test_generate_code_stats(self):
diff --git a/Allura/allura/tests/test_validators.py b/Allura/allura/tests/test_validators.py
index cc9f2fba0..2e0f5af65 100644
--- a/Allura/allura/tests/test_validators.py
+++ b/Allura/allura/tests/test_validators.py
@@ -27,7 +27,7 @@ from allura.websetup.bootstrap import create_user
 from allura.tests.pytest_helpers import with_nose_compatibility
 
 
-def setup_method(self, method):
+def _setup_method():
     setup_basic_test()
 
 
@@ -40,6 +40,9 @@ def dummy_task(*args, **kw):
 class TestJsonConverter(unittest.TestCase):
     val = v.JsonConverter
 
+    def setup_method(self, method):
+        _setup_method()
+
     def test_valid(self):
         self.assertEqual({}, self.val.to_python('{}'))
 
@@ -52,6 +55,10 @@ class TestJsonConverter(unittest.TestCase):
 
 @with_nose_compatibility
 class TestJsonFile(unittest.TestCase):
+
+    def setup_method(self, method):
+        _setup_method()
+
     val = v.JsonFile
 
     class FieldStorage:
@@ -71,6 +78,9 @@ class TestJsonFile(unittest.TestCase):
 class TestUserMapFile(unittest.TestCase):
     val = v.UserMapJsonFile()
 
+    def setup_method(self, method):
+        _setup_method()
+
     class FieldStorage:
 
         def __init__(self, content):
@@ -94,6 +104,9 @@ class TestUserMapFile(unittest.TestCase):
 class TestUserValidator(unittest.TestCase):
     val = v.UserValidator
 
+    def setup_method(self, method):
+        _setup_method()
+
     def test_valid(self):
         self.assertEqual(M.User.by_username('root'),
                          self.val.to_python('root'))
@@ -108,6 +121,9 @@ class TestUserValidator(unittest.TestCase):
 class TestAnonymousValidator(unittest.TestCase):
     val = v.AnonymousValidator
 
+    def setup_method(self, method):
+        _setup_method()
+
     @patch('allura.lib.validators.c')
     def test_valid(self, c):
         c.user = M.User.by_username('root')
@@ -124,6 +140,9 @@ class TestAnonymousValidator(unittest.TestCase):
 @with_nose_compatibility
 class TestMountPointValidator(unittest.TestCase):
 
+    def setup_method(self, method):
+        _setup_method()
+
     @patch('allura.lib.validators.c')
     def test_valid(self, c):
         App = Mock()
@@ -186,6 +205,9 @@ class TestMountPointValidator(unittest.TestCase):
 class TestTaskValidator(unittest.TestCase):
     val = v.TaskValidator
 
+    def setup_method(self, method):
+        _setup_method()
+
     def test_valid(self):
         self.assertEqual(
             dummy_task, self.val.to_python('allura.tests.test_validators.dummy_task'))
@@ -208,15 +230,18 @@ class TestTaskValidator(unittest.TestCase):
 
     def test_not_a_task(self):
         with self.assertRaises(fe.Invalid) as cm:
-            self.val.to_python('allura.tests.test_validators.setUp')
+            self.val.to_python('allura.tests.test_validators._setup_method')
         self.assertEqual(str(cm.exception),
-                         '"allura.tests.test_validators.setUp" is not a task.')
+                         '"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={})
 
+    def setup_method(self, method):
+        _setup_method()
+
     def test_valid_project(self):
         project = M.Project.query.get(shortname='test')
         d = self.val.to_python('/p/test')
@@ -266,6 +291,9 @@ class TestPathValidator(unittest.TestCase):
 class TestUrlValidator(unittest.TestCase):
     val = v.URL
 
+    def setup_method(self, method):
+        _setup_method()
+
     def test_valid(self):
         self.assertEqual('http://192.168.0.1', self.val.to_python('192.168.0.1'))
         self.assertEqual('http://url', self.val.to_python('url'))
@@ -285,6 +313,9 @@ class TestUrlValidator(unittest.TestCase):
 class TestNonHttpUrlValidator(unittest.TestCase):
     val = v.NonHttpUrl
 
+    def setup_method(self, method):
+        _setup_method()
+
     def test_valid(self):
         self.assertEqual('svn://192.168.0.1', self.val.to_python('svn://192.168.0.1'))
         self.assertEqual('ssh+git://url', self.val.to_python('ssh+git://url'))
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index df7512d59..5b9d5b1f2 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -62,7 +62,7 @@ with_git2 = td.with_tool(test_project_with_repo, 'git', 'src2', 'Git2')
 
 @with_nose_compatibility
 class TestWebhookBase:
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.patches = self.monkey_patch()
         for p in self.patches:
@@ -132,7 +132,7 @@ class TestValidators(TestWebhookBase):
 
 @with_nose_compatibility
 class TestWebhookController(TestController):
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.patches = self.monkey_patch()
         for p in self.patches:
@@ -424,8 +424,8 @@ class TestWebhookController(TestController):
 
 @with_nose_compatibility
 class TestSendWebhookHelper(TestWebhookBase):
-    def setUp(self, *args, **kw):
-        super().setUp(*args, **kw)
+    def setup_method(self, method):
+        super().setup_method(method)
         self.payload = {'some': ['data', 23]}
         self.h = SendWebhookHelper(self.wh, self.payload)
 
@@ -678,7 +678,7 @@ class TestModels(TestWebhookBase):
 
 @with_nose_compatibility
 class TestWebhookRestController(TestRestApiBase):
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.patches = self.monkey_patch()
         for p in self.patches:
diff --git a/Allura/allura/tests/unit/__init__.py b/Allura/allura/tests/unit/__init__.py
index 43b8c8256..4097ac952 100644
--- a/Allura/allura/tests/unit/__init__.py
+++ b/Allura/allura/tests/unit/__init__.py
@@ -19,14 +19,14 @@ from alluratest.controller import setup_basic_test
 from allura.websetup.bootstrap import clear_all_database_tables
 
 
-def setup_class(self, method):
+def setup_module(module):
     setup_basic_test()
 
 
 class MockPatchTestCase:
     patches = []
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self._patch_instances = [patch_fn(self) for patch_fn in self.patches]
         for patch_instance in self._patch_instances:
             patch_instance.__enter__()
@@ -38,6 +38,6 @@ class MockPatchTestCase:
 
 class WithDatabase(MockPatchTestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         clear_all_database_tables()
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 10a0e113d..72abd8ed4 100644
--- a/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
+++ b/Allura/allura/tests/unit/controllers/test_discussion_moderation_controller.py
@@ -35,7 +35,7 @@ class TestWhenModerating(WithDatabase):
                patches.fake_request_patch,
                patches.disable_notifications_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         post = create_post('mypost')
         discussion_controller = Mock(
@@ -89,7 +89,7 @@ class TestIndexWithNoPosts(WithDatabase):
 class TestIndexWithAPostInTheDiscussion(WithDatabase):
     patches = [patches.fake_app_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.post = create_post('mypost')
         discussion = self.post.discussion
diff --git a/Allura/allura/tests/unit/phone/test_nexmo.py b/Allura/allura/tests/unit/phone/test_nexmo.py
index 776b4af01..2bcff5cfa 100644
--- a/Allura/allura/tests/unit/phone/test_nexmo.py
+++ b/Allura/allura/tests/unit/phone/test_nexmo.py
@@ -27,7 +27,7 @@ from allura.lib.phone.nexmo import NexmoPhoneService
 @with_nose_compatibility
 class TestPhoneService:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         config = {'phone.api_key': 'test-api-key',
                   'phone.api_secret': 'test-api-secret',
                   'site_name': 'Very loooooooooong site name'}
diff --git a/Allura/allura/tests/unit/spam/test_spam_filter.py b/Allura/allura/tests/unit/spam/test_spam_filter.py
index fa71ccecd..99f6ce1a9 100644
--- a/Allura/allura/tests/unit/spam/test_spam_filter.py
+++ b/Allura/allura/tests/unit/spam/test_spam_filter.py
@@ -79,7 +79,7 @@ class TestSpamFilter(unittest.TestCase):
 @with_nose_compatibility
 class TestSpamFilterFunctional:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
 
     def test_record_result(self):
diff --git a/Allura/allura/tests/unit/spam/test_stopforumspam.py b/Allura/allura/tests/unit/spam/test_stopforumspam.py
index fbc6f9e40..2037ddde8 100644
--- a/Allura/allura/tests/unit/spam/test_stopforumspam.py
+++ b/Allura/allura/tests/unit/spam/test_stopforumspam.py
@@ -28,7 +28,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestStopForumSpam:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.content = 'spåm text'
 
         self.artifact = mock.Mock()
diff --git a/Allura/allura/tests/unit/test_app.py b/Allura/allura/tests/unit/test_app.py
index f60b95432..2a374f415 100644
--- a/Allura/allura/tests/unit/test_app.py
+++ b/Allura/allura/tests/unit/test_app.py
@@ -17,8 +17,6 @@
 
 from unittest import TestCase
 
-from alluratest.tools import assert_equal
-
 from allura.app import Application
 from allura import model
 from allura.tests.unit import WithDatabase
@@ -73,7 +71,7 @@ class TestInstall(WithDatabase):
 class TestDefaultDiscussion(WithDatabase):
     patches = [fake_app_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         install_app()
         self.discussion = model.Discussion.query.get(
@@ -94,7 +92,7 @@ class TestDefaultDiscussion(WithDatabase):
 class TestAppDefaults(WithDatabase):
     patches = [fake_app_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.app = install_app()
 
diff --git a/Allura/allura/tests/unit/test_discuss.py b/Allura/allura/tests/unit/test_discuss.py
index 393d08921..9cb225e40 100644
--- a/Allura/allura/tests/unit/test_discuss.py
+++ b/Allura/allura/tests/unit/test_discuss.py
@@ -15,8 +15,6 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from alluratest.tools import assert_false, assert_true
-
 from allura import model as M
 from allura.tests.unit import WithDatabase
 from allura.tests.unit.patches import 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 0c538e752..7fef39014 100644
--- a/Allura/allura/tests/unit/test_helpers/test_ago.py
+++ b/Allura/allura/tests/unit/test_helpers/test_ago.py
@@ -27,7 +27,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestAgo:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.start_time = datetime(2010, 1, 1, 0, 0, 0)
 
     def test_that_exact_times_are_phrased_in_seconds(self):
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 102e48ab8..3468ceed0 100644
--- a/Allura/allura/tests/unit/test_helpers/test_set_context.py
+++ b/Allura/allura/tests/unit/test_helpers/test_set_context.py
@@ -32,7 +32,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestWhenProjectIsFoundAndAppIsNot(WithDatabase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.myproject = create_project('myproject')
         set_context('myproject', neighborhood=self.myproject.neighborhood)
@@ -47,7 +47,7 @@ class TestWhenProjectIsFoundAndAppIsNot(WithDatabase):
 @with_nose_compatibility
 class TestWhenProjectIsFoundInNeighborhood(WithDatabase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.myproject = create_project('myproject')
         set_context('myproject', neighborhood=self.myproject.neighborhood)
@@ -63,7 +63,7 @@ class TestWhenProjectIsFoundInNeighborhood(WithDatabase):
 class TestWhenAppIsFoundByID(WithDatabase):
     patches = [patches.project_app_loading_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.myproject = create_project('myproject')
         self.app_config = create_app_config(self.myproject, 'my_mounted_app')
@@ -81,7 +81,7 @@ class TestWhenAppIsFoundByID(WithDatabase):
 class TestWhenAppIsFoundByMountPoint(WithDatabase):
     patches = [patches.project_app_loading_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.myproject = create_project('myproject')
         self.app_config = create_app_config(self.myproject, 'my_mounted_app')
diff --git a/Allura/allura/tests/unit/test_ldap_auth_provider.py b/Allura/allura/tests/unit/test_ldap_auth_provider.py
index 57c0ea6cb..d94430188 100644
--- a/Allura/allura/tests/unit/test_ldap_auth_provider.py
+++ b/Allura/allura/tests/unit/test_ldap_auth_provider.py
@@ -38,7 +38,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestLdapAuthenticationProvider:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         setup_basic_test()
         self.provider = plugin.LdapAuthenticationProvider(Request.blank('/'))
 
diff --git a/Allura/allura/tests/unit/test_mixins.py b/Allura/allura/tests/unit/test_mixins.py
index 670a8c929..f4e50aafe 100644
--- a/Allura/allura/tests/unit/test_mixins.py
+++ b/Allura/allura/tests/unit/test_mixins.py
@@ -23,7 +23,7 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestVotableArtifact:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.user1 = Mock()
         self.user1.username = 'test-user'
         self.user2 = Mock()
diff --git a/Allura/allura/tests/unit/test_post_model.py b/Allura/allura/tests/unit/test_post_model.py
index a6d62fff8..e1f7504ed 100644
--- a/Allura/allura/tests/unit/test_post_model.py
+++ b/Allura/allura/tests/unit/test_post_model.py
@@ -30,7 +30,7 @@ class TestPostModel(WithDatabase):
     patches = [patches.fake_app_patch,
                patches.disable_notifications_patch]
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         super().setup_method(method)
         self.post = create_post('mypost')
 
diff --git a/Allura/allura/tests/unit/test_repo.py b/Allura/allura/tests/unit/test_repo.py
index 12bc058c0..df862d547 100644
--- a/Allura/allura/tests/unit/test_repo.py
+++ b/Allura/allura/tests/unit/test_repo.py
@@ -314,7 +314,7 @@ class TestPrefixPathsUnion(unittest.TestCase):
 @with_nose_compatibility
 class TestGroupCommits:
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.repo = Mock()
         self.repo.symbolics_for_commit.return_value = ([], [])
 
diff --git a/Allura/allura/tests/unit/test_session.py b/Allura/allura/tests/unit/test_session.py
index fbf6e7f0e..cb7f02f7d 100644
--- a/Allura/allura/tests/unit/test_session.py
+++ b/Allura/allura/tests/unit/test_session.py
@@ -88,7 +88,7 @@ class TestSessionExtension(TestCase):
 @with_nose_compatibility
 class TestIndexerSessionExtension(TestSessionExtension):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         session = mock.Mock()
         self.ExtensionClass = IndexerSessionExtension
         self.extension = self.ExtensionClass(session)
@@ -127,7 +127,7 @@ class TestIndexerSessionExtension(TestSessionExtension):
 @with_nose_compatibility
 class TestArtifactSessionExtension(TestSessionExtension):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         session = mock.Mock(disable_index=False)
         self.ExtensionClass = ArtifactSessionExtension
         self.extension = self.ExtensionClass(session)
@@ -157,7 +157,7 @@ class TestArtifactSessionExtension(TestSessionExtension):
 @with_nose_compatibility
 class TestBatchIndexer(TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         session = mock.Mock()
         self.extcls = BatchIndexer
         self.ext = self.extcls(session)
diff --git a/Allura/allura/tests/unit/test_solr.py b/Allura/allura/tests/unit/test_solr.py
index 7bb5a188a..cc73cdb6f 100644
--- a/Allura/allura/tests/unit/test_solr.py
+++ b/Allura/allura/tests/unit/test_solr.py
@@ -32,6 +32,10 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestSolr(unittest.TestCase):
 
+    def setup_method(self, method):
+        # need to create the "test" project so @td.with_wiki works
+        setup_basic_test()
+
     @mock.patch('allura.lib.solr.pysolr')
     def test_init(self, pysolr):
         servers = ['server1', 'server2']
@@ -122,7 +126,7 @@ class TestSolr(unittest.TestCase):
 @with_nose_compatibility
 class TestSearchIndexable(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         self.obj = SearchIndexable()
 
     def test_solarize_empty_index(self):
@@ -147,7 +151,7 @@ class TestSearchIndexable(unittest.TestCase):
 @with_nose_compatibility
 class TestSearch_app(unittest.TestCase):
 
-    def setup_class(self, method):
+    def setup_method(self, method):
         # need to create the "test" project so @td.with_wiki works
         setup_basic_test()
 
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index 9121f6085..02ff29132 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -66,8 +66,8 @@ def find(d, pred):
 
 
 class TrackerTestController(TestController):
-    def setUp(self):
-        super().setUp()
+    def setup_method(self, method):
+        super().setup_method(method)
         self.setup_with_tools()
 
     @td.with_tracker