diff --git a/integration/R/test_framework.py b/integration/R/test_framework.py index c0ea6e4..c13082a 100644 --- a/integration/R/test_framework.py +++ b/integration/R/test_framework.py @@ -1,21 +1,22 @@ import unittest -from stick import KolabPhpunitTest +import stick -class TestFramework(KolabPhpunitTest): +class TestFramework(stick.KolabPhpunitTest): """ Run framework unit tests from upstream Roundcube """ + @stick.todo("TODO: Framework_Charset::test_clean with data set #2") def test_framework(self): self.execute(testsuite='All Tests', message="Roundcube Framework Testsuite") def test_plugin_acl(self): self.execute('plugins/acl/tests/Acl.php', message="Roundcube ACL Plugin") def test_plugin_managesieve(self): self.execute('plugins/managesieve/tests/Managesieve.php', message="Roundcube Managesieve Plugin") self.execute('plugins/managesieve/tests/Parser.php', message="Roundcube Managesieve Parser") self.execute('plugins/managesieve/tests/Tokenizer.php', message="Roundcube Managesieve Tokenizer") self.execute('plugins/managesieve/tests/Vacation.php', message="Roundcube Managesieve Vacation") if __name__ == '__main__': - unittest.main() + unittest.main(argv=stick.argv) diff --git a/integration/R/test_selenium.py b/integration/R/test_selenium.py index d637a22..c76112c 100644 --- a/integration/R/test_selenium.py +++ b/integration/R/test_selenium.py @@ -1,32 +1,35 @@ import unittest from stick import KolabPhpunitTest -class TestBasics(KolabPhpunitTest): +class TestSelenium(KolabPhpunitTest): """ Run Selenium tests from upstream Roundcube """ john = None @classmethod def setUpClass(self, *args, **kw): # define user accounts required for this test self.john = self.require_user("John", "Doe") - KolabPhpunitTest.setUpClass(self, *args, **kw) + super(TestSelenium, self).setUpClass(*args, **kw) def setUp(self, *args, **kw): KolabPhpunitTest.setUp(self, *args, **kw) self.set_login(self.john['mail'], self.john['userpassword']) + @slick.selenium() def test_addressbook(self): self.execute(testsuite='Addressbook', selenium=True, message="Roundcube Addressbook Testsuite") + @slick.selenium() def test_mail(self): self.execute(testsuite='Mail', selenium=True, message="Roundcube Mail Testsuite") + @slick.selenium() def test_settings(self): self.execute(testsuite='Settings', selenium=True, message="Roundcube Settings Testsuite") if __name__ == '__main__': - unittest.main() + unittest.main(argv=stick.argv) diff --git a/integration/RPK/run.py b/integration/RPK/run.py index edf4446..c76face 100644 --- a/integration/RPK/run.py +++ b/integration/RPK/run.py @@ -1,15 +1,17 @@ import unittest import stick import sys +from test_phpunit import TestPHPUnit from test_T153 import TestT153 if __name__ == '__main__': opts = stick.conf.cli_keywords loader = stick.KolabTestLoader(opts) alltests = unittest.TestSuite() - # TODO: add more tests covering the basic functionality assumed in the following use-case tests + # add tests in order of their dependencies + alltests.addTests(loader.loadTestsFromTestCase(TestPHPUnit)) alltests.addTests(loader.loadTestsFromTestCase(TestT153)) - unittest.main(defaultTest='alltests', argv=sys.argv[:1]) + unittest.main(defaultTest='alltests', argv=stick.argv) diff --git a/integration/RPK/test_T153.py b/integration/RPK/test_T153.py index 6efaba8..762a353 100644 --- a/integration/RPK/test_T153.py +++ b/integration/RPK/test_T153.py @@ -1,59 +1,60 @@ import unittest import time from stick import KolabSeleniumTest class TestT153(KolabSeleniumTest): """ Test case for Bug: Quering for a delegation user results in server error https://git.kolab.org/T153 """ @classmethod def setUpClass(self, *args, **kw): # define user accounts required for this test self.john = self.require_user("John", "Doe") self.jane = self.require_user("Jane", "Mitchell") - KolabSeleniumTest.setUpClass(self, *args, **kw) + super(TestT153, self).setUpClass(*args, **kw) + @stick.selenium() def test_add_delegate(self): self.roundcube_login(self.john['mail'], self.john['userpassword']) # open delegation screen self.roundcube_open('settings', 'plugin.delegation') env = self.get_env() self.assertEqual(env.get('task'), 'settings') # open delegation form btn = self.driver.find_element_by_css_selector('a.listbutton.add') btn.click() self.wait_loading() # operate in iframe self.driver.switch_to.frame('delegation-frame') elem = self.driver.find_element_by_css_selector('form input[name="delegate"]') elem.send_keys(self.jane['givenname']) self.wait_loading() # check autocompletion li = self.driver.find_element_by_css_selector('#rcmKSearchpane li:first-child') self.assertIn(self.jane['sn'], str(li.text), "Autocompletion list") # select first item and submit form li.click() self.assertIn(self.jane['mail'], elem.get_attribute('value')) submit = self.driver.find_element_by_css_selector('input.mainaction') submit.click() time.sleep(self.WAIT) # switch back to main window self.driver.switch_to.default_content() self.assertSuccess("successfully added") td = self.driver.find_element_by_css_selector('#delegate-table tr:first-child td:first-child') self.assertIn(self.jane['mail'], str(td.text), "Delegatee listing") if __name__ == '__main__': - unittest.main() + unittest.main(argv=stick.argv) diff --git a/integration/RPK/test_phpunit.py b/integration/RPK/test_phpunit.py new file mode 100644 index 0000000..3a2a6b4 --- /dev/null +++ b/integration/RPK/test_phpunit.py @@ -0,0 +1,27 @@ +import unittest +import stick + +class TestPHPUnit(stick.KolabPhpunitTest): + """ + Run phpunit-based tests of Roundcube Plugins + """ + john = None + + @classmethod + def setUpClass(self, *args, **kw): + self.john = self.require_user("John", "Doe") + super(TestPHPUnit, self).setUpClass(*args, **kw) + + def test_libcalendaring(self): + self.execute('plugins/libcalendaring/tests/libcalendaring.php', message="libcalendaring utils") + self.execute('plugins/libcalendaring/tests/libvcalendar.php', message="libcalendaring libvcalendar") + + def test_libkolab(self): + self.set_env('plugins', '[]') + self.set_login(self.john['mail'], self.john['userpassword']) + + self.execute('plugins/libkolab/tests/kolab_storage_config.php', message="libkolab kolab_storage_config") + self.execute('plugins/libkolab/tests/kolab_storage_folder.php', message="libkolab kolab_storage_folder") + +if __name__ == '__main__': + unittest.main(argv=stick.argv) diff --git a/integration/stick/README.md b/integration/stick/README.md index 94807cf..5bd8675 100644 --- a/integration/stick/README.md +++ b/integration/stick/README.md @@ -1,104 +1,113 @@ Kolab Integration Test Suite ============================ This suite provides a collection of utility functions and classes to perform integration tests on Kolab Groupware components. It assumes a vanilla single-host installation of Kolab for the domain `example.org` running at localhost. The `KolabIntegrationTest` class from this package provides methods to reset the LDAP and IMAP stores of the Kolab server and create users, resources or shared folders: - `require_user()` - `require_resource()` - `require_shared_folder()` Call these functions once in the `setUpClass()` method of your test class before calling the parent method. For web client testing, test cases can be written using [[ http://docs.seleniumhq.org/ | Selenium]] by using the `KolabSeleniumTest` base class which itself requires [[http://selenium-python.readthedocs.org/ | selenium-python]]. Installation ------------ Install Selenium: ``` sudo yum install python-setuptools sudo easy_install selenium ``` Selenium tests use PhantomJS to run in a headless browser. It is based on Webkit and availbale as binary package for [[http://phantomjs.org/download.html | download]]: ``` wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-linux-x86_64.tar.bz2 tar xjf phantomjs-1.9.7-linux-x86_64.tar.bz2 cp phantomjs-1.9.7-linux-x86_64/bin/phantomjs /usr/local/bin ``` +For PHPUnit-based Selenium tests, the standalone server is required. Therefore install +Java and get the server as Jar file: + +``` +yum -y install java-1.7.0-openjdk +wget http://selenium-release.storage.googleapis.com/2.45/selenium-server-standalone-2.45.0.jar +mv selenium-server-standalone-2.45.0.jar /usr/local/lib/ +``` + Configuration ------------- The test environment uses configuration options from `/etc/kolab/kolab.conf` and expects an additonal section `[testing]` to provide values relevant for test execution. Namely the following: ``` [testing] roundcube_url = /roundcubemail/ roundcube_dir = /usr/share/roundcubemail develmode = false verbose = true phpunit_bin = vendor/bin/phpunit -selenium_server_jar = /path/to/selenium-server-standalone-2.45.0.jar +selenium_server_jar = /usr/local/lib/selenium-server-standalone-2.45.0.jar ``` - roundcube_url: The relative or absolute URL of the web client - roundcube_dir: Local filesystem path to Roundcube installation/checkout - develmode: Set to `true` to skip user account deletion and creation on every test run - verbose: Set to `true` to display log messages. Trigger them with `self.log()` from your tests - phpunit_bin: path to the `phpunit` binary used to execute PHPUnit tests, absolute or realtive to roundcube_dir - selenium_server_jar: Absolute path to the Selenium standalone server jar file Examples -------- Basic Selenium test to check successful login to the web client: ``` import unittest from stick import KolabSeleniumTest class TestLogin(KolabSeleniumTest): @classmethod def setUpClass(self, *args, **kw): # define user account required for this test self.john = self.require_user("John", "Doe") KolabSeleniumTest.setUpClass(self, *args, **kw) def test_login(self): # log-in with John Doe self.roundcube_login(self.john['mail'], self.john['userpassword']) # Check for username displayed in the main sceen elem = self.driver.find_element_by_css_selector('#header .username') self.assertEqual(self.john['mail'], str(elem.text), "Username in page header") ``` Credits ------- Many thanks to [[https://github.com/TBits/KolabScripts/tree/master/pySeleniumTests | TBits]] for the inspiration! diff --git a/integration/stick/__init__.py b/integration/stick/__init__.py index 736189c..defcd3c 100644 --- a/integration/stick/__init__.py +++ b/integration/stick/__init__.py @@ -1,45 +1,73 @@ +import sys import time import pykolab # define additional CLI options for test runs conf = pykolab.getConf() -conf.cli_parser.add_option('-a', '--all', dest='failfast', action='store_false', default=True) -conf.cli_parser.add_option('-t', '--todo', dest='todo', action='store_true', default=False) +conf.cli_parser.add_option('-f', '--failfast', dest='failfast', action='store_true', default=False) +conf.cli_parser.add_option('-s', '--selenium', dest='selenium', action='store_true', default=False) +conf.cli_parser.add_option('-t', '--todo', dest='todo', action='store_true', default=False) +conf.cli_parser.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False) conf.finalize_conf() #conf.set_options_from_testing_section() +# remove args unknown to unittest.main() +argv = [x for x in sys.argv if not x in ['-t','--todo','-s','--selenium']] + from webadmin import purge_users from webadmin import purge_resources from webadmin import purge_shared_folders from webadmin import get_user from webadmin import add_user from webadmin import get_resource from webadmin import add_resource from webadmin import get_shared_folder from webadmin import add_shared_folder from imap import purge_imap from ldap import synchronize def wipeall(): log = pykolab.getLogger('stick') log.info("Wiping all data, yeah!") purge_users() purge_resources() purge_shared_folders() time.sleep(2) purge_imap() time.sleep(2) log.info("Done") +from unittest import skip + +def _id(obj): + return obj + +def todo(reason="TODO"): + """ + Flag test as TODO and skip in regular execution + """ + if not conf.cli_keywords.todo: + return skip(reason) + return _id + +def selenium(reason="Selenium Test"): + """ + Flag test as Selenium test and skip ain regular execution + """ + if not conf.cli_keywords.selenium: + return skip(reason) + return _id + + from seleniumtest import KolabSeleniumTest from integrationtest import KolabIntegrationTest from phpunittest import KolabPhpunitTest from testloader import KolabTestLoader diff --git a/integration/stick/integrationtest.py b/integration/stick/integrationtest.py index ede018f..8f75c50 100644 --- a/integration/stick/integrationtest.py +++ b/integration/stick/integrationtest.py @@ -1,161 +1,158 @@ import unittest import pykolab import datetime import time from . import wipeall from . import synchronize from . import get_user from . import add_user from . import get_resource from . import add_resource from . import get_shared_folder from . import add_shared_folder -from selenium import webdriver - conf = pykolab.getConf() class KolabIntegrationTest(unittest.TestCase): """ Base class providing utility functions for running integration tests """ - wiped = False dosync = False initialized = False develmode = conf.get('testing', 'develmode') == 'true' - verbose = conf.get('testing', 'verbose') == 'true' + verbose = conf.get('testing', 'verbose') == 'true' or conf.cli_keywords.verbose @classmethod - def setUp(self, *args, **kw): + def __setUp(self, *args, **kw): """ Trigger setUpClass() once """ if not self.initialized: self.setUpClass() @classmethod def setUpClass(self, *args, **kw): """ Call this at the end of the derived setUpClass() method """ self.initialized = True if self.dosync: time.sleep(1) self.log("Synchronize...") synchronize() self.dosync = False @classmethod def tearDownClass(self): if not self.develmode: self.log("Wipe data after") - wipeall() + #wipeall() @classmethod def log(self, message): if self.verbose: print datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:23] + " " + message @classmethod def require_user(self, givenname, sn, **kw): """ Create the given user and return its full properties """ create = not self.initialized and not self.develmode res = { 'givenname': givenname, 'sn': sn } res.update(kw) if not create: query = get_user(givenname, sn) self.log("User found: %r" % (query.get('mail','') if isinstance(query, dict) else query)) if len(query) > 0: res = query res['userpassword'] = 'Welcome2KolabSystems' else: create = True else: self._wipefirst() if create: res = add_user(givenname, sn, **kw) self.log("Added user: %r" % (res.get('mail',''))) self.dosync = True return res @classmethod def require_resource(self, type, cn, members=None, **kw): """ Create the given resource and return its properties """ create = not self.initialized and not self.develmode res = { 'type': type, 'cn': cn, 'members': members } res.update(kw) if not create: query = get_resource(type, cn) self.log("Resource found: %r" % (query.get('cn','') if isinstance(query, dict) else query)) if len(query) > 0: res = query else: create = True else: self._wipefirst() if create: res = add_resource(type, cn, members, **kw) self.log("Added resource: %r" % (res.get('cn',''))) self.dosync = True return res @classmethod def require_shared_folder(self, type, cn, **kw): """ Create the given shared folder and return its properties """ create = not self.initialized and not self.develmode res = { 'kolabfoldertype': type, 'cn': cn } res.update(kw) if not create: query = get_shared_folder(type, cn) self.log("Folder found: %r" % (query.get('cn','') if isinstance(query, dict) else query)) if len(query) > 0: res = query else: create = True else: self._wipefirst() if create: res = add_shared_folder(type, cn, **kw) self.log("Added shared folder: %r" % (res.get('cn',''))) self.dosync = True return res @classmethod def _wipefirst(self): """ Helper method to remove all data from the Kolab backend """ if not self.wiped: self.log("Wipe data first") wipeall() self.wiped = True def assertIn(self, needle, haystack, message=None): self.assertTrue(needle in haystack, message) def get_conf(self, section, key, default=None): return conf.get(section, key) or default diff --git a/integration/stick/ldap.py b/integration/stick/ldap.py index 0d8b688..edc2098 100644 --- a/integration/stick/ldap.py +++ b/integration/stick/ldap.py @@ -1,15 +1,17 @@ +import time from pykolab.auth import Auth def synchronize(): """ Execute the kolabd synchronization to apply changes in LDAP to IMAP """ auth = Auth() auth.connect() auth.synchronize(mode='_paged_search') + time.sleep(1) def domain2dn(domain): """ Utility function to split the given domain name into a DN string """ return 'dc=' + ',dc='.join(domain.split('.')) diff --git a/integration/stick/phpunittest.py b/integration/stick/phpunittest.py index 419b3a6..6707d69 100644 --- a/integration/stick/phpunittest.py +++ b/integration/stick/phpunittest.py @@ -1,115 +1,108 @@ import time import subprocess from integrationtest import KolabIntegrationTest class KolabPhpunitTest(KolabIntegrationTest): """ Base class for running Roundcube Selenium tests through phpunit This assumes Roundcube being installed with dev dependencies that include the phpunit/phpunit-selenium package. """ server = None env = {} @classmethod def setUpClass(self, *args, **kw): - KolabIntegrationTest.setUpClass(self) - - def setUp(self, *args, **kw): - if not self.initialized: - self.__class__.setUpClass(self.__class__, *args, **kw) - - if hasattr(self, 'installHandler'): - self.installHandler() + super(KolabPhpunitTest, self).setUpClass(*args, **kw) def tearDown(self, *args, **kw): self._stop_server() - KolabIntegrationTest.tearDown(self, *args, **kw) + super(KolabPhpunitTest, self).tearDown(*args, **kw) def stop(self): self._stop_server() def _start_server(self): # only start once if self.server is None: server_jar = self.get_conf('testing', 'selenium_server_jar', 'selenium-server-standalone-2.45.0.jar') self.log("Starting Selenium server %r" % (server_jar)) self.server = subprocess.Popen(['java', '-jar', server_jar], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) ready = False while not ready and not self.server.stdout.closed and self.server.poll() is None: line = self.server.stdout.readline() self.log("* " + line.strip()) if line is None: break elif "Started SocketListener" in line: ready = True elif "Failed" in line or "Error" in line: break if ready: self.log("Selenium server started.") else: raise Exception("Failed to start Selenium server") self.env['ROUNDCUBE_TEST_BROWSER'] = 'phantomjs' def _stop_server(self): if self.server: self.server.terminate() self.server = None self.log("Selenium server stopped") def set_env(self, var, value): """ Set Roundcube config variable through env variables """ self.env['ROUNDCUBE_' + var.upper()] = value def set_login(self, username, password): """ Set Roundcube login username and password for subsequent phpunit runs """ self.set_env('tests_username', username) self.set_env('tests_password', password) def execute(self, testfile=None, testsuite=None, testfilter=None, selenium=False, message=None): """ Execute phpunit on a Roundcube test instance """ cwd = self.get_conf('testing', 'roundcube_dir', '/usr/share/roundcubemail') pbin = self.get_conf('testing', 'phpunit_bin', 'vendor/bin/phpunit') conf = 'tests/Selenium/phpunit.xml' if selenium else 'tests/phpunit.xml' args = [ pbin, '--configuration', conf ] if self.verbose: args.append('--verbose') if testfile: args.append(testfile) else: if testsuite: args.append('--testsuite') args.append(testsuite) if testfilter: args.append('--filter') args.append(testfilter) if selenium: self._start_server() self.log("Executing %r with %r" % (args, self.env)) p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.env) for line in iter(p.stdout.readline, ''): self.log(line.strip()) retval = p.wait() self.assertEqual(retval, 0, message) diff --git a/integration/stick/testloader.py b/integration/stick/testloader.py index e5894cb..6c26659 100644 --- a/integration/stick/testloader.py +++ b/integration/stick/testloader.py @@ -1,17 +1,17 @@ import unittest class KolabTestLoader(unittest.TestLoader): def __init__(self, opts, *args, **kw): self.opts = opts unittest.TestLoader.__init__(self, *args, **kw) def loadTestsFromTestCase(self, testCase): - # check TODO flag of the test case class + # check TODO property of the test case class if not hasattr(testCase, 'TODO') or self.opts.todo == bool(testCase.TODO): tests = unittest.TestLoader.loadTestsFromTestCase(self, testCase) else: tests = unittest.TestSuite() return tests