From 124336ed57c8f2776d8ec83130b448fa82abdbe3 Mon Sep 17 00:00:00 2001 From: Davide Laezza Date: Mon, 22 Apr 2019 00:38:33 +0200 Subject: [PATCH] Testing merge funcationalities in import_configs --- dotdrop/action.py | 13 +- dotdrop/config.py | 23 +++- tests/helpers.py | 58 ++++++++- tests/test_config.py | 276 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 305 insertions(+), 65 deletions(-) diff --git a/dotdrop/action.py b/dotdrop/action.py index 8dac8de..6b22280 100644 --- a/dotdrop/action.py +++ b/dotdrop/action.py @@ -14,6 +14,7 @@ from dotdrop.logger import Logger class Cmd: + eq_ignore = ('log',) def __init__(self, key, action): """constructor @@ -31,7 +32,17 @@ class Cmd: return 'cmd({})'.format(self.__str__()) def __eq__(self, other): - return self.__dict__ == other.__dict__ + self_dict = { + k: v + for k, v in self.__dict__.items() + if k not in self.eq_ignore + } + other_dict = { + k: v + for k, v in other.__dict__.items() + if k not in self.eq_ignore + } + return self_dict == other_dict def __hash__(self): return hash(self.key) ^ hash(self.action) diff --git a/dotdrop/config.py b/dotdrop/config.py index ab0e204..62e583e 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -154,6 +154,9 @@ class Cfg: if not self._load_config(profile=profile): raise ValueError('config is not valid') + def __eq__(self, other): + return self.cfgpath == other.cfgpath + def eval_dotfiles(self, profile, variables, debug=False): """resolve dotfiles src/dst/actions templating for this profile""" t = Templategen(variables=variables) @@ -297,7 +300,11 @@ class Cfg: # If read transformations are None, replaces them with empty dict try: read_trans = self.content[self.key_trans_r] or {} - self.trans_r = {k: Transform(k, v) for k, v in read_trans.items()} + self.trans_r.update({ + k: Transform(k, v) + for k, v + in read_trans.items() + }) except KeyError: pass @@ -305,7 +312,11 @@ class Cfg: # If write transformations are None, replaces them with empty dict try: read_trans = self.content[self.key_trans_w] or {} - self.trans_w = {k: Transform(k, v) for k, v in read_trans.items()} + self.trans_w.update({ + k: Transform(k, v) + for k, v + in read_trans.items() + }) except KeyError: pass @@ -462,6 +473,14 @@ class Cfg: raise ValueError( 'external config file not found: {}'.format(config_path)) + list_settings = ( + (k, v) + for k, v in ext_config.lnk_settings.items() + if isinstance(v, list) + ) + for k, v in list_settings: + self.lnk_settings[k] += v + ext_members = ( (name, member) for name, member in inspect.getmembers(ext_config, is_dict) diff --git a/tests/helpers.py b/tests/helpers.py index c06854b..09f24eb 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,10 +5,12 @@ helpers for the unittests """ import os +import random import shutil import string -import random import tempfile +from unittest import TestCase + import yaml from dotdrop.options import Options, ENV_NODEBUG @@ -18,6 +20,30 @@ from dotdrop.utils import strip_home TMPSUFFIX = '-dotdrop-tests' +class SubsetTestCase(TestCase): + def assertIsSubset(self, sub, sup): + for subKey, subValue in sub.items(): + self.assertIn(subKey, sup) + supValue = sup[subKey] + + if isinstance(subValue, str): + self.assertEquals(subValue, supValue) + continue + + if isinstance(subValue, dict): + self.assertIsSubset(subValue, supValue) + continue + + try: + iter(subValue) + self.assertTrue(all( + subItem in supValue + for subItem in subValue + )) + except TypeError: + self.assertEquals(subValue, supValue) + + def clean(path): """Delete file or directory""" if not os.path.exists(path): @@ -157,7 +183,8 @@ def yaml_dashed_list(items, indent=0): def create_fake_config(directory, configname='config.yaml', dotpath='dotfiles', backup=True, create=True, - import_configs=()): + import_configs=(), import_actions=(), + import_variables=()): """Create a fake config file""" path = os.path.join(directory, configname) workdir = os.path.join(directory, 'workdir') @@ -167,16 +194,36 @@ def create_fake_config(directory, configname='config.yaml', f.write(' create: {}\n'.format(str(create))) f.write(' dotpath: {}\n'.format(dotpath)) f.write(' workdir: {}\n'.format(workdir)) + if import_actions: + f.write(' import_actions:\n') + f.write(yaml_dashed_list(import_actions, 4)) if import_configs: f.write(' import_configs:\n') f.write(yaml_dashed_list(import_configs, 4)) + if import_variables: + f.write(' import_variables:\n') + f.write(yaml_dashed_list(import_variables, 4)) f.write('dotfiles:\n') f.write('profiles:\n') f.write('actions:\n') return path -def populate_fake_config(config, dotfiles=(), profiles=()): +def create_yaml_keyval(pairs, parent_dir=None, top_key=None): + if top_key: + pairs = {top_key: pairs} + if not parent_dir: + parent_dir = get_tempdir() + + fd, file_name = tempfile.mkstemp(dir=parent_dir, suffix='.yaml', text=True) + with os.fdopen(fd, 'w') as f: + yaml.safe_dump(pairs, f) + return file_name + + +def populate_fake_config(config, dotfiles=(), profiles=(), actions=(), + trans=(), trans_write=(), variables=(), + dynvariables=()): """Adds some juicy content to config files""" is_path = isinstance(config, str) if is_path: @@ -186,6 +233,11 @@ def populate_fake_config(config, dotfiles=(), profiles=()): config['dotfiles'] = dotfiles config['profiles'] = profiles + config['actions'] = actions + config['trans'] = trans + config['trans_write'] = trans_write + config['variables'] = variables + config['dynvariables'] = dynvariables if is_path: with open(config_path, 'w') as config_file: diff --git a/tests/test_config.py b/tests/test_config.py index b331ab0..b20f990 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,11 +13,12 @@ import yaml from dotdrop.config import Cfg from dotdrop.options import Options from dotdrop.linktypes import LinkTypes -from tests.helpers import get_tempdir, clean, \ - create_fake_config, _fake_args, populate_fake_config +from tests.helpers import (SubsetTestCase, _fake_args, clean, + create_fake_config, create_yaml_keyval, get_tempdir, + populate_fake_config) -class TestConfig(unittest.TestCase): +class TestConfig(SubsetTestCase): CONFIG_BACKUP = False CONFIG_CREATE = True @@ -200,78 +201,235 @@ profiles: conf = Cfg(confpath) self.assertTrue(conf is not None) - def test_include_profiles(self): + def test_import_configs_merge(self): + """Test import_configs when all config keys merge.""" tmp = get_tempdir() self.assertTrue(os.path.exists(tmp)) self.addCleanup(clean, tmp) - # create the imported base config file - imported = create_fake_config(tmp, - configname=self.CONFIG_NAME_2, - dotpath=self.CONFIG_DOTPATH, - backup=self.CONFIG_BACKUP, - create=self.CONFIG_CREATE) - # create the importing base config file - importing = create_fake_config(tmp, - configname=self.CONFIG_NAME, - dotpath=self.CONFIG_DOTPATH, - backup=self.CONFIG_BACKUP, - create=self.CONFIG_CREATE, - import_configs=(imported,)) + vars_ed = { + 'variables': { + 'a_var_ed': '33', + }, + 'dynvariables': { + 'a_dynvar_ed': 'echo 33', + }, + } + vars_ing = { + 'variables': { + 'a_var_ing': 'dd', + }, + 'dynvariables': { + 'a_dynvar_ing': 'echo dd', + }, + } + vars_ed_file = create_yaml_keyval(vars_ed, tmp) + vars_ing_file = create_yaml_keyval(vars_ing, tmp) - # keys - keys = { - 'dotfile1': 'f_vimrc', - 'dotfile2': 'f_xinitrc', - 'profile1': 'host1', - 'profile2': 'host2', - } + imported = { + 'config': { + 'dotpath': 'importing', + 'import_variables': [vars_ed_file], + }, + 'dotfiles': { + 'f_vimrc': {'dst': '~/.vimrc', 'src': 'vimrc'}, + }, + 'profiles': { + 'host1': { + 'dotfiles': ['f_vimrc'], + }, + }, + 'actions': { + 'pre': { + 'a_pre_log_ed': 'echo pre 2', + }, + 'post': { + 'a_post_log_ed': 'echo post 2', + }, + 'a_log_ed': 'echo 2', + }, + 'trans': { + 't_log_ed': 'echo 3', + }, + 'trans_write': { + 'tw_log_ed': 'echo 4', + }, + 'variables': { + 'v_log_ed': '42', + }, + 'dynvariables': { + 'dv_log_ed': 'echo 5', + }, + } + importing = { + 'config': { + 'dotpath': 'importing', + 'import_variables': [vars_ing_file], + }, + 'dotfiles': { + 'f_xinitrc': {'dst': '~/.xinitrc', 'src': 'xinitrc'}, + }, + 'profiles': { + 'host2': { + 'dotfiles': ['f_xinitrc'], + 'include': ['host1'], + }, + }, + 'actions': { + 'pre': { + 'a_pre_log_ing': 'echo pre a', + }, + 'post': { + 'a_post_log_ing': 'echo post a', + }, + 'a_log_ing': 'echo a', + }, + 'trans': { + 't_log_ing': 'echo b', + }, + 'trans_write': { + 'tw_log_ing': 'echo c', + }, + 'variables': { + 'v_log_ing': 'd', + }, + 'dynvariables': { + 'dv_log_ing': 'echo e', + }, + } + + # create the imported base config file + imported_path = create_fake_config(tmp, + configname=self.CONFIG_NAME_2, + **imported['config']) + # create the importing base config file + importing_path = create_fake_config(tmp, + configname=self.CONFIG_NAME, + import_configs=(imported_path,), + **importing['config']) # edit the imported config - dotfiles_imported = { - keys['dotfile1']: {'dst': '~/.vimrc', 'src': 'vimrc'}, - } - profiles_imported = { - keys['profile1']: {'dotfiles': [keys['dotfile1']]}, - } - populate_fake_config(imported, - dotfiles=dotfiles_imported, - profiles=profiles_imported) + populate_fake_config(imported_path, **{ + k: v + for k, v in imported.items() + if k != 'config' + }) # edit the importing config - dotfiles_importing = { - keys['dotfile2']: {'dst': '~/.vimrc', 'src': 'vimrc'}, - } - profiles_importing = { - keys['profile2']: { - 'dotfiles': [keys['dotfile2']], - 'include': [keys['profile1']], - } - } - populate_fake_config(importing, - dotfiles=dotfiles_importing, - profiles=profiles_importing) + populate_fake_config(importing_path, **{ + k: v + for k, v in importing.items() + if k != 'config' + }) # do the tests - importing_cfg = Cfg(importing) + importing_cfg = Cfg(importing_path) + imported_cfg = Cfg(imported_path) self.assertIsNotNone(importing_cfg) + self.assertIsNotNone(imported_cfg) - # test profile - profiles = importing_cfg.get_profiles() - self.assertIn(keys['profile2'], profiles) + # test settings + self.assertIsSubset(imported_cfg.lnk_settings, + importing_cfg.lnk_settings) + + # test profiles + self.assertIsSubset(imported_cfg.lnk_profiles, + importing_cfg.lnk_profiles) # test dotfiles - importing_cfg_dotfiles = [ - (dotfile.key, {'src': dotfile.src, 'dst': dotfile.dst}) - for dotfile in importing_cfg.prodots[keys['profile2']] - ] + self.assertIsSubset(imported_cfg.dotfiles, + importing_cfg.dotfiles) - self.assertIn( - (keys['dotfile2'], dotfiles_importing[keys['dotfile2']]), - importing_cfg_dotfiles) - self.assertIn( - (keys['dotfile1'], dotfiles_imported[keys['dotfile1']]), - importing_cfg_dotfiles) + # test actions + self.assertIsSubset(imported_cfg.actions['pre'], + importing_cfg.actions['pre']) + self.assertIsSubset(imported_cfg.actions['post'], + importing_cfg.actions['post']) + + # test transactions + self.assertIsSubset(imported_cfg.trans_r, + importing_cfg.trans_r) + self.assertIsSubset(imported_cfg.trans_w, + importing_cfg.trans_w) + + # test transactions + self.assertIsSubset(imported_cfg.trans_r, + importing_cfg.trans_r) + self.assertIsSubset(imported_cfg.trans_w, + importing_cfg.trans_w) + + # test prodots + self.assertIsSubset(imported_cfg.prodots, + importing_cfg.prodots) + + # test ext_variables + self.assertIsSubset(imported_cfg.ext_variables, + importing_cfg.ext_variables) + + # test ext_dynvariables + self.assertIsSubset(imported_cfg.ext_dynvariables, + importing_cfg.ext_dynvariables) + + def test_import_configs_override(self): + imported = { + 'config': { + 'dotpath': 'imported', + 'backup': False, + }, + 'dotfiles': { + 'f_vimrc': {'dst': '~/.vimrc', 'src': 'vimrc'}, + }, + 'profiles': { + 'host1': { + 'dotfiles': ['f_vimrc'], + }, + }, + 'actions': { + 'a_log': 'echo 2', + }, + 'trans': { + 't_log': 'echo 3', + }, + 'trans_write': { + 'tw_log': 'echo 4', + }, + 'variables': { + 'v_log': '42', + }, + 'dynvariables': { + 'dv_log': 'echo 5', + }, + } + importing = { + 'config': { + 'dotpath': 'importing', + 'backup': True, + }, + 'dotfiles': { + 'f_xinitrc': {'dst': '~/.xinitrc', 'src': 'xinitrc'}, + }, + 'profiles': { + 'host2': { + 'dotfiles': ['f_xinitrc'], + 'include': ['host1'], + }, + }, + 'actions': { + 'a_log': 'echo a', + }, + 'trans': { + 't_log': 'echo b', + }, + 'trans_write': { + 'tw_log': 'echo c', + }, + 'variables': { + 'v_log': 'd', + }, + 'dynvariables': { + 'dv_log': 'echo e', + }, + } def main():