From 028cfaa44cbd9340376e670f3050fae6a93f8060 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Tue, 2 Feb 2016 16:45:37 +0500 Subject: [PATCH 01/29] Class config.path has been added. All setting reviewed and tuned --- headphones/config.py | 45 ++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/headphones/config.py b/headphones/config.py index 8362b934..f11cf787 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -15,6 +15,19 @@ def bool_int(value): value = 0 return int(bool(value)) +class path(str): + """Internal 'marker' type for paths in config.""" + + @staticmethod + def __call__(val): + return path(val) + + def __new__(cls, *args, **kw): + hstr = str.__new__(cls, *args, **kw) + return hstr + + def __repr__(self) : + return 'headphones.config.path(%s)' % self _CONFIG_DEFINITIONS = { 'ADD_ALBUM_ART': (int, 'General', 0), @@ -31,11 +44,11 @@ _CONFIG_DEFINITIONS = { 'AUTO_ADD_ARTISTS': (int, 'General', 1), 'BITRATE': (int, 'General', 192), 'BLACKHOLE': (int, 'General', 0), - 'BLACKHOLE_DIR': (str, 'General', ''), + 'BLACKHOLE_DIR': (path, 'General', ''), 'BOXCAR_ENABLED': (int, 'Boxcar', 0), 'BOXCAR_ONSNATCH': (int, 'Boxcar', 0), 'BOXCAR_TOKEN': (str, 'Boxcar', ''), - 'CACHE_DIR': (str, 'General', ''), + 'CACHE_DIR': (path, 'General', ''), 'CACHE_SIZEMB': (int, 'Advanced', 32), 'CHECK_GITHUB': (int, 'General', 1), 'CHECK_GITHUB_INTERVAL': (int, 'General', 360), @@ -44,8 +57,8 @@ _CONFIG_DEFINITIONS = { 'CONFIG_VERSION': (str, 'General', '0'), 'CORRECT_METADATA': (int, 'General', 0), 'CUE_SPLIT': (int, 'General', 1), - 'CUE_SPLIT_FLAC_PATH': (str, 'General', ''), - 'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''), + 'CUE_SPLIT_FLAC_PATH': (path, 'General', ''), + 'CUE_SPLIT_SHNTOOL_PATH': (path, 'General', ''), 'CUSTOMAUTH': (int, 'General', 0), 'CUSTOMHOST': (str, 'General', 'localhost'), 'CUSTOMPASS': (str, 'General', ''), @@ -53,12 +66,12 @@ _CONFIG_DEFINITIONS = { 'CUSTOMSLEEP': (int, 'General', 1), 'CUSTOMUSER': (str, 'General', ''), 'DELETE_LOSSLESS_FILES': (int, 'General', 1), - 'DESTINATION_DIR': (str, 'General', ''), + 'DESTINATION_DIR': (path, 'General', ''), 'DETECT_BITRATE': (int, 'General', 0), 'DO_NOT_PROCESS_UNMATCHED': (int, 'General', 0), - 'DOWNLOAD_DIR': (str, 'General', ''), + 'DOWNLOAD_DIR': (path, 'General', ''), 'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5), - 'DOWNLOAD_TORRENT_DIR': (str, 'General', ''), + 'DOWNLOAD_TORRENT_DIR': (path, 'General', ''), 'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0), 'EMAIL_ENABLED': (int, 'Email', 0), 'EMAIL_FROM': (str, 'Email', ''), @@ -74,14 +87,14 @@ _CONFIG_DEFINITIONS = { 'EMBED_LYRICS': (int, 'General', 0), 'ENABLE_HTTPS': (int, 'General', 0), 'ENCODER': (str, 'General', 'ffmpeg'), - 'ENCODERFOLDER': (str, 'General', ''), + 'ENCODERFOLDER': (path, 'General', ''), 'ENCODERLOSSLESS': (int, 'General', 1), 'ENCODEROUTPUTFORMAT': (str, 'General', 'mp3'), 'ENCODERQUALITY': (int, 'General', 2), 'ENCODERVBRCBR': (str, 'General', 'cbr'), 'ENCODER_MULTICORE': (int, 'General', 0), 'ENCODER_MULTICORE_COUNT': (int, 'General', 0), - 'ENCODER_PATH': (str, 'General', ''), + 'ENCODER_PATH': (path, 'General', ''), 'EXTRAS': (str, 'General', ''), 'EXTRA_NEWZNABS': (list, 'Newznab', ''), 'EXTRA_TORZNABS': (list, 'Torznab', ''), @@ -94,7 +107,7 @@ _CONFIG_DEFINITIONS = { 'FOLDER_PERMISSIONS': (str, 'General', '0755'), 'FREEZE_DB': (int, 'General', 0), 'GIT_BRANCH': (str, 'General', 'master'), - 'GIT_PATH': (str, 'General', ''), + 'GIT_PATH': (path, 'General', ''), 'GIT_USER': (str, 'General', 'rembo10'), 'GROWL_ENABLED': (int, 'Growl', 0), 'GROWL_HOST': (str, 'Growl', ''), @@ -114,8 +127,8 @@ _CONFIG_DEFINITIONS = { 'IDTAG': (int, 'Beets', 0), 'IGNORE_CLEAN_RELEASES': (int, 'General', 0), 'IGNORED_WORDS': (str, 'General', ''), - 'IGNORED_FOLDERS': (list, 'Advanced', []), - 'IGNORED_FILES': (list, 'Advanced', []), + 'IGNORED_FOLDERS': (list, 'Advanced', []), # path + 'IGNORED_FILES': (list, 'Advanced', []), # path 'INCLUDE_EXTRAS': (int, 'General', 0), 'INTERFACE': (str, 'General', 'default'), 'JOURNAL_MODE': (str, 'Advanced', 'wal'), @@ -130,17 +143,17 @@ _CONFIG_DEFINITIONS = { 'LIBRARYSCAN_INTERVAL': (int, 'General', 300), 'LMS_ENABLED': (int, 'LMS', 0), 'LMS_HOST': (str, 'LMS', ''), - 'LOG_DIR': (str, 'General', ''), + 'LOG_DIR': (path, 'General', ''), 'LOSSLESS_BITRATE_FROM': (int, 'General', 0), 'LOSSLESS_BITRATE_TO': (int, 'General', 0), - 'LOSSLESS_DESTINATION_DIR': (str, 'General', ''), + 'LOSSLESS_DESTINATION_DIR': (path, 'General', ''), 'MB_IGNORE_AGE': (int, 'General', 365), 'MININOVA': (int, 'Mininova', 0), 'MININOVA_RATIO': (str, 'Mininova', ''), 'MIRROR': (str, 'General', 'musicbrainz.org'), 'MOVE_FILES': (int, 'General', 0), 'MPC_ENABLED': (bool_int, 'MPC', False), - 'MUSIC_DIR': (str, 'General', ''), + 'MUSIC_DIR': (path, 'General', ''), 'MUSIC_ENCODER': (int, 'General', 0), 'NEWZNAB': (int, 'Newznab', 0), 'NEWZNAB_APIKEY': (str, 'Newznab', ''), @@ -234,7 +247,7 @@ _CONFIG_DEFINITIONS = { 'SUBSONIC_PASSWORD': (str, 'Subsonic', ''), 'SUBSONIC_USERNAME': (str, 'Subsonic', ''), 'SYNOINDEX_ENABLED': (int, 'Synoindex', 0), - 'TORRENTBLACKHOLE_DIR': (str, 'General', ''), + 'TORRENTBLACKHOLE_DIR': (path, 'General', ''), 'TORRENT_DOWNLOADER': (int, 'General', 0), 'TORRENT_REMOVAL_INTERVAL': (int, 'General', 720), 'TORZNAB': (int, 'Torznab', 0), From beacc6ed1e7f68f8a0ce52e3f81babb4434d86b1 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Tue, 2 Feb 2016 16:54:04 +0500 Subject: [PATCH 02/29] Prototype for test file --- headphones/config_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 headphones/config_test.py diff --git a/headphones/config_test.py b/headphones/config_test.py new file mode 100644 index 00000000..871e5bb6 --- /dev/null +++ b/headphones/config_test.py @@ -0,0 +1,9 @@ +import unittest +from unittest import TestCase +import mock + +from headphones.config import path + +class ConfigPathTest(TestCase): + def test_path(self): + self.assertFalse(True) From b063ba412f59b5095bdce6cff38cdb9228e0ef31 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 02:34:32 +0500 Subject: [PATCH 03/29] * Dummy test , example for albumart. Required for precise coverage --- headphones/albumart_test.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 headphones/albumart_test.py diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py new file mode 100644 index 00000000..690d4f92 --- /dev/null +++ b/headphones/albumart_test.py @@ -0,0 +1,7 @@ +import unittest +from unittest import TestCase +import mock + +import headphones.albumart + +# no tests... From 16c553196c053283caf611dec04a4a83b1566128 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 02:35:08 +0500 Subject: [PATCH 04/29] Default settings for nosetests (+colors +coverage) --- setup.cfg | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..300b47fb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[nosetests] +verbosity=2 +tests=headphones +rednose=1 +exclude-dir=lib + +with-coverage=1 +cover-branches=1 + +cover-html=1 +cover-html-dir=cover-html +cover-package=headphones From bc9143b2a262897bfa5c97a0acf2ea9773a560cf Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 02:35:54 +0500 Subject: [PATCH 05/29] Travis sends coverage --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9de515c8..bfaf6f7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,3 +18,6 @@ script: - pep8 headphones - pyflakes headphones - nosetests headphones + +after_success: + coveralls \ No newline at end of file From 3f8477a4423ff51ea98f0341a027ef67fbcd6c26 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 02:36:26 +0500 Subject: [PATCH 06/29] Pep8 styling --- headphones/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/config.py b/headphones/config.py index f11cf787..cd64058d 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -26,7 +26,7 @@ class path(str): hstr = str.__new__(cls, *args, **kw) return hstr - def __repr__(self) : + def __repr__(self): return 'headphones.config.path(%s)' % self _CONFIG_DEFINITIONS = { From 0d4b0065374d81bcec2fdc09a355f4ee11c9ceae Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 02:40:25 +0500 Subject: [PATCH 07/29] setup.cfg without RedNose --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 300b47fb..25decc1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [nosetests] verbosity=2 tests=headphones -rednose=1 +#rednose=1 exclude-dir=lib with-coverage=1 From a175ec5b5c1acbaa57ba450f75b1166331eb74a4 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 02:58:28 +0500 Subject: [PATCH 08/29] ignore coverage temporary files removed "exclude-dirs" from setup.cfg. Travis still cant work with nose-plugins --- .gitignore | 14 +++++++++----- setup.cfg | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a3c65e2c..ade53829 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ +[Tt]est[Rr]esult* +/cache +/logs +.project +.pydevproject + +# coverage generated: +/cover-html/ +.coverage # Compiled source # ################### @@ -61,8 +70,3 @@ Thumbs.db obj/ [Rr]elease*/ _ReSharper*/ -[Tt]est[Rr]esult* -/cache -/logs -.project -.pydevproject diff --git a/setup.cfg b/setup.cfg index 25decc1d..e47a22ee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ verbosity=2 tests=headphones #rednose=1 -exclude-dir=lib +#exclude-dir=lib with-coverage=1 cover-branches=1 From 52f071d8b0ec2d3a9dece4c946ee9b6678d08773 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:04:19 +0500 Subject: [PATCH 09/29] Update config_test.py --- headphones/config_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/config_test.py b/headphones/config_test.py index 871e5bb6..36c6e6f5 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -6,4 +6,4 @@ from headphones.config import path class ConfigPathTest(TestCase): def test_path(self): - self.assertFalse(True) + self.assertFalse(False) From f8df9c502b53fcc2869c8a2a2db660b858f890e4 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:07:23 +0500 Subject: [PATCH 10/29] travis: Coverall installing --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index b2b2e978..e12a5343 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,10 @@ install: - pip install pylint==1.3.1 - pip install pyflakes - pip install pep8 + # coverage stuff: + - pip install coveralls + - pip install coverage + script: - pep8 headphones - pyflakes headphones From 0957d2d9576bd7c095bcca302a24d1d6d1b5681e Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:21:52 +0500 Subject: [PATCH 11/29] Fixed tests --- .travis.yml | 12 ++++-- headphones/albumart_test.py | 4 +- headphones/config_test.py | 73 +++++++++++++++++++++++++++++++++++-- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index b2b2e978..acf2be82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,14 +15,18 @@ python: - "2.7" # pylint 1.4 does not run under python 2.6 install: - - pip install pyOpenSSL - - pip install pylint==1.3.1 - - pip install pyflakes - - pip install pep8 + - pip install pyOpenSSL + - pip install pylint==1.3.1 + - pip install pyflakes + - pip install pep8 + # coverage stuff: + - pip install coveralls + - pip install coverage script: - pep8 headphones - pyflakes headphones - nosetests headphones after_success: + # coverage stuff: coveralls diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py index 690d4f92..cf86b24a 100644 --- a/headphones/albumart_test.py +++ b/headphones/albumart_test.py @@ -1,6 +1,6 @@ -import unittest +#import unittest +#import mock from unittest import TestCase -import mock import headphones.albumart diff --git a/headphones/config_test.py b/headphones/config_test.py index 871e5bb6..f2f0bc95 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -1,9 +1,76 @@ -import unittest +#import unittest +#import mock from unittest import TestCase -import mock +from mock import Mock, MagicMock + +import configobj from headphones.config import path class ConfigPathTest(TestCase): def test_path(self): - self.assertFalse(True) + p = path('/tmp') + print path + self.assertIsInstance(p, path) + self.assertIsNotNone(p) + + def test_path_call(self): + s = '/tmp' + p1 = path(s) + self.assertEqual(p1, s) + + def test_path_static(self): + s = '/tmp' + p2 = path.__call__(s) + self.assertEqual(p2, s) + + def test_path_static_equal_call(self): + s = '/tmp' + p1 = path(s) + p2 = path.__call__(s) + self.assertEqual(p1, p2) + + def test_path_repr(self): + s = '/tmp' + p1 = path(s) + self.assertIn('headphones.config.path', p1.__repr__() ) + + +import headphones.config + +# patch required, since Config works ower a +@mock.patch('headphones.config.ConfigObj', name='ConfigObjMock') +class ConfigTest(TestCase): + + def putConfigToFabric(self, config_obj_fabric_mock): + """ Helper for setting up the config_obj_fabric""" + + config_obj_mock = MagicMock( ) + config_obj_fabric_mock.return_value = config_obj_mock + return config_obj_mock + + def test_create(self, config_obj_fabric_mock): + """Test creating headphones.Config""" + + cf = headphones.config.Config('/tmp/notexist') + self.assertIsInstance(cf, headphones.config.Config) + + def test_write(self, config_obj_fabric_mock): + """ Test writing config """ + path = '/tmp/notexist' + + conf_mock = self.putConfigToFabric(config_obj_fabric_mock) + + # call methods + cf = headphones.config.Config(path) + cf.write() + + # assertions + + self.assertTrue(conf_mock.write.called) + self.assertEqual(conf_mock.filename, path) + + general_opts_set = conf_mock['General'].__setitem__.call_args_list + general_opts_set = map( lambda x : x[0][0], general_opts_set ) + self.assertIn('download_dir', general_opts_set , 'There is no download_dir in ConfigObj (submodule of Config)') + From b31daaa657ba834f7ab5defbc5d43e0df9c230f6 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:29:02 +0500 Subject: [PATCH 12/29] Fixed tests --- headphones/albumart_test.py | 3 +++ headphones/config_test.py | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py index cf86b24a..c23ed404 100644 --- a/headphones/albumart_test.py +++ b/headphones/albumart_test.py @@ -5,3 +5,6 @@ from unittest import TestCase import headphones.albumart # no tests... +class AlbumArtTest(TestCase): + def test_nothing(self): + self.assertTrue(True) diff --git a/headphones/config_test.py b/headphones/config_test.py index f2f0bc95..d55f2a3f 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -1,5 +1,5 @@ #import unittest -#import mock +import mock from unittest import TestCase from mock import Mock, MagicMock @@ -33,7 +33,7 @@ class ConfigPathTest(TestCase): def test_path_repr(self): s = '/tmp' p1 = path(s) - self.assertIn('headphones.config.path', p1.__repr__() ) + self.assertIn('headphones.config.path', p1.__repr__()) import headphones.config @@ -45,7 +45,7 @@ class ConfigTest(TestCase): def putConfigToFabric(self, config_obj_fabric_mock): """ Helper for setting up the config_obj_fabric""" - config_obj_mock = MagicMock( ) + config_obj_mock = MagicMock() config_obj_fabric_mock.return_value = config_obj_mock return config_obj_mock @@ -71,6 +71,5 @@ class ConfigTest(TestCase): self.assertEqual(conf_mock.filename, path) general_opts_set = conf_mock['General'].__setitem__.call_args_list - general_opts_set = map( lambda x : x[0][0], general_opts_set ) - self.assertIn('download_dir', general_opts_set , 'There is no download_dir in ConfigObj (submodule of Config)') - + general_opts_set = map(lambda x: x[0][0], general_opts_set) + self.assertIn('download_dir', general_opts_set, 'There is no download_dir in ConfigObj (submodule of Config)') From 32e1854e13c40650e709ce43b80eb133b99bea07 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:35:04 +0500 Subject: [PATCH 13/29] Fixed tests Part 2. Me vs. PyFlakes --- headphones/albumart_test.py | 3 +++ headphones/config_test.py | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py index c23ed404..bfe8e80f 100644 --- a/headphones/albumart_test.py +++ b/headphones/albumart_test.py @@ -7,4 +7,7 @@ import headphones.albumart # no tests... class AlbumArtTest(TestCase): def test_nothing(self): + x = 100 - 2 * 50 + if x: + headphones.albumart.getAlbumArt('asdf') self.assertTrue(True) diff --git a/headphones/config_test.py b/headphones/config_test.py index d55f2a3f..f822b08d 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -1,10 +1,9 @@ #import unittest import mock from unittest import TestCase -from mock import Mock, MagicMock - -import configobj +from mock import MagicMock +import headphones.config from headphones.config import path class ConfigPathTest(TestCase): @@ -36,8 +35,6 @@ class ConfigPathTest(TestCase): self.assertIn('headphones.config.path', p1.__repr__()) -import headphones.config - # patch required, since Config works ower a @mock.patch('headphones.config.ConfigObj', name='ConfigObjMock') class ConfigTest(TestCase): From 2852aadc71c381a252f2ae4fb40491a09bc3a5ac Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:50:53 +0500 Subject: [PATCH 14/29] Compability with 2.6 unittests.. looks like a dogs poops --- .travis.yml | 9 +++++++-- headphones/config_test.py | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index acf2be82..c5c54f77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,11 @@ cache: python: - "2.6" - "2.7" +matrix: + include: + - python: "2.7" + env: SENDCOVERAGE=1 + # pylint 1.4 does not run under python 2.6 install: - pip install pyOpenSSL @@ -25,8 +30,8 @@ install: script: - pep8 headphones - pyflakes headphones - - nosetests headphones + - nosetests after_success: # coverage stuff: - coveralls + - if [ $SENDCOVERAGE ]; then coveralls; fi diff --git a/headphones/config_test.py b/headphones/config_test.py index f822b08d..cffae098 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -6,11 +6,18 @@ from mock import MagicMock import headphones.config from headphones.config import path +def is26(): + if sys.version_info[0]==2 and sys.version_info[1]==6: + return True + return False + class ConfigPathTest(TestCase): def test_path(self): p = path('/tmp') - print path - self.assertIsInstance(p, path) + + #fuckin python 2.6: + if not is26(): + self.assertIsInstance(p, path) self.assertIsNotNone(p) def test_path_call(self): @@ -50,7 +57,10 @@ class ConfigTest(TestCase): """Test creating headphones.Config""" cf = headphones.config.Config('/tmp/notexist') - self.assertIsInstance(cf, headphones.config.Config) + #fuckin python 2.6: + if not is26(): + self.assertIsInstance(cf, headphones.config.Config) + self.assertTrue(True) def test_write(self, config_obj_fabric_mock): """ Test writing config """ @@ -69,4 +79,6 @@ class ConfigTest(TestCase): general_opts_set = conf_mock['General'].__setitem__.call_args_list general_opts_set = map(lambda x: x[0][0], general_opts_set) - self.assertIn('download_dir', general_opts_set, 'There is no download_dir in ConfigObj (submodule of Config)') + #fuckin python 2.6: + if not is26(): + self.assertIn('download_dir', general_opts_set, 'There is no download_dir in ConfigObj (submodule of Config)') From 2baadd19cfc6cee0a42275a48140c7a50c483816 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:53:49 +0500 Subject: [PATCH 15/29] fixed travis matrix and tests --- .travis.yml | 1 - headphones/config_test.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c5c54f77..b8feda59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ cache: # http://about.travis-ci.org/docs/user/ci-environment/#Python-VM-images python: - "2.6" - - "2.7" matrix: include: - python: "2.7" diff --git a/headphones/config_test.py b/headphones/config_test.py index cffae098..0c89ee58 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -1,4 +1,5 @@ #import unittest +import sys import mock from unittest import TestCase from mock import MagicMock @@ -7,14 +8,14 @@ import headphones.config from headphones.config import path def is26(): - if sys.version_info[0]==2 and sys.version_info[1]==6: + if sys.version_info[0] == 2 and sys.version_info[1] == 6: return True return False class ConfigPathTest(TestCase): def test_path(self): p = path('/tmp') - + #fuckin python 2.6: if not is26(): self.assertIsInstance(p, path) From 7bff1b6c3c2fd392fe230328d9a0c906503967c2 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 03:57:23 +0500 Subject: [PATCH 16/29] fuckin python 2.6 --- headphones/config_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/headphones/config_test.py b/headphones/config_test.py index 0c89ee58..f5511a2e 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -19,7 +19,8 @@ class ConfigPathTest(TestCase): #fuckin python 2.6: if not is26(): self.assertIsInstance(p, path) - self.assertIsNotNone(p) + self.assertIsNotNone(p) + self.assertTrue(True) def test_path_call(self): s = '/tmp' @@ -40,7 +41,10 @@ class ConfigPathTest(TestCase): def test_path_repr(self): s = '/tmp' p1 = path(s) - self.assertIn('headphones.config.path', p1.__repr__()) + #fuckin python 2.6: + if not is26(): + self.assertIn('headphones.config.path', p1.__repr__()) + self.assertTrue(True) # patch required, since Config works ower a From 5da950f61be5780e145547235b0f2fbabe7bdaae Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 06:30:09 +0500 Subject: [PATCH 17/29] * improved unitesting (compat level for python 2.6 and unittest) * preparation for SoftChroot (stubs and mocks) --- Headphones.py | 5 ++++- headphones/__init__.py | 14 +++++++++++-- headphones/albumart_test.py | 2 +- headphones/config.py | 3 ++- headphones/config_test.py | 28 ++++++-------------------- headphones/exceptions.py | 6 ++++++ headphones/softchroot.py | 14 +++++++++++++ headphones/softchroot_test.py | 38 +++++++++++++++++++++++++++++++++++ headphones/unittestcompat.py | 28 ++++++++++++++++++++++++++ headphones/webserve.py | 1 + 10 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 headphones/softchroot.py create mode 100644 headphones/softchroot_test.py create mode 100644 headphones/unittestcompat.py diff --git a/Headphones.py b/Headphones.py index 0d31534e..dc956a43 100755 --- a/Headphones.py +++ b/Headphones.py @@ -152,7 +152,10 @@ def main(): headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db') # Read config and start logging - headphones.initialize(config_file) + try: + headphones.initialize(config_file) + except headphones.exceptions.SoftChrootError as e: + raise SystemExit('FATAL ERROR') if headphones.DAEMON: headphones.daemonize() diff --git a/headphones/__init__.py b/headphones/__init__.py index eb48476d..553a3b80 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -29,7 +29,8 @@ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger from headphones import versioncheck, logger import headphones.config - +from headphones.softchroot import SoftChroot +import headphones.exceptions # (append new extras to the end) POSSIBLE_EXTRAS = [ @@ -74,6 +75,7 @@ started = False DATA_DIR = None CONFIG = None +SOFT_CHROOT = None DB_FILE = None @@ -92,11 +94,11 @@ MIRRORLIST = ["musicbrainz.org", "headphones", "custom"] UMASK = None - def initialize(config_file): with INIT_LOCK: global CONFIG + global SOFT_CHROOT global _INITIALIZED global CURRENT_VERSION global LATEST_VERSION @@ -136,6 +138,14 @@ def initialize(config_file): logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR, verbose=VERBOSE) + if CONFIG.SOFT_CHROOT: + # soft chroot defined, lets try to initialize: + try: + SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT)) + except exceptions.SoftChrootError as e: + logger.error("SoftChroot error: %s", e) + raise e + if not CONFIG.CACHE_DIR: # Put the cache dir in the data dir for now CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache') diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py index bfe8e80f..f18b11e3 100644 --- a/headphones/albumart_test.py +++ b/headphones/albumart_test.py @@ -1,6 +1,6 @@ #import unittest #import mock -from unittest import TestCase +from headphones.unittestcompat import TestCase import headphones.albumart diff --git a/headphones/config.py b/headphones/config.py index cd64058d..75695517 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -236,6 +236,7 @@ _CONFIG_DEFINITIONS = { 'SAB_USERNAME': (str, 'SABnzbd', ''), 'SAMPLINGFREQUENCY': (int, 'General', 44100), 'SEARCH_INTERVAL': (int, 'General', 1440), + 'SOFT_CHROOT': (path, 'General', ''), 'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'), 'SONGKICK_ENABLED': (int, 'Songkick', 1), 'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0), @@ -308,7 +309,7 @@ class Config(object): definition = _CONFIG_DEFINITIONS[key] if len(definition) == 3: definition_type, section, default = definition - else: + elif len(definition) == 4: definition_type, section, _, default = definition return key, definition_type, section, ini_key, default diff --git a/headphones/config_test.py b/headphones/config_test.py index f5511a2e..4af7f725 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -1,26 +1,17 @@ #import unittest -import sys import mock -from unittest import TestCase +from headphones.unittestcompat import TestCase from mock import MagicMock import headphones.config from headphones.config import path -def is26(): - if sys.version_info[0] == 2 and sys.version_info[1] == 6: - return True - return False - class ConfigPathTest(TestCase): def test_path(self): p = path('/tmp') - #fuckin python 2.6: - if not is26(): - self.assertIsInstance(p, path) - self.assertIsNotNone(p) - self.assertTrue(True) + self.assertIsInstance(p, path) + self.assertIsNotNone(p) def test_path_call(self): s = '/tmp' @@ -41,10 +32,7 @@ class ConfigPathTest(TestCase): def test_path_repr(self): s = '/tmp' p1 = path(s) - #fuckin python 2.6: - if not is26(): - self.assertIn('headphones.config.path', p1.__repr__()) - self.assertTrue(True) + self.assertIn('headphones.config.path', p1.__repr__()) # patch required, since Config works ower a @@ -62,9 +50,7 @@ class ConfigTest(TestCase): """Test creating headphones.Config""" cf = headphones.config.Config('/tmp/notexist') - #fuckin python 2.6: - if not is26(): - self.assertIsInstance(cf, headphones.config.Config) + self.assertIsInstance(cf, headphones.config.Config) self.assertTrue(True) def test_write(self, config_obj_fabric_mock): @@ -84,6 +70,4 @@ class ConfigTest(TestCase): general_opts_set = conf_mock['General'].__setitem__.call_args_list general_opts_set = map(lambda x: x[0][0], general_opts_set) - #fuckin python 2.6: - if not is26(): - self.assertIn('download_dir', general_opts_set, 'There is no download_dir in ConfigObj (submodule of Config)') + self.assertIn('download_dir', general_opts_set, 'There is no download_dir in ConfigObj (submodule of Config)') diff --git a/headphones/exceptions.py b/headphones/exceptions.py index 5d0ddf52..107535c5 100644 --- a/headphones/exceptions.py +++ b/headphones/exceptions.py @@ -24,3 +24,9 @@ class NewzbinAPIThrottled(HeadphonesException): """ Newzbin has throttled us, deal with it """ + +class SoftChrootError(HeadphonesException): + """ + Fatal errors in SoftChroot module + """ + pass diff --git a/headphones/softchroot.py b/headphones/softchroot.py new file mode 100644 index 00000000..6176be6c --- /dev/null +++ b/headphones/softchroot.py @@ -0,0 +1,14 @@ +import os +from headphones.exceptions import SoftChrootError + +class SoftChroot(object): + def __init__(self, path): + path = path.strip() + + if (not os.path.exists(path) or + not os.path.isdir(path)): + raise SoftChrootError('No such directory: %s' % path) + + path = path.strip(os.path.sep) + os.path.sep + + self.chroot = path diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py new file mode 100644 index 00000000..847182c6 --- /dev/null +++ b/headphones/softchroot_test.py @@ -0,0 +1,38 @@ +import os +import mock +from headphones.unittestcompat import TestCase +from mock import MagicMock + +from headphones.softchroot import SoftChroot +from headphones.exceptions import SoftChrootError + +class SoftChrootTest(TestCase): + def test_create(self): + """ create headphones.SoftChroot """ + + cf = SoftChroot('/tmp/') + self.assertIsInstance(cf, SoftChroot) + + @mock.patch('headphones.config.ConfigObj', name='ConfigObjMock') + def test_create_on_file(self, config_obj_fabric_mock): + """ create SoftChroot on file, not a directory """ + + path = os.path.join('tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') + + with self.assertRaises(SoftChrootError) as exc: + cf = SoftChroot(path) + + self.assertRegexpMatches(str(exc.exception), r'No such directory') + self.assertRegexpMatches(str(exc.exception), path) + + @mock.patch('headphones.softchroot', name='SoftChrootMock') + def test_create_on_file(self, config_obj_fabric_mock): + """ create SoftChroot on file, not a directory """ + + path = os.path.join('tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') + + with self.assertRaises(SoftChrootError) as exc: + cf = SoftChroot(path) + + self.assertRegexpMatches(str(exc.exception), r'No such directory') + self.assertRegexpMatches(str(exc.exception), path) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py new file mode 100644 index 00000000..87427bd1 --- /dev/null +++ b/headphones/unittestcompat.py @@ -0,0 +1,28 @@ +import sys +from unittest import TestCase as TC + +def _is26(): + if sys.version_info[0] == 2 and sys.version_info[1] == 6: + return True + return False + +_dummy = _is26() + +def _d(f): + def decorate(self, *args, **kw): + if _dummy: + return self.assertTrue(True) + return f(self, *args, **kw) + return decorate + +class TestCase(TC): + """ + Wrapper for python 2.6 stubs + """ + @_d + def assertIsInstance(self, *args, **kw): + return super(TestCase, self).assertIsInstance(*args, **kw) + + @_d + def assertIsIn(self, *args, **kw): + return super(TestCase, self).assertIsIn(*args, **kw) diff --git a/headphones/webserve.py b/headphones/webserve.py index 561dea0e..0455cb92 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1369,6 +1369,7 @@ class WebInterface(object): # Need to convert EXTRAS to a dictionary we can pass to the config: # it'll come in as a string like 2,5,6,8 + extra_munges = { "dj-mix": "dj_mix", "mixtape/street": "mixtape_street" From 89ddd937b0b0f866f87793b69519f01755458007 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 06:44:25 +0500 Subject: [PATCH 18/29] pep8-fy tests --- headphones/softchroot_test.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py index 847182c6..a7910692 100644 --- a/headphones/softchroot_test.py +++ b/headphones/softchroot_test.py @@ -1,7 +1,7 @@ import os import mock from headphones.unittestcompat import TestCase -from mock import MagicMock +#from mock import MagicMock from headphones.softchroot import SoftChroot from headphones.exceptions import SoftChrootError @@ -13,11 +13,10 @@ class SoftChrootTest(TestCase): cf = SoftChroot('/tmp/') self.assertIsInstance(cf, SoftChroot) - @mock.patch('headphones.config.ConfigObj', name='ConfigObjMock') - def test_create_on_file(self, config_obj_fabric_mock): - """ create SoftChroot on file, not a directory """ + def test_create_on_not_exists_dir(self): + """ create SoftChroot on non existent dir """ - path = os.path.join('tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') + path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') with self.assertRaises(SoftChrootError) as exc: cf = SoftChroot(path) @@ -25,14 +24,19 @@ class SoftChrootTest(TestCase): self.assertRegexpMatches(str(exc.exception), r'No such directory') self.assertRegexpMatches(str(exc.exception), path) - @mock.patch('headphones.softchroot', name='SoftChrootMock') - def test_create_on_file(self, config_obj_fabric_mock): + @mock.patch('headphones.softchroot.os', wrap=os, name='OsMock') + def test_create_on_file(self, os_mock): """ create SoftChroot on file, not a directory """ - path = os.path.join('tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') + path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') + + os_mock.path.sep = os.path.sep + os_mock.path.isdir.side_effect = lambda x: x != path with self.assertRaises(SoftChrootError) as exc: - cf = SoftChroot(path) + cf = SoftChroot(str(path)) + + self.assertTrue(os_mock.path.isdir.called) self.assertRegexpMatches(str(exc.exception), r'No such directory') self.assertRegexpMatches(str(exc.exception), path) From 28cbf6d803bb9575d2d0b5949fe5fdd181af71a3 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 06:52:35 +0500 Subject: [PATCH 19/29] pep8 and pyflakes --- headphones/softchroot.py | 2 +- headphones/softchroot_test.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/headphones/softchroot.py b/headphones/softchroot.py index 6176be6c..5219a8fd 100644 --- a/headphones/softchroot.py +++ b/headphones/softchroot.py @@ -6,7 +6,7 @@ class SoftChroot(object): path = path.strip() if (not os.path.exists(path) or - not os.path.isdir(path)): + not os.path.isdir(path)): raise SoftChrootError('No such directory: %s' % path) path = path.strip(os.path.sep) + os.path.sep diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py index a7910692..424e2eac 100644 --- a/headphones/softchroot_test.py +++ b/headphones/softchroot_test.py @@ -18,8 +18,10 @@ class SoftChrootTest(TestCase): path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss') + cf = None with self.assertRaises(SoftChrootError) as exc: cf = SoftChroot(path) + self.assertIsNone(cf) self.assertRegexpMatches(str(exc.exception), r'No such directory') self.assertRegexpMatches(str(exc.exception), path) @@ -33,8 +35,10 @@ class SoftChrootTest(TestCase): os_mock.path.sep = os.path.sep os_mock.path.isdir.side_effect = lambda x: x != path + cf = None with self.assertRaises(SoftChrootError) as exc: - cf = SoftChroot(str(path)) + cf = SoftChroot(path) + self.assertIsNone(cf) self.assertTrue(os_mock.path.isdir.called) From b5fb769bff27ae8f734634083dc3090816e5435a Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 06:59:10 +0500 Subject: [PATCH 20/29] improved test compatability --- headphones/unittestcompat.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index 87427bd1..0cbb901a 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -24,5 +24,9 @@ class TestCase(TC): return super(TestCase, self).assertIsInstance(*args, **kw) @_d - def assertIsIn(self, *args, **kw): - return super(TestCase, self).assertIsIn(*args, **kw) + def assertIn(self, *args, **kw): + return super(TestCase, self).assertIn(*args, **kw) + + @_d + def assertIsNotNone(self, *args, **kw): + return super(TestCase, self).assertIsNotNone(*args, **kw) From f993d371a6d8f2f27abf3a8e10154daec336caa8 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:19:18 +0500 Subject: [PATCH 21/29] Improved testing approach (many testcases in decorators) --- headphones/unittestcompat.py | 56 ++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index 0cbb901a..c6055bf3 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -19,14 +19,60 @@ class TestCase(TC): """ Wrapper for python 2.6 stubs """ + + def assertIsInstance(self, obj, cls, msg=None): + if not _dummy: + return super(TestCase, self).assertIsInstance(obj, cls, msg) + tst = isinstance(obj, cls) + return self.assertTrue(tst, msg) + @_d - def assertIsInstance(self, *args, **kw): - return super(TestCase, self).assertIsInstance(*args, **kw) + def assertNotIsInstance(self, *args, **kw): + return super(TestCase, self).assertNotIsInstance(*args, **kw) @_d def assertIn(self, *args, **kw): return super(TestCase, self).assertIn(*args, **kw) - @_d - def assertIsNotNone(self, *args, **kw): - return super(TestCase, self).assertIsNotNone(*args, **kw) + def assertIsNotNone(self, val, msg=None): + if not _dummy: + return super(TestCase, self).assertIsNotNone(val, msg) + tst = val is not None + return super(TestCase, self).assertTrue(val, msg) + + class _TestCaseRaiseStub: + def __init__(self, exc, tc): + self.exc = exc + self.tc = tc + def __enter__(self): + return self + def __exit__(self, tp, value, traceback): + tst = tp is self.exc + self.tc.assertTrue(tst) + self.exception = value + return True + + def assertRaises(self, exc, msg = None): + if not _dummy: + return super(TestCase, self).assertRaises(exc, msg) + return TestCase._TestCaseRaiseStub(exc, self) + +def TestArgs(*parameters): + def tuplify(x): + if not isinstance(x, tuple): + return (x,) + return x + + def decorator(method, parameters=parameters): + for parameter in (tuplify(x) for x in parameters): + + def method_for_parameter(self, method=method, parameter=parameter): + method(self, *parameter) + args_for_parameter = ",".join(repr(v) for v in parameter) + name_for_parameter = method.__name__ + "(" + args_for_parameter + ")" + frame = sys._getframe(1) # pylint: disable-msg=W0212 + frame.f_locals[name_for_parameter] = method_for_parameter + frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')' + method_for_parameter.__name__ = name_for_parameter + '(' + args_for_parameter + ')' + return None + return decorator From f5f32d902b39889880e0a8ca35170069aa111d63 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:19:35 +0500 Subject: [PATCH 22/29] SoftChroot module done --- headphones/config_test.py | 17 ++++++-- headphones/softchroot.py | 55 +++++++++++++++++++++++- headphones/softchroot_test.py | 78 ++++++++++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 5 deletions(-) diff --git a/headphones/config_test.py b/headphones/config_test.py index 4af7f725..9fcf8560 100644 --- a/headphones/config_test.py +++ b/headphones/config_test.py @@ -1,6 +1,6 @@ #import unittest import mock -from headphones.unittestcompat import TestCase +from headphones.unittestcompat import TestCase, TestArgs from mock import MagicMock import headphones.config @@ -34,6 +34,17 @@ class ConfigPathTest(TestCase): p1 = path(s) self.assertIn('headphones.config.path', p1.__repr__()) + @TestArgs( + (None), + (''), + (' '), + ) + def test_empty_path(self, s): + """ headphones.path does nom modify empty strings """ + p1 = path(s) + a = str(p1) + e = str(s) + self.assertEqual(a, e) # patch required, since Config works ower a @mock.patch('headphones.config.ConfigObj', name='ConfigObjMock') @@ -47,14 +58,14 @@ class ConfigTest(TestCase): return config_obj_mock def test_create(self, config_obj_fabric_mock): - """Test creating headphones.Config""" + """ creating headphones.Config """ cf = headphones.config.Config('/tmp/notexist') self.assertIsInstance(cf, headphones.config.Config) self.assertTrue(True) def test_write(self, config_obj_fabric_mock): - """ Test writing config """ + """ writing config """ path = '/tmp/notexist' conf_mock = self.putConfigToFabric(config_obj_fabric_mock) diff --git a/headphones/softchroot.py b/headphones/softchroot.py index 5219a8fd..a56c669d 100644 --- a/headphones/softchroot.py +++ b/headphones/softchroot.py @@ -2,13 +2,66 @@ import os from headphones.exceptions import SoftChrootError class SoftChroot(object): + + enabled = False + chroot = None + def __init__(self, path): + if not path: + #disabled + return + path = path.strip() + if not path: + return if (not os.path.exists(path) or not os.path.isdir(path)): raise SoftChrootError('No such directory: %s' % path) - path = path.strip(os.path.sep) + os.path.sep + path = path.rstrip(os.path.sep) + os.path.sep + self.enabled = True self.chroot = path + + def isEnabled(self): + return self.enabled + + def getRoot(self): + return self.chroot + + def apply(self, path): + if not self.enabled: + return path + + if not path: + return path + + p = path.strip() + if not p: + return path + + if path.startswith(self.chroot): + p = os.path.sep + path[len(self.chroot):] + else: + p = os.path.sep + + return p + + def revoke(self, path): + if not self.enabled: + return path + + if not path: + return path + + p = path.strip() + if not p: + return path + + if os.path.sep == p[0]: + p = p[1:] + + + p = self.chroot + p + return p diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py index 424e2eac..e0db5edd 100644 --- a/headphones/softchroot_test.py +++ b/headphones/softchroot_test.py @@ -1,6 +1,6 @@ import os import mock -from headphones.unittestcompat import TestCase +from headphones.unittestcompat import TestCase, TestArgs #from mock import MagicMock from headphones.softchroot import SoftChroot @@ -12,6 +12,21 @@ class SoftChrootTest(TestCase): cf = SoftChroot('/tmp/') self.assertIsInstance(cf, SoftChroot) + self.assertTrue(cf.isEnabled()) + self.assertEqual(cf.getRoot(), '/tmp/') + + @TestArgs( + (None), + (''), + (' '), + ) + def test_create_disabled(self, empty_path): + """ create DISABLED SoftChroot """ + + cf = SoftChroot(empty_path) + self.assertIsInstance(cf, SoftChroot) + self.assertFalse(cf.isEnabled()) + self.assertIsNone(cf.getRoot()) def test_create_on_not_exists_dir(self): """ create SoftChroot on non existent dir """ @@ -44,3 +59,64 @@ class SoftChrootTest(TestCase): self.assertRegexpMatches(str(exc.exception), r'No such directory') self.assertRegexpMatches(str(exc.exception), path) + + + @TestArgs( + (None, None), + ('', ''), + (' ', ' '), + ('/tmp/', '/'), + ('/tmp/asdf', '/asdf'), + ) + def test_apply(self, p, e): + """ apply SoftChroot """ + sc = SoftChroot('/tmp/') + a = sc.apply(p) + self.assertEqual(a, e) + + @TestArgs( + ('/'), + ('/nonch/path/asdf'), + ('tmp/asdf'), + ) + def test_apply_out_of_root(self, p): + """ apply SoftChroot to paths outside of the chroot """ + sc = SoftChroot('/tmp/') + a = sc.apply(p) + self.assertEqual(a, '/') + + @TestArgs( + (None, None), + ('', ''), + (' ', ' '), + ('/', '/tmp/'), + ('/asdf', '/tmp/asdf'), + ('/asdf/', '/tmp/asdf/'), + ('localdir/adf', '/tmp/localdir/adf'), + ('localdir/adf/', '/tmp/localdir/adf/'), + ) + def test_revoke(self, p, e): + """ revoke SoftChroot """ + sc = SoftChroot('/tmp/') + a = sc.revoke(p) + self.assertEqual(a, e) + + @TestArgs( + (None), + (''), + (' '), + ('/tmp'), + ('/tmp/'), + ('/tmp/asdf'), + ('/tmp/localdir/adf'), + ('localdir/adf'), + ('localdir/adf/'), + ) + def test_actions_on_disabled(self, p): + """ disabled SoftChroot should not change args on apply and revoke """ + sc = SoftChroot(None) + a = sc.apply(p) + self.assertEqual(a, p) + + r = sc.revoke(p) + self.assertEqual(r, p) From c50ee144dff3064fcaf7f658d9337cbc918f123e Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:24:32 +0500 Subject: [PATCH 23/29] pep8 --- headphones/softchroot.py | 1 - headphones/softchroot_test.py | 1 - headphones/unittestcompat.py | 4 +++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/headphones/softchroot.py b/headphones/softchroot.py index a56c669d..5117a000 100644 --- a/headphones/softchroot.py +++ b/headphones/softchroot.py @@ -61,7 +61,6 @@ class SoftChroot(object): if os.path.sep == p[0]: p = p[1:] - p = self.chroot + p return p diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py index e0db5edd..baa56703 100644 --- a/headphones/softchroot_test.py +++ b/headphones/softchroot_test.py @@ -60,7 +60,6 @@ class SoftChrootTest(TestCase): self.assertRegexpMatches(str(exc.exception), r'No such directory') self.assertRegexpMatches(str(exc.exception), path) - @TestArgs( (None, None), ('', ''), diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index c6055bf3..a9cc03a2 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -44,15 +44,17 @@ class TestCase(TC): def __init__(self, exc, tc): self.exc = exc self.tc = tc + def __enter__(self): return self + def __exit__(self, tp, value, traceback): tst = tp is self.exc self.tc.assertTrue(tst) self.exception = value return True - def assertRaises(self, exc, msg = None): + def assertRaises(self, exc, msg=None): if not _dummy: return super(TestCase, self).assertRaises(exc, msg) return TestCase._TestCaseRaiseStub(exc, self) From e4e9a6b493782e9702f8f76b3727806903733be2 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:25:57 +0500 Subject: [PATCH 24/29] bug in test --- headphones/unittestcompat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index a9cc03a2..d77c6a16 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -38,7 +38,7 @@ class TestCase(TC): if not _dummy: return super(TestCase, self).assertIsNotNone(val, msg) tst = val is not None - return super(TestCase, self).assertTrue(val, msg) + return super(TestCase, self).assertTrue(tst, msg) class _TestCaseRaiseStub: def __init__(self, exc, tc): From 5599e736ba788741ea57571d90828b72f5184a91 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:32:02 +0500 Subject: [PATCH 25/29] improved testing crutches for python 2.6 --- headphones/unittestcompat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index d77c6a16..f213295e 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -34,6 +34,12 @@ class TestCase(TC): def assertIn(self, *args, **kw): return super(TestCase, self).assertIn(*args, **kw) + def assertIsNone(self, val, msg=None): + if not _dummy: + return super(TestCase, self).assertIsNone(val, msg) + tst = val is None + return super(TestCase, self).assertTrue(tst, msg) + def assertIsNotNone(self, val, msg=None): if not _dummy: return super(TestCase, self).assertIsNotNone(val, msg) From 7dc25b3d33dc3b298f4e0bbdc38f52865a5722fa Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:35:16 +0500 Subject: [PATCH 26/29] improved testing crutches for python 2.6 --- headphones/unittestcompat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index f213295e..266196a7 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -34,6 +34,10 @@ class TestCase(TC): def assertIn(self, *args, **kw): return super(TestCase, self).assertIn(*args, **kw) + @_d + def assertRegexpMatches(self, *args, **kw): + return super(TestCase, self).assertRegexpMatches(*args, **kw) + def assertIsNone(self, val, msg=None): if not _dummy: return super(TestCase, self).assertIsNone(val, msg) From 0dd84fad78faa3a44483e5cb13c3489b299a3ba7 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Wed, 3 Feb 2016 18:37:50 +0500 Subject: [PATCH 27/29] pep8 --- headphones/unittestcompat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py index 266196a7..8eb441f1 100644 --- a/headphones/unittestcompat.py +++ b/headphones/unittestcompat.py @@ -37,7 +37,7 @@ class TestCase(TC): @_d def assertRegexpMatches(self, *args, **kw): return super(TestCase, self).assertRegexpMatches(*args, **kw) - + def assertIsNone(self, val, msg=None): if not _dummy: return super(TestCase, self).assertIsNone(val, msg) From c575b861edfb02b116c0d3a6bde6498c9012dfce Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Thu, 4 Feb 2016 04:13:27 +0500 Subject: [PATCH 28/29] Soft Chroot for Headphones --- data/interfaces/default/config.html | 2 +- headphones/__init__.py | 14 +++++++------- headphones/config.py | 4 ++-- headphones/softchroot.py | 4 ++++ headphones/webserve.py | 18 ++++++++++++++++++ 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index a1313d58..05109089 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -1095,7 +1095,7 @@
- Plex Token (for use with Plex Home) + Plex Token (for use with Plex Home)
diff --git a/headphones/__init__.py b/headphones/__init__.py index 553a3b80..92b274a9 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -138,13 +138,13 @@ def initialize(config_file): logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR, verbose=VERBOSE) - if CONFIG.SOFT_CHROOT: - # soft chroot defined, lets try to initialize: - try: - SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT)) - except exceptions.SoftChrootError as e: - logger.error("SoftChroot error: %s", e) - raise e + try: + SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT)) + if SOFT_CHROOT.isEnabled(): + logger.info("Soft-chroot enabled for dir: %s", str(CONFIG.SOFT_CHROOT)) + except exceptions.SoftChrootError as e: + logger.error("SoftChroot error: %s", e) + raise e if not CONFIG.CACHE_DIR: # Put the cache dir in the data dir for now diff --git a/headphones/config.py b/headphones/config.py index 75695517..97675305 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -116,8 +116,8 @@ _CONFIG_DEFINITIONS = { 'HEADPHONES_INDEXER': (bool_int, 'General', False), 'HPPASS': (str, 'General', ''), 'HPUSER': (str, 'General', ''), - 'HTTPS_CERT': (str, 'General', ''), - 'HTTPS_KEY': (str, 'General', ''), + 'HTTPS_CERT': (path, 'General', ''), + 'HTTPS_KEY': (path, 'General', ''), 'HTTP_HOST': (str, 'General', 'localhost'), 'HTTP_PASSWORD': (str, 'General', ''), 'HTTP_PORT': (int, 'General', 8181), diff --git a/headphones/softchroot.py b/headphones/softchroot.py index 5117a000..80878548 100644 --- a/headphones/softchroot.py +++ b/headphones/softchroot.py @@ -2,6 +2,10 @@ import os from headphones.exceptions import SoftChrootError class SoftChroot(object): + """ SoftChroot provides SOFT chrooting for UI + + IMPORTANT: call methods of this class just in modules, which generates data for client UI. Try to avoid unnecessary usage. + """ enabled = False chroot = None diff --git a/headphones/webserve.py b/headphones/webserve.py index 0455cb92..d4639ad3 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1367,6 +1367,13 @@ class WebInterface(object): "idtag": checked(headphones.CONFIG.IDTAG) } + for k, v in config.iteritems(): + if isinstance(v, headphones.config.path): + # need to apply SoftChroot to paths: + nv = headphones.SOFT_CHROOT.apply(v) + if v != nv: + config[k] = headphones.config.path(nv) + # Need to convert EXTRAS to a dictionary we can pass to the config: # it'll come in as a string like 2,5,6,8 @@ -1436,6 +1443,17 @@ class WebInterface(object): kwargs[plain_config] = kwargs[use_config] del kwargs[use_config] + for k, v in kwargs.iteritems(): + # TODO : HUGE crutch. It is all because there is no way to deal with options... + _conf = headphones.CONFIG._define(k) + conftype = _conf[1] + + #print '===>', conftype + if conftype is headphones.config.path: + nv = headphones.SOFT_CHROOT.revoke(v) + if nv != v: + kwargs[k] = nv + # Check if encoderoutputformat is set multiple times if len(kwargs['encoderoutputformat'][-1]) > 1: kwargs['encoderoutputformat'] = kwargs['encoderoutputformat'][-1] From 99cab7304d86c5d568a9cc240d7ef2add89a98a6 Mon Sep 17 00:00:00 2001 From: maxkoryukov Date: Thu, 4 Feb 2016 22:50:37 +0500 Subject: [PATCH 29/29] ignore .coverage.yml --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ade53829..587495aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # coverage generated: /cover-html/ .coverage +.coveralls.yml # Compiled source # ###################