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',