diff --git a/tests/test_application.py b/tests/test_application.py new file mode 100644 index 0000000..b454d8b --- /dev/null +++ b/tests/test_application.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +"""Tests for the main app.""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals, +) + +import itertools +import json +import logging +import os +import subprocess +from unittest import TestCase + +import jupyter_core.paths +import nose.tools as nt +from jupyter_contrib_core.notebook_compat import nbextensions +from jupyter_contrib_core.testing_utils import ( + get_logger, patch_traitlets_app_logs, +) +from jupyter_contrib_core.testing_utils.jupyter_env import patch_jupyter_dirs +from traitlets.config import Config +from traitlets.tests.utils import check_help_all_output, check_help_output + +from jupyter_contrib_nbextensions.application import main as main_app +from jupyter_contrib_nbextensions.application import ( + BaseContribNbextensionsApp, BaseContribNbextensionsInstallApp, + ContribNbextensionsApp, InstallContribNbextensionsApp, + UninstallContribNbextensionsApp, +) + +app_classes = ( + BaseContribNbextensionsApp, BaseContribNbextensionsInstallApp, + ContribNbextensionsApp, + InstallContribNbextensionsApp, UninstallContribNbextensionsApp, +) + + +class AppTest(TestCase): + """Tests for the main app.""" + + @classmethod + def setup_class(cls): + cls.log = cls.log = get_logger(cls.__name__) + cls.log.handlers = [] + cls.log.propagate = True + + def setUp(self): + """Set up test fixtures for each test.""" + (jupyter_patches, self.jupyter_dirs, + remove_jupyter_dirs) = patch_jupyter_dirs() + for ptch in jupyter_patches: + ptch.start() + self.addCleanup(ptch.stop) + self.addCleanup(remove_jupyter_dirs) + for klass in app_classes: + patch_traitlets_app_logs(klass) + klass.log_level.default_value = logging.DEBUG + + def _check_install(self, dirs): + installed_files = [] + for tree_dir in dirs.values(): + in_this_tree = [] + for root, subdirs, files in os.walk(tree_dir, followlinks=True): + in_this_tree.extend([os.path.join(root, f) for f in files]) + nt.assert_true( + in_this_tree, + 'Install should create file(s) in {}'.format(*dirs.values())) + installed_files.extend(in_this_tree) + + return installed_files + + def _check_uninstall(self, dirs, installed_files): + # check that nothing remains in the data directory + data_installed = [ + path for path in installed_files + if path.startswith(dirs['data']) and os.path.exists(path)] + nt.assert_false( + data_installed, + 'Uninstall should remove all data files from {}'.format( + dirs['data'])) + # check the config directory + conf_installed = [ + path for path in installed_files + if path.startswith(dirs['conf']) and os.path.exists(path)] + for path in conf_installed: + with open(path, 'r') as f: + conf = Config(json.load(f)) + confstrip = {} + confstrip.update(conf) + confstrip.pop('NotebookApp', None) + confstrip.pop('version', None) + nt.assert_false(confstrip, 'disable should leave config empty.') + + def _get_default_check_kwargs(self, argv, dirs): + if argv is None: + argv = [] + if dirs is None: + dirs = { + 'conf': jupyter_core.paths.jupyter_config_dir(), + 'data': jupyter_core.paths.jupyter_data_dir(), + } + return argv, dirs + + def _call_main_app(self, argv): + main_app(argv=argv) + # a bit of a hack to allow initializing a new app instance + for klass in app_classes: + klass.clear_instance() + + def _check_subproc(self, args): + proc = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + try: + output, unused_err = proc.communicate() + except: + proc.kill() + proc.wait() + raise + print(output.decode()) # this gets it captured by nose + retcode = proc.poll() + nt.assert_equal(retcode, 0, 'command should exit with code 0') + + def check_app_install(self, argv=None, dirs=None): + """Check files were installed in the correct place.""" + argv, dirs = self._get_default_check_kwargs(argv, dirs) + self._call_main_app(argv=['install'] + argv) + installed_files = self._check_install(dirs) + self._call_main_app(argv=['uninstall'] + argv) + self._check_uninstall(dirs, installed_files) + + def check_cli_install(self, argv=None, dirs=None): + argv, dirs = self._get_default_check_kwargs(argv, dirs) + args = InstallContribNbextensionsApp.name.split(' ') + argv + self._check_subproc(args) + installed_files = self._check_install(dirs) + args = UninstallContribNbextensionsApp.name.split(' ') + argv + self._check_subproc(args) + self._check_uninstall(dirs, installed_files) + + def test_00_extra_args(self): + """Check that app complains about extra args.""" + for subcom in ('install', 'uninstall'): + # sys.exit should be called if extra args specified + with nt.assert_raises(SystemExit): + main_app([subcom, 'arbitrary_extension_name']) + for klass in app_classes: + klass.clear_instance() + + def test_01_help_output(self): + """Check that app help works.""" + app_module = 'jupyter_contrib_nbextensions.application' + for subcommand in (None, ['install'], ['uninstall']): + check_help_output(app_module, subcommand=subcommand) + check_help_all_output(app_module, subcommand=subcommand) + # sys.exit should be called if empty argv specified + with nt.assert_raises(SystemExit): + main_app([]) + + def test_02_argument_conflict(self): + """Check that install objects to multiple flags.""" + conflicting_flags = ('--user', '--system', '--sys-prefix') + conflicting_flagsets = [] + for nn in range(2, len(conflicting_flags) + 1): + conflicting_flagsets.extend( + itertools.combinations(conflicting_flags, nn)) + for subcommand in ('install', 'uninstall'): + for flagset in conflicting_flagsets: + self.log.info('testing conflicting flagset {}'.format(flagset)) + nt.assert_raises(nbextensions.ArgumentConflict, + main_app, [subcommand] + list(flagset)) + + def test_03_app_install_defaults(self): + """Check that app install works correctly using defaults.""" + self.check_app_install() + + def test_04_cli_install_defaults(self): + """Check that cli install works correctly using defaults.""" + self.check_cli_install() + + def test_05_app_install_user(self): + """Check that app install works correctly using --user flag.""" + self.check_app_install(argv=['--user']) + + def test_06_cli_install_user(self): + """Check that cli install works correctly using --user flag.""" + self.check_cli_install(argv=['--user']) + + def test_07_app_install_sys_prefix(self): + """Check that app install works correctly using --sys-prefix flag.""" + self.check_app_install( + dirs=self.jupyter_dirs['sys_prefix'], argv=['--sys-prefix']) + + # don't test cli install with --sys-prefix flag, as we can't patch + # directories in the subprocess + + def test_08_app_install_system(self): + """Check that app install works correctly using --system flag.""" + self.check_app_install( + dirs=self.jupyter_dirs['system'], argv=['--system']) + + # don't test cli install with --system flag, as we can't patch + # directories in the subprocess + + def test_09_app_install_symlink(self): + """Check that app install works correctly using --symlink flag.""" + self.check_app_install(argv=['--symlink']) + + def test_10_cli_install_symlink(self): + """Check that cli install works correctly using --symlink flag.""" + self.check_cli_install(argv=['--symlink']) + + def test_11_app_install_nbextensions_dir(self): + """Check that app install works correctly using --nbextensions arg.""" + self.check_app_install( + dirs={'conf': self.jupyter_dirs['system']['conf'], + 'data': self.jupyter_dirs['custom']['data'], }, + # we need to set user flag false to avoid ArgumentConflict + argv=[ + '--BaseContribNbextensionsInstallApp.user=False', + '--nbextensions={}'.format( + os.path.join( + self.jupyter_dirs['custom']['data'], 'nbextensions'))]) + + # We can't test cli install using nbextensions_dir, since it edits system + # config, and we can't patch directories in the subprocess diff --git a/tests/test_migrate.py b/tests/test_migrate.py new file mode 100644 index 0000000..209e78e --- /dev/null +++ b/tests/test_migrate.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +"""Tests for the retiring old IPython-notebook-extensions installations.""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals, +) + +import imp +import json +import os +from unittest import TestCase + +import nose.tools as nt +import pip +from jupyter_contrib_core.testing_utils import ( + get_logger, patch_traitlets_app_logs, +) +from jupyter_contrib_core.testing_utils.jupyter_env import patch_jupyter_dirs + +import jupyter_contrib_nbextensions.application +import jupyter_contrib_nbextensions.migrate + + +def refresh_packages(): + """ + Refresh pip's list of installed packages. + + Rather hackish, taken from https://github.com/pypa/pip/issues/2695 + """ + pip.utils.pkg_resources = imp.reload(pip.utils.pkg_resources) + + +def get_installed_project_names(): + """Return all installed project names as reported by (refreshed) pip.""" + refresh_packages() + return [p.project_name for p in pip.get_installed_distributions()] + + +class MigrateTest(TestCase): + """Tests for retiring old IPython-notebook-extensions installs.""" + old_pkg_git_ref = 'master' + old_pkg_project_name = 'Python-contrib-nbextensions' + + @classmethod + def setup_class(cls): + cls.log = cls.log = get_logger(cls.__name__) + cls.log.handlers = [] + cls.log.propagate = True + + @classmethod + def get_old_pkg_url(cls): + return ('https://github.com/ipython-contrib/' + 'IPython-notebook-extensions/archive/{}.zip').format( + cls.old_pkg_git_ref) + + def setUp(self): + """Set up test fixtures.""" + (jupyter_patches, self.jupyter_dirs, + remove_jupyter_dirs) = patch_jupyter_dirs() + for ptch in jupyter_patches: + ptch.start() + self.addCleanup(ptch.stop) + self.addCleanup(remove_jupyter_dirs) + + def install_old_pkg(self): + old_pkg_project_name = self.old_pkg_project_name + # install old repo version + pip.main(['install', '-v', self.get_old_pkg_url()]) + # check pip installed ok + nt.assert_in( + old_pkg_project_name, get_installed_project_names(), + 'pip should list {} as installed'.format(old_pkg_project_name)) + # list all files that got installed + installed = {} + for sect, dirpath in self.jupyter_dirs['env_vars'].items(): + installed[sect] = [] + for root, subdirs, files in os.walk(dirpath): + installed[sect].extend([os.path.join(root, f) for f in files]) + nt.assert_true(installed, + 'Old install should create files in {}'.format( + dirpath)) + + return installed + + def check_old_pkg_uninstalled(self, installed): + old_pkg_project_name = self.old_pkg_project_name + # check pip uninstalled ok + nt.assert_not_in( + old_pkg_project_name, get_installed_project_names(), + 'pip should no longer list {} as installed'.format( + old_pkg_project_name)) + # check that nothing remains of the old install + remaining = [pp for pp in installed['data'] if os.path.exists(pp)] + nt.assert_false( + remaining, '{} should remove all data files from {}'.format( + 'jupyter_contrib_nbextensions.migrate', + self.jupyter_dirs['env_vars']['data'])) + # check that python configs are empty + for pp in [pp for pp in installed['conf'] + if (os.path.exists(pp) and pp.endswith('.py'))]: + with open(pp, 'r') as f: + contents = f.read() + nt.assert_false( + contents.strip(), + '.py config file {} contains text:\n{}'.format(pp, contents)) + # check that json configs are essentially empty + for pp in [pp for pp in installed['conf'] + if (os.path.exists(pp) and pp.endswith('.json'))]: + with open(pp, 'r') as f: + contents = f.read() + conf = json.loads(contents) + # ignore keys added by jupyter_nbextensions_configurator + load_exts = conf.get('load_extensions', {}) + for req in list(load_exts.keys()): + if req.startswith('nbextensions_configurator'): + load_exts.pop(req, None) + if not load_exts: + conf.pop('load_extensions', None) + conf.pop('version', None) + srvext_to_remove = 'jupyter_nbextensions_configurator' + nbapp_sect = conf.get('NotebookApp', {}) + srvxts_key = 'nbserver_extensions' + srvxts = nbapp_sect.get(srvxts_key, {}) + srvxts.pop(srvext_to_remove, None) + if not srvxts: + nbapp_sect.pop(srvxts_key, None) + srvxts_key = 'server_extensions' + srvxts = nbapp_sect.get(srvxts_key, []) + while srvext_to_remove in srvxts: + srvxts.remove(srvext_to_remove) + if not srvxts: + nbapp_sect.pop(srvxts_key, None) + if not nbapp_sect: + conf.pop('NotebookApp', None) + nt.assert_false( + conf, + 'config file {} is not empty:\n{}'.format( + pp, json.dumps(conf, indent=2, sort_keys=True))) + + def test_01_migrate_main(self): + """Check migrate script removes old install correctly.""" + installed_files = self.install_old_pkg() + # execute the migrate script + jupyter_contrib_nbextensions.migrate.main() + self.check_old_pkg_uninstalled(installed_files) + + def test_02_migrate_app(self): + """Check migrate application removes old install correctly.""" + installed_files = self.install_old_pkg() + # execute the migrate app + klass = jupyter_contrib_nbextensions.application.MigrateContribNbextensionsApp # noqa + patch_traitlets_app_logs(klass) + jupyter_contrib_nbextensions.application.main(['migrate']) + self.check_old_pkg_uninstalled(installed_files) + + def test_03_migrate_blank(self): + """Check migrate can run correctly even without a previous install.""" + jupyter_contrib_nbextensions.migrate.main() diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 0000000..5869c9b --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +"""Test the package""" + +from jupyter_contrib_core.testing_utils import raise_on_bad_version + +from jupyter_contrib_nbextensions import __version__ + + +def test_current_version(): + """check that version string complies with pep440""" + raise_on_bad_version(__version__)