diff --git a/plinth/context_processors.py b/plinth/context_processors.py
index 920b90175..e58d7e162 100644
--- a/plinth/context_processors.py
+++ b/plinth/context_processors.py
@@ -25,7 +25,12 @@ from plinth import cfg
def common(request):
- """Add additional context values to RequestContext for use in templates."""
+ """
+ Add additional context values to RequestContext for use in templates.
+
+ Any resources referenced in the return value are expected to have been
+ initialized or configured externally beforehand.
+ """
slash_indices = [match.start() for match in re.finditer('/', request.path)]
active_menu_urls = [request.path[:index + 1] for index in slash_indices]
return {
diff --git a/plinth/tests/coverage/coverage.py b/plinth/tests/coverage/coverage.py
index b41d367c2..aa22e7cbf 100644
--- a/plinth/tests/coverage/coverage.py
+++ b/plinth/tests/coverage/coverage.py
@@ -26,13 +26,11 @@
Support for integration of code test coverage analysis with setuptools.
"""
+import coverage
import glob
import setuptools
import shutil
import time
-import unittest
-
-from plinth import tests
# Project directories with testable source code
@@ -78,22 +76,14 @@ class CoverageCommand(setuptools.Command):
except:
pass
- # Initialize a test suite for one or all modules
- if self.test_module is None:
- test_suite = tests.TEST_SUITE
- else:
- test = unittest.defaultTestLoader.loadTestsFromNames(
- [self.test_module])
- test_suite = unittest.TestSuite(test)
-
# Run the coverage analysis
- runner = unittest.TextTestRunner()
- import coverage
cov = coverage.coverage(auto_data=True, config_file=True,
source=SOURCE_DIRS, omit=FILES_TO_OMIT)
cov.erase() # Erase existing coverage data file
cov.start()
- runner.run(test_suite)
+ # Invoke the Django test setup and test runner logic
+ from plinth.tests.runtests import run_tests
+ run_tests(pattern=self.test_module, return_to_caller=True)
cov.stop()
# Generate an HTML report
diff --git a/plinth/tests/data/django_test_settings.py b/plinth/tests/data/django_test_settings.py
new file mode 100644
index 000000000..894b60da3
--- /dev/null
+++ b/plinth/tests/data/django_test_settings.py
@@ -0,0 +1,28 @@
+# Django settings for test modules.
+
+import os
+TEST_DATA_DIR = os.path.dirname(os.path.abspath(__file__))
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(TEST_DATA_DIR, 'plinth.sqlite3'),
+ }
+}
+
+DEFAULT_INDEX_TABLESPACE = ''
+
+INSTALLED_APPS = (
+ 'plinth',
+)
+
+# These are included here solely to suppress Django warnings
+# during testing setup
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'plinth.urls'
+
+SECRET_KEY = 'django_tests_secret_key'
diff --git a/plinth/tests/runtests.py b/plinth/tests/runtests.py
new file mode 100644
index 000000000..6aabc7862
--- /dev/null
+++ b/plinth/tests/runtests.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python3
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""Module for Django pre-test configuration and setup."""
+
+import os
+import sys
+
+import django
+from django.conf import settings
+from django.test.utils import get_runner
+
+
+def run_tests(pattern=None, return_to_caller=False):
+ """Set up the Django test environment and run the specified tests."""
+ os.environ['DJANGO_SETTINGS_MODULE'] =\
+ 'plinth.tests.data.django_test_settings'
+ django.setup()
+ TestRunner = get_runner(settings)
+ test_runner = TestRunner()
+
+ if pattern is None:
+ pattern = 'plinth.tests'
+ failures = test_runner.run_tests([pattern])
+ if failures > 0 or not return_to_caller:
+ sys.exit(bool(failures))
+
+if __name__ == '__main__':
+ run_tests()
diff --git a/plinth/tests/test_context_processors.py b/plinth/tests/test_context_processors.py
index 0634a8acb..f3be34fab 100644
--- a/plinth/tests/test_context_processors.py
+++ b/plinth/tests/test_context_processors.py
@@ -17,17 +17,19 @@
#
from django.http import HttpRequest
-import unittest
+from django.test import TestCase
+from plinth import cfg
from plinth import context_processors as cp
-class ContextProcessorsTestCase(unittest.TestCase):
+class ContextProcessorsTestCase(TestCase):
"""Verify behavior of the context_processors module."""
- @unittest.skip('requires configuring Django beforehand')
def test_common(self):
- """Verify that the 'common' function returns the correct values."""
+ """Verify that the common() function returns the correct values."""
+ cfg.read() # initialize config settings
+
request = HttpRequest()
request.path = '/aaa/bbb/ccc/'
response = cp.common(request)
@@ -45,9 +47,8 @@ class ContextProcessorsTestCase(unittest.TestCase):
self.assertIsNotNone(urls)
self.assertEqual(['/', '/aaa/', '/aaa/bbb/', '/aaa/bbb/ccc/'], urls)
- @unittest.skip('requires configuring Django beforehand')
def test_common_border_conditions(self):
- """Verify that the 'common' functions works for border conditions."""
+ """Verify that the common() function works for border conditions."""
request = HttpRequest()
request.path = ''
response = cp.common(request)
@@ -60,7 +61,3 @@ class ContextProcessorsTestCase(unittest.TestCase):
request.path = '/aaa/bbb'
response = cp.common(request)
self.assertEqual(['/', '/aaa/'], response['active_menu_urls'])
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/plinth/tests/test_kvstore.py b/plinth/tests/test_kvstore.py
new file mode 100644
index 000000000..fdf078bc0
--- /dev/null
+++ b/plinth/tests/test_kvstore.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python3
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+from django.test import TestCase
+
+from plinth import kvstore
+
+
+class KvstoreTestCase(TestCase):
+ """Verify the behavior of the kvstore module."""
+
+ def test_get(self):
+ """Verify that a set value can be retrieved."""
+ key = 'name'
+ expected_value = 'Guido'
+ kvstore.set(key, expected_value)
+ actual_value = kvstore.get(key)
+ self.assertEqual(expected_value, actual_value)
+
+ def test_get_default(self):
+ """Verify that either a set value or its default can be retrieved."""
+ expected = 'default'
+ actual = kvstore.get_default('bad_key', expected)
+ self.assertEqual(expected, actual)
diff --git a/plinth/tests/test_menu.py b/plinth/tests/test_menu.py
index 8e27a3b02..dcb214d26 100644
--- a/plinth/tests/test_menu.py
+++ b/plinth/tests/test_menu.py
@@ -16,9 +16,10 @@
# along with this program. If not, see .
#
+from django.core.urlresolvers import reverse
from django.http import HttpRequest
+from django.test import TestCase
import random
-import unittest
from plinth.menu import Menu
@@ -26,10 +27,35 @@ from plinth.menu import Menu
URL_TEMPLATE = '/a{}/b{}/c{}/'
-class MenuTestCase(unittest.TestCase):
- """Verify the behavior of the Plinth Menu class."""
+# Test helper methods
- # Test methods
+def build_menu(size=5):
+ """Build a menu with the specified number of items."""
+ random.seed()
+ item_data = []
+ for index in range(1, size + 1):
+ item_data.append(['Item' + str(index),
+ 'Icon' + str(index),
+ URL_TEMPLATE.format(index, index, index),
+ random.randint(0, 1000)])
+ menu = Menu()
+ for data in item_data:
+ menu.add_item(data[0], data[1], data[2], data[3])
+ return menu
+
+
+def dump_menu(menu):
+ """Dump the specified menu URL hierarchy to the console."""
+ print()
+ print('# # # # # # # # # #')
+ print('Top level URL: %s' % menu.url)
+ for item in menu.items:
+ print(' Item URL: %s' % item.url)
+ print('# # # # # # # # # #')
+
+
+class MenuTestCase(TestCase):
+ """Verify the behavior of the Plinth Menu class."""
def test_menu_creation_without_arguments(self):
"""Verify the Menu state without initialization parameters."""
@@ -55,28 +81,40 @@ class MenuTestCase(unittest.TestCase):
self.assertEqual(expected_order, menu.order)
self.assertEqual(0, len(menu.items))
- @unittest.skip('requires configuring Django beforehand')
def test_get(self):
"""Verify that a menu item can be correctly retrieved."""
expected_label = 'Label2'
expected_icon = 'Icon2'
- expected_url = '/ddd/eee/fff/'
+ expected_url = 'index'
+ reversed_url = reverse(expected_url)
expected_order = 2
menu = Menu()
- menu.add_item(expected_label, expected_icon, expected_url,
+ menu.add_item(expected_label, expected_icon, reversed_url,
expected_order)
actual_item = menu.get(expected_url)
self.assertIsNotNone(actual_item)
self.assertEqual(expected_label, actual_item.label)
self.assertEqual(expected_icon, actual_item.icon)
- self.assertEqual(expected_url, actual_item.url)
+ self.assertEqual(reversed_url, actual_item.url)
self.assertEqual(expected_order, actual_item.order)
self.assertEqual(0, len(actual_item.items))
+ def test_get_with_item_not_found(self):
+ """Verify that a KeyError is raised if a menu item is not found."""
+ expected_label = 'Label3'
+ expected_icon = 'Icon3'
+ expected_url = 'index'
+ expected_order = 3
+ menu = Menu()
+ menu.add_item(expected_label, expected_icon, expected_url,
+ expected_order)
+
+ self.assertRaises(KeyError, menu.get, expected_url)
+
def test_sort_items(self):
"""Verify that menu items are sorted correctly."""
- menu = self.build_menu()
+ menu = build_menu()
# Verify that the order of every item is equal to or greater
# than the order of the item preceding it
@@ -84,16 +122,30 @@ class MenuTestCase(unittest.TestCase):
self.assertGreaterEqual(menu.items[index].order,
menu.items[index - 1].order)
- @unittest.skip('requires configuring Django beforehand')
def test_add_urlname(self):
"""Verify that a named URL can be added to a menu correctly."""
+ expected_label = 'Label4'
+ expected_icon = 'Icon4'
+ expected_url = 'index'
+ reversed_url = reverse(expected_url)
+ expected_order = 4
+ menu = Menu()
+ actual_item = menu.add_urlname(expected_label, expected_icon,
+ expected_url, expected_order)
+
+ self.assertIsNotNone(actual_item)
+ self.assertEqual(expected_label, actual_item.label)
+ self.assertEqual(expected_icon, actual_item.icon)
+ self.assertEqual(reversed_url, actual_item.url)
+ self.assertEqual(expected_order, actual_item.order)
+ self.assertEqual(0, len(actual_item.items))
def test_add_item(self):
"""Verify that a menu item can be correctly added."""
- expected_label = 'Label3'
- expected_icon = 'Icon3'
- expected_url = '/ggg/hhh/iii/'
- expected_order = 3
+ expected_label = 'Label5'
+ expected_icon = 'Icon5'
+ expected_url = '/jjj/kkk/lll/'
+ expected_order = 5
menu = Menu()
actual_item = menu.add_item(expected_label, expected_icon,
expected_url, expected_order)
@@ -105,10 +157,9 @@ class MenuTestCase(unittest.TestCase):
self.assertEqual(expected_order, actual_item.order)
self.assertEqual(0, len(actual_item.items))
- @unittest.skip('requires configuring Django beforehand')
def test_active_item(self):
"""Verify that an active menu item can be correctly retrieved."""
- menu = self.build_menu()
+ menu = build_menu()
for index in range(1, 8):
request = HttpRequest()
@@ -120,33 +171,12 @@ class MenuTestCase(unittest.TestCase):
else:
self.assertIsNone(item)
- @unittest.skip('requires configuring Django beforehand')
def test_active_item_when_inside_subpath(self):
- """Verify that the current URL could be a sub-path of menu item."""
- menu = self.build_menu()
+ """Verify that the current URL could be a sub-path of a menu item."""
+ menu = build_menu()
expected_url = URL_TEMPLATE.format(1, 1, 1)
request = HttpRequest()
request.path = expected_url + 'd/e/f/'
item = menu.active_item(request)
self.assertEqual('Item1', item.label)
self.assertEqual(expected_url, item.url)
-
- # Helper methods
-
- def build_menu(self, size=5):
- """Build a menu with the specified number of items."""
- random.seed()
- item_data = []
- for index in range(1, size + 1):
- item_data.append(['Item' + str(index),
- 'Icon' + str(index),
- URL_TEMPLATE.format(index, index, index),
- random.randint(0, 1000)])
- menu = Menu()
- for data in item_data:
- menu.add_item(data[0], data[1], data[2], data[3])
- return menu
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/setup.py b/setup.py
index 9c5c1ec9a..67bb9572f 100755
--- a/setup.py
+++ b/setup.py
@@ -93,7 +93,7 @@ setuptools.setup(
packages=find_packages(include=['plinth', 'plinth.*'],
exclude=['*.templates']),
scripts=['bin/plinth'],
- test_suite='plinth.tests.TEST_SUITE',
+ test_suite='plinth.tests.runtests.run_tests',
license='COPYING',
classifiers=[
'Development Status :: 3 - Alpha',