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()

Reply via email to