Source: khard Version: 0.15.0-2 Severity: wishlist Tags: patch User: reproducible-bui...@lists.alioth.debian.org Usertags: buildpath X-Debbugs-Cc: reproducible-b...@lists.alioth.debian.org
Hi, Whilst working on the Reproducible Builds effort [0] we noticed that khard could not be built reproducibly. This is because it includes the absolute build directory in the documentation via the SPEC_FILE attribute. Patch attached that calculates this dynamically instead. [0] https://reproducible-builds.org/ Regards, -- ,''`. : :' : Chris Lamb `. `'` la...@debian.org / chris-lamb.co.uk `-
--- a/debian/patches/0003-add-khard-data-dir-to-MANIFEST.in.patch 2019-10-25 09:18:54.208690284 +0100 --- b/debian/patches/0003-add-khard-data-dir-to-MANIFEST.in.patch 2019-10-25 09:46:27.257501095 +0100 @@ -6,10 +6,8 @@ MANIFEST.in | 1 + 1 file changed, 1 insertion(+) -diff --git a/MANIFEST.in b/MANIFEST.in -index 9b1fe7d..20fab9d 100644 ---- a/MANIFEST.in -+++ b/MANIFEST.in +--- khard-0.15.0.orig/MANIFEST.in ++++ khard-0.15.0/MANIFEST.in @@ -4,3 +4,4 @@ include LICENSE include README.md recursive-include misc * --- a/debian/patches/0004-reproducible-build.patch 1970-01-01 01:00:00.000000000 +0100 --- b/debian/patches/0004-reproducible-build.patch 2019-10-25 09:49:09.070068812 +0100 @@ -0,0 +1,42 @@ +Description: Make the build reproducible +Author: Chris Lamb <la...@debian.org> +Last-Update: 2019-10-25 + +--- khard-0.15.0.orig/khard/config.py ++++ khard-0.15.0/khard/config.py +@@ -92,7 +92,7 @@ def validate_private_objects(value): + class Config: + + supported_vcard_versions = ("3.0", "4.0") +- SPEC_FILE = os.path.join(os.path.dirname(__file__), 'data', 'config.spec') ++ SPEC_FILE = None + + def __init__(self, config_file=None): + self.config = None +@@ -116,8 +116,12 @@ class Config: + config_file = os.getenv("KHARD_CONFIG", os.path.join( + xdg_config_home, "khard", "khard.conf")) + try: ++ configspec = cls.SPEC_FILE ++ if configspec is None: ++ configspec = os.path.join(os.path.dirname(__file__), ++ 'data', 'config.spec') + return configobj.ConfigObj( +- infile=config_file, configspec=cls.SPEC_FILE, ++ infile=config_file, configspec=configspec, + interpolation=False, file_error=True) + except configobj.ConfigObjError as err: + exit(str(err)) +--- khard-0.15.0.orig/test/test_config.py ++++ khard-0.15.0/test/test_config.py +@@ -148,7 +148,9 @@ class Validation(unittest.TestCase): + + @staticmethod + def _template(section, key, value): +- c = configobj.ConfigObj(configspec=config.Config.SPEC_FILE) ++ configspec = os.path.join(os.path.dirname(os.path.dirname(__file__)), ++ 'khard', 'data', 'config.spec') ++ c = configobj.ConfigObj(configspec=configspec) + c['general'] = {} + c['vcard'] = {} + c['contact table'] = {} --- a/debian/patches/0005-reproducible-build.patch 1970-01-01 01:00:00.000000000 +0100 --- b/debian/patches/0005-reproducible-build.patch 2019-10-25 09:46:49.261410668 +0100 @@ -0,0 +1,17 @@ +Description: Make the build reproducible +Author: Chris Lamb <la...@debian.org> +Last-Update: 2019-10-25 + +--- khard-0.15.0.orig/test/test_config.py ++++ khard-0.15.0/test/test_config.py +@@ -148,7 +148,9 @@ class Validation(unittest.TestCase): + + @staticmethod + def _template(section, key, value): +- c = configobj.ConfigObj(configspec=config.Config.SPEC_FILE) ++ configspec = os.path.join(os.path.dirname(__file__), ++ 'data', 'config.spec') ++ c = configobj.ConfigObj(configspec=configspec) + c['general'] = {} + c['vcard'] = {} + c['contact table'] = {} --- a/debian/patches/series 2019-10-25 09:18:54.208690284 +0100 --- b/debian/patches/series 2019-10-25 09:47:07.465377164 +0100 @@ -1,3 +1,4 @@ 0001-remove-travis-repology-buttons.patch 0002-use-debian-paths-in-doc.patch 0003-add-khard-data-dir-to-MANIFEST.in.patch +0004-reproducible-build.patch --- a/debian/rules 2019-10-25 09:18:54.208690284 +0100 --- b/debian/rules 2019-10-25 09:33:48.499676643 +0100 @@ -8,5 +8,9 @@ make man dh_auto_build +override_dh_auto_clean: + dh_auto_clean + rm -f khard/version.py + %: dh $@ --with python3 --buildsystem=pybuild --- a/debian/source/options 1970-01-01 01:00:00.000000000 +0100 --- b/debian/source/options 2019-10-25 09:25:24.627743707 +0100 @@ -0,0 +1 @@ +tar-ignore = "*/version.py" --- a/khard/config.py 2019-10-25 09:18:54.208690284 +0100 --- b/khard/config.py 2019-10-25 09:46:41.000000000 +0100 @@ -92,7 +92,7 @@ class Config: supported_vcard_versions = ("3.0", "4.0") - SPEC_FILE = os.path.join(os.path.dirname(__file__), 'data', 'config.spec') + SPEC_FILE = None def __init__(self, config_file=None): self.config = None @@ -116,8 +116,12 @@ config_file = os.getenv("KHARD_CONFIG", os.path.join( xdg_config_home, "khard", "khard.conf")) try: + configspec = cls.SPEC_FILE + if configspec is None: + configspec = os.path.join(os.path.dirname(__file__), + 'data', 'config.spec') return configobj.ConfigObj( - infile=config_file, configspec=cls.SPEC_FILE, + infile=config_file, configspec=configspec, interpolation=False, file_error=True) except configobj.ConfigObjError as err: exit(str(err)) --- a/test/test_config.py 2019-10-25 09:18:54.212690314 +0100 --- b/test/test_config.py 2019-10-25 09:50:16.610281543 +0100 @@ -148,7 +148,9 @@ @staticmethod def _template(section, key, value): - c = configobj.ConfigObj(configspec=config.Config.SPEC_FILE) + configspec = os.path.join(os.path.dirname(os.path.dirname(__file__)), + 'khard', 'data', 'config.spec') + c = configobj.ConfigObj(configspec=configspec) c['general'] = {} c['vcard'] = {} c['contact table'] = {} --- a/test/test_config.py.orig 1970-01-01 01:00:00.000000000 +0100 --- b/test/test_config.py.orig 2019-10-25 09:46:49.000000000 +0100 @@ -0,0 +1,189 @@ +"""Tests for the config module.""" +# pylint: disable=missing-docstring + +import io +import logging +import os.path +import tempfile +import unittest +import unittest.mock as mock + +from khard import config + +import configobj + + +class LoadingConfigFile(unittest.TestCase): + + def test_load_non_existing_file_fails(self): + filename = "I hope this file never exists" + stdout = io.StringIO() + with self.assertRaises(IOError) as cm: + config.Config._load_config_file(filename) + self.assertTrue(str(cm.exception).startswith('Config file not found:')) + + def test_uses_khard_config_environment_variable(self): + filename = "this is some very random string" + with mock.patch.dict("os.environ", clear=True, KHARD_CONFIG=filename): + with mock.patch("configobj.ConfigObj", dict): + ret = config.Config._load_config_file("") + self.assertEqual(ret['infile'], filename) + + def test_uses_xdg_config_home_environment_variable(self): + prefix = "this is some very random string" + with mock.patch.dict("os.environ", clear=True, XDG_CONFIG_HOME=prefix): + with mock.patch("configobj.ConfigObj", dict): + ret = config.Config._load_config_file("") + expected = os.path.join(prefix, 'khard', 'khard.conf') + self.assertEqual(ret['infile'], expected) + + def test_uses_config_dir_if_environment_unset(self): + prefix = "this is some very random string" + with mock.patch.dict("os.environ", clear=True, HOME=prefix): + with mock.patch("configobj.ConfigObj", dict): + ret = config.Config._load_config_file("") + expected = os.path.join(prefix, '.config', 'khard', 'khard.conf') + self.assertEqual(ret['infile'], expected) + + def test_load_empty_file_fails(self): + stdout = io.StringIO() + with tempfile.NamedTemporaryFile() as name: + with self.assertLogs(level=logging.ERROR) as cm: + with self.assertRaises(SystemExit): + config.Config(name) + + @mock.patch.dict('os.environ', EDITOR='editor', MERGE_EDITOR='meditor') + def test_load_minimal_file_by_name(self): + cfg = config.Config("test/fixture/minimal.conf") + self.assertEqual(cfg.editor, "editor") + self.assertEqual(cfg.merge_editor, "meditor") + + +class ConfigPreferredVcardVersion(unittest.TestCase): + + def test_default_value_is_3(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.preferred_vcard_version, "3.0") + + def test_set_preferred_version(self): + c = config.Config("test/fixture/minimal.conf") + c.preferred_vcard_version = "11" + self.assertEqual(c.preferred_vcard_version, "11") + + +class Defaults(unittest.TestCase): + + def test_debug_defaults_to_false(self): + c = config.Config("test/fixture/minimal.conf") + self.assertFalse(c.debug) + + def test_default_action_defaults_to_list(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.default_action, 'list') + + def test_reverse_defaults_to_false(self): + c = config.Config("test/fixture/minimal.conf") + self.assertFalse(c.reverse) + + def test_group_by_addressbook_defaults_to_false(self): + c = config.Config("test/fixture/minimal.conf") + self.assertFalse(c.group_by_addressbook) + + def test_show_nicknames_defaults_to_false(self): + c = config.Config("test/fixture/minimal.conf") + self.assertFalse(c.show_nicknames) + + def test_show_uids_defaults_to_true(self): + c = config.Config("test/fixture/minimal.conf") + self.assertTrue(c.show_uids) + + def test_sort_defaults_to_first_name(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.sort, 'first_name') + + def test_display_defaults_to_first_name(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.display, 'first_name') + + def test_localize_dates_defaults_to_true(self): + c = config.Config("test/fixture/minimal.conf") + self.assertTrue(c.localize_dates) + + def test_preferred_phone_number_type_defaults_to_pref(self): + c = config.Config("test/fixture/minimal.conf") + self.assertListEqual(c.preferred_phone_number_type, ['pref']) + + def test_preferred_email_address_type_defaults_to_pref(self): + c = config.Config("test/fixture/minimal.conf") + self.assertListEqual(c.preferred_email_address_type, ['pref']) + + def test_private_objects_defaults_to_empty(self): + c = config.Config("test/fixture/minimal.conf") + self.assertListEqual(c.private_objects, []) + + def test_search_in_source_files_defaults_to_false(self): + c = config.Config("test/fixture/minimal.conf") + self.assertFalse(c.search_in_source_files) + + def test_skip_unparsable_defaults_to_false(self): + c = config.Config("test/fixture/minimal.conf") + self.assertFalse(c.skip_unparsable) + + def test_preferred_version_defaults_to_3(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.preferred_vcard_version, '3.0') + + @mock.patch.dict('os.environ', clear=True) + def test_editor_defaults_to_vim(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.editor, 'vim') + + @mock.patch.dict('os.environ', clear=True) + def test_merge_editor_defaults_to_vimdiff(self): + c = config.Config("test/fixture/minimal.conf") + self.assertEqual(c.merge_editor, 'vimdiff') + + +class Validation(unittest.TestCase): + + @staticmethod + def _template(section, key, value): + c = configobj.ConfigObj(configspec=config.Config.SPEC_FILE) + c['general'] = {} + c['vcard'] = {} + c['contact table'] = {} + c['addressbooks'] = {'test': {'path': '/tmp'}} + c[section][key] = value + return c + + def test_rejects_invalid_default_actions(self): + action = 'this is not a valid action' + conf = self._template('general', 'default_action', action) + with self.assertLogs(level=logging.ERROR): + with self.assertRaises(SystemExit): + config.Config._validate(conf) + + def test_rejects_unparsable_editor_commands(self): + editor = 'editor --option "unparsable because quotes are missing' + conf = self._template('general', 'editor', editor) + with self.assertLogs(level=logging.ERROR): + with self.assertRaises(SystemExit): + config.Config._validate(conf) + + def test_rejects_private_objects_with_strange_chars(self): + obj = 'X-VCÃRD-EXTENSIÃN' + conf = self._template('vcard', 'private_objects', obj) + with self.assertLogs(level=logging.ERROR): + with self.assertRaises(SystemExit): + config.Config._validate(conf) + + def test_rejects_private_objects_starting_with_minus(self): + obj = '-INVALID-' + conf = self._template('vcard', 'private_objects', obj) + with self.assertLogs(level=logging.ERROR): + with self.assertRaises(SystemExit): + config.Config._validate(conf) + + +if __name__ == "__main__": + unittest.main()