diff --git a/pycapo/_version.py b/pycapo/_version.py index 61f2905c27af849c44c2823d8f4b94d2b681e778..09d340ed7a4a399169a1a6bdf698ca028c4c450f 100644 --- a/pycapo/_version.py +++ b/pycapo/_version.py @@ -1,2 +1,2 @@ """ Version information for this package, don't put anything else here. """ -___version___ = '0.2.1' +___version___ = '0.3.1' diff --git a/pycapo/models.py b/pycapo/models.py index 158a21da000f56ea9f711b48b81e14d74d8eb503..c0382ff6d75d3afa57ee732f10ded6093b53fcbe 100644 --- a/pycapo/models.py +++ b/pycapo/models.py @@ -2,7 +2,6 @@ import os import os.path import re -import sys try: import configparser @@ -16,7 +15,7 @@ except ImportError: from pycapo import DEFAULT_CAPO_PATH -_ENV_PATTERN = re.compile('\$\{env:([a-zA-Z\_]+)\}') +_ENV_PATTERN = re.compile(r'\$\{env:([a-zA-Z\_]+)\}') class CapoConfig: @@ -262,9 +261,10 @@ class _SimpleConfigParser(configparser.RawConfigParser): NOSECTION = 'NOSECTION' def read(self, filename): - text = open(filename, 'r').read() - f = stringio.StringIO("[%s]\n" % self.NOSECTION + text) - self.readfp(f, filename) + with open(filename, 'r') as f: + text = f.read() + f = stringio.StringIO("[%s]\n" % self.NOSECTION + text) + self.read_file(f, filename) def getoption(self, option): 'get the value of an option' diff --git a/setup.py b/setup.py index 5391dd1899da35c373a40e6691e126116ea22451..2a1ff5fa62c50dfd6fd628282d8c61d5a8d3c65e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- -from setuptools import setup, find_packages +''' CAPO setup ''' + import os +from setuptools import setup, find_packages NAME = 'pycapo' HERE = os.path.abspath(os.path.dirname(__file__)) @@ -18,6 +20,8 @@ setup(name=NAME, long_description=README + '\n\n' + CHANGES, author='Stephan Witz', author_email='switz@nrao.edu', + maintainer='Janet L. Goldstein', + maintainer_email='jgoldste@nrao.edu', url='https://open-bitbucket.nrao.edu/projects/SSA/repos/pycapo', keywords='', license='GPL', @@ -43,7 +47,7 @@ setup(name=NAME, test_suite='pycapo.tests', install_requires=[], tests_require=['pytest'], - setup_requires=['pytest-runner','pytest'], + setup_requires=['pytest-runner', 'pytest'], entry_points={ 'console_scripts': [ 'pycapo = pycapo.commands:pycapo' diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_data/dev_profile.properties b/tests/test_data/dev_profile.properties new file mode 100755 index 0000000000000000000000000000000000000000..7875987a37448ae8cf9822c01153ca79ca31897f --- /dev/null +++ b/tests/test_data/dev_profile.properties @@ -0,0 +1,11 @@ +# PyCapo configuration file to set values for development context. +# Ensure that the environment variable $CAPO_PROFILE is set correctly +# to enable use of this file. + +tunes.looney.metadataDatabaseJdbcUsername = elmer +tunes.looney.metadataDatabaseJdbcPassword = wascally_wabbit +tunes.looney.metadataDatabaseJdbcUrl = jdbc:postgresql://dev.looney-toons.net/meta + +tunes.looney.someOtherDatabaseJdbcUsername = bugs_ro +tunes.looney.someOtherDatabaseJdbcPassword = 'wazzup-doc' +tunes.looney.someOtherDatabaseJdbcUrl = jdbc:oracle:thin:@//dev.looney-toons.net:1521/legacy diff --git a/tests/test_data/empty_profile.properties b/tests/test_data/empty_profile.properties new file mode 100755 index 0000000000000000000000000000000000000000..cbfb60d43ef877189f01a33a136c3103d44c35f7 --- /dev/null +++ b/tests/test_data/empty_profile.properties @@ -0,0 +1 @@ +tunes.looney.metadataDatabaseJdbcUsername = diff --git a/tests/test_data/prod_profile.properties b/tests/test_data/prod_profile.properties new file mode 100755 index 0000000000000000000000000000000000000000..55d6d65a50e726f8c91971a7e79791678f583c87 --- /dev/null +++ b/tests/test_data/prod_profile.properties @@ -0,0 +1,11 @@ +# PyCapo configuration file to set values for production context. +# Ensure that the environment variable $CAPO_PROFILE is set correctly +# to enable use of this file. + +tunes.looney.metadataDatabaseJdbcUsername = elmer +tunes.looney.metadataDatabaseJdbcPassword = wascally_wabbit +tunes.looney.metadataDatabaseJdbcUrl = jdbc:postgresql://prod.looney-toons.net/meta + +tunes.looney.someOtherDatabaseJdbcUsername = bugs +tunes.looney.someOtherDatabaseJdbcPassword = 'no, really' +tunes.looney.someOtherDatabaseJdbcUrl = jdbc:oracle:thin:@//prod.looney-toons.net:1521/legacy diff --git a/tests/test_properties.py b/tests/test_properties.py index c001f2dd19732fd133dfde14490d231074a01497..2dca14cc5dd46eed0409c0d3a4ba161ae4328bec 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -27,7 +27,7 @@ OPTIONS = ( class CapoTest: """ A class that builds a test environment for pycapo with two properties files, a/test.properties and b/test.properties, with - options defined in the OPTOINS dict above. """ + options defined in the OPTIONS dict above. """ def __init__(self, tmpdir): self.tmpdir = tmpdir diff --git a/tests/test_pycapo.py b/tests/test_pycapo.py new file mode 100644 index 0000000000000000000000000000000000000000..3b38ad10e11c343274e75d05b28fb0e04503e47d --- /dev/null +++ b/tests/test_pycapo.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +""" CapoConfig tests """ + +import os +import shutil +import subprocess +import sys +import unittest +from enum import Enum +from pathlib import Path + +import pytest + +from pycapo import CapoConfig + +_TEST_PROFILES = ['dev_profile', 'prod_profile', 'empty_profile'] + +class Keys(Enum): + ''' Keys in our fake Capo profiles ''' + METADATA_USER = 'TUNES.LOONEY.METADATADATABASE.JDBCUSERNAME' + METADATA_PW = 'TUNES.LOONEY.METADATADATABASEJDBCPASSWORD' + METADATA_URL = 'TUNES.LOONEY.METADATADATABASEJDBCURL' + OTHER_USER = 'TUNES.LOONEY.SOMEOTHERDATABASEJDBCUSERNAME' + OTHER_PW = 'TUNES.LOONEY.SOMEOTHERDATABASEJDBCPASSWORD' + OTHER_URL = 'TUNES.LOONEY.SOMEOTHERDATABASEJDBCURL' + +class PycapoTestCase(unittest.TestCase): + ''' Tests for pycapo and CapoConfig() ''' + + @classmethod + def setUpClass(cls) -> None: + cls.get_props_files(cls) + cls.capo_dir_was_created = False + + @classmethod + def tearDownClass(cls) -> None: + cls.delete_properties(cls) + + def test_capo_copes_with_bad_path(self): + ''' if capo path is bad, capo should default to something usual + rather than crashing + ''' + user = os.environ['USER'] + for profile in _TEST_PROFILES: + capo_config = CapoConfig(profile=profile, path='foo') + self.assertTrue(user in capo_config.getpath()) + try: + for key, val in capo_config.getoptions().items(): + if 'prod' not in profile: + self.assertFalse('prod' in val) + if key == Keys.OTHER_URL.value: + self.assertTrue('dev' in val) + else: + if key == Keys.METADATA_USER.value: + self.assertTrue('_ro' not in val) + if key == Keys.OTHER_URL.value: + self.assertTrue('prod' in val) + except Exception as exc: + pytest.fail(f'failure for {capo_config.profile}, ' + f'{capo_config.getpath()}: {exc}') + + def test_gets_expected_values_for_profile(self): + ''' properties files for different profiles may (or may not) + have disparate values + ''' + for profile in _TEST_PROFILES: + + try: + capo_config = CapoConfig(profile=profile) + + try: + options = capo_config.getoptions() + for key, val in options.items(): + if key == Keys.OTHER_URL: + self.assertTrue('thin' in val) + if 'prod' not in profile: + self.assertFalse('prod' in val) + if key == Keys.OTHER_URL.value: + self.assertTrue('dev' in val) + else: + if key == Keys.METADATA_USER.value: + self.assertTrue('_ro' not in val) + if key == Keys.OTHER_URL.value: + self.assertTrue('prod' in val) + except Exception as exc: + pytest.fail(f'failure for {profile} ' + f'when {key}=={val}: {exc}') + + except Exception as exc: + pytest.fail(f'failure for {profile}: {exc}') + + def test_capo_config_handles_bad_args_expectedly(self): + ''' CapoConfig() should complain when given bad args or none ''' + with pytest.raises(ValueError): + CapoConfig(profile=None) + CapoConfig() + CapoConfig(path='foo') + + bogus_config = CapoConfig(profile='bogus') + # CapoConfig() should NOT have any default values + self.assertEqual(0, len(bogus_config.getoptions())) + self.assertEqual(0, len(bogus_config.getlocations())) + + def test_missing_setting_fails_expectedly(self): + ''' missing Capo setting should return appropriate code ''' + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(profile='empty_profile') + self.assertEqual(3, exc.value) + + + def test_command_line_returns_expected_code(self): + ''' under various scenarios, pycapo should return appropriate code + for each + ''' + + # no arguments: should fail w/approp return code + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run() + self.assertEqual(2, exc.value) + # one argument, not a k/v pair: should fail w/approp return code + with pytest.raises(TypeError) as exc: + CommandLineLauncher().run('dev_profile') + self.assertEqual(2, exc.value) + # invalid k/v pair + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(foo='bar') + self.assertEqual(1, exc.value) + # key but no value + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(profile=None) + self.assertEqual(1, exc.value) + # invalid profile + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(profile='foo') + self.assertEqual(1, exc.value) + # no profile; invalid path + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(path='foo') + self.assertEqual(1, exc.value) + # no profile; valid path + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(path=Path.cwd()) + self.assertEqual(1, exc.value) + # valid profile, invalid path + with pytest.raises(SystemExit) as exc: + CommandLineLauncher().run(profile='dev_profile', path='foo') + self.assertEqual(1, exc.value) + + ### UTILITIES ### + + def get_props_files(self): + ''' grab our fake properties files and copy them to user's .capo dir ''' + user_home = Path(os.environ['HOME']) + capo_path = user_home / '.capo' + props_source_dir = Path.cwd() / 'tests/test_data' + + if not capo_path.is_dir(): + capo_path.mkdir() + self.capo_dir_was_created = True + for profile in _TEST_PROFILES: + filename = profile + '.properties' + source = props_source_dir / filename + destn = capo_path / filename + shutil.copy(str(source), str(destn)) + + + def delete_properties(self): + ''' delete fake properties files from user's .capo dir ''' + user_home = Path(os.environ['HOME']) + default_path = user_home / '.capo' + for profile in _TEST_PROFILES: + filename = profile + '.properties' + to_delete = default_path / filename + if to_delete.is_file(): + to_delete.unlink() + if self.capo_dir_was_created: + default_path.unlink() + + +class CommandLineLauncher: + ''' launches a system process and executes a pycapo command ''' + + def run(self, **kwargs): + ''' kick off pycapo with these args and grab return code ''' + args = ['pycapo'] + if kwargs: + for arg in kwargs: + args.append(arg) + + try: + proc = subprocess.run(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + timeout=100, + bufsize=1, + universal_newlines=True) + if proc.returncode != 0: + sys.exit(proc.returncode) + return proc.returncode + + except Exception as exc: + pytest.fail(f'Error launching pycapo with args {args}: {exc}') + + +if __name__ == '__main__': + unittest.main()