[scm] Mock global git config scope globally.

This will be useful for writing tests which rely on shared 'global'
config between different working directories.

This also adds support for mocking 'system' (global, immutable) and
'workspace' (local, mutable). The workspace scope I think is a bit iffy
though, given how `git cl` is actually built - currently scm.GIT doesn't
really know about clone vs. workspace, and afaik no config adjustements
actually apply to the workspace scope.

Adds tests for mocked git config implementation, including bug fixes
to the current implementation revealed by the tests.

R=ayatane, yiwzhang

Change-Id: Ia56d2a81d8df6ae75d9f8d0497be0d67bdc03651
Bug: 355505750
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/5759163
Reviewed-by: Yiwei Zhang <yiwzhang@google.com>
Commit-Queue: Robbie Iannucci <iannucci@chromium.org>
changes/63/5759163/9
Robert Iannucci 1 year ago committed by LUCI CQ
parent cdf5599e67
commit a3fb9bad66

281
scm.py

@ -6,15 +6,15 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
import contextlib
import os import os
import pathlib import pathlib
import platform import platform
import re import re
import threading import threading
import typing
from collections import defaultdict from collections import defaultdict
from typing import Collection, Iterable, Literal, Dict from typing import Collection, Iterable, Iterator, Literal, Dict
from typing import Optional, Sequence, Mapping from typing import Optional, Sequence, Mapping
import gclient_utils import gclient_utils
@ -48,8 +48,8 @@ def determine_scm(root):
return 'diff' return 'diff'
GitConfigScope = Literal['system', 'local', 'worktree'] GitConfigScope = Literal['system', 'global', 'local', 'worktree']
GitScopeOrder: list[GitConfigScope] = ['system', 'local', 'worktree'] GitScopeOrder: list[GitConfigScope] = ['system', 'global', 'local', 'worktree']
GitFlatConfigData = Mapping[str, Sequence[str]] GitFlatConfigData = Mapping[str, Sequence[str]]
@ -85,7 +85,7 @@ class GitConfigStateBase(metaclass=abc.ABCMeta):
""" """
@abc.abstractmethod @abc.abstractmethod
def set_config_multi(self, key: str, value: str, *, append: bool, def set_config_multi(self, key: str, value: str, *,
value_pattern: Optional[str], scope: GitConfigScope): value_pattern: Optional[str], scope: GitConfigScope):
"""When invoked, this should replace all existing values of `key` with """When invoked, this should replace all existing values of `key` with
`value` in the git scope `scope` in this state's underlying data. `value` in the git scope `scope` in this state's underlying data.
@ -103,7 +103,10 @@ class GitConfigStateBase(metaclass=abc.ABCMeta):
"""When invoked, remove a singlar value from `key` in this state's underlying data. """When invoked, remove a singlar value from `key` in this state's underlying data.
If missing_ok is False and `key` is not present in the given scope, this If missing_ok is False and `key` is not present in the given scope, this
must raise GitConfigUnsetMissingValue with `key`. must raise GitConfigUnsetMissingValue with `key` and `scope`.
If `key` is multi-valued in this scope, this must raise
GitConfigUnsetMultipleValues with `key` and `scope`.
""" """
@abc.abstractmethod @abc.abstractmethod
@ -115,7 +118,7 @@ class GitConfigStateBase(metaclass=abc.ABCMeta):
be removed. be removed.
If missing_ok is False and `key` is not present in the given scope, this If missing_ok is False and `key` is not present in the given scope, this
must raise GitConfigUnsetMissingValue with `key`. must raise GitConfigUnsetMissingValue with `key` and `scope`.
TODO: Make value_pattern an re.Pattern. This wasn't done at the time of TODO: Make value_pattern an re.Pattern. This wasn't done at the time of
this refactor to keep the refactor small. this refactor to keep the refactor small.
@ -130,6 +133,26 @@ class GitConfigUnsetMissingValue(ValueError):
) )
class GitConfigUnsetMultipleValues(ValueError):
def __init__(self, key: str, scope: str) -> None:
super().__init__(
f'Cannot unset multi-value key {key!r} in scope {scope!r} with modify_all=False.'
)
class GitConfigUneditableScope(ValueError):
def __init__(self, scope: str) -> None:
super().__init__(f'Cannot edit git config in scope {scope!r}.')
class GitConfigUnknownScope(ValueError):
def __init__(self, scope: str) -> None:
super().__init__(f'Unknown git config scope {scope!r}.')
class CachedGitConfigState(object): class CachedGitConfigState(object):
"""This represents the observable git configuration state for a given """This represents the observable git configuration state for a given
repository (whose top-level path is `root`). repository (whose top-level path is `root`).
@ -223,7 +246,8 @@ class CachedGitConfigState(object):
key: The specific config key to affect. key: The specific config key to affect.
value: The value to set. If this is None, `key` will be unset. value: The value to set. If this is None, `key` will be unset.
append: If True and `value` is not None, this will append append: If True and `value` is not None, this will append
the value instead of replacing an existing one. the value instead of replacing an existing one. Must not be
specified with value_pattern.
missing_ok: If `value` is None (i.e. this is an unset operation), missing_ok: If `value` is None (i.e. this is an unset operation),
ignore retcode=5 from `git config` (meaning that the value is ignore retcode=5 from `git config` (meaning that the value is
not present). If `value` is not None, then this option has no not present). If `value` is not None, then this option has no
@ -231,15 +255,20 @@ class CachedGitConfigState(object):
GitConfigUnsetMissingValue. GitConfigUnsetMissingValue.
modify_all: If True, this will change a set operation to modify_all: If True, this will change a set operation to
`--replace-all`, and will change an unset operation to `--replace-all`, and will change an unset operation to
`--unset-all`. `--unset-all`. Must not be specified with value_pattern.
scope: By default this is the local scope, but could be `system`, scope: By default this is the `local` scope, but could be `global`
`global`, or `worktree`, depending on which config scope you or `worktree`, depending on which config scope you want to affect.
want to affect. Note that the `system` scope cannot be modified.
value_pattern: For use with `modify_all=True`, allows value_pattern: For use with `modify_all=True`, allows
further filtering of the set or unset operation based on further filtering of the set or unset operation based on
the currently configured value. Ignored for the currently configured value. Ignored for
`modify_all=False`. `modify_all=False`.
""" """
if scope not in GitScopeOrder:
raise GitConfigUnknownScope(scope)
if scope == 'system':
raise GitConfigUneditableScope(scope)
if value is None: if value is None:
if modify_all: if modify_all:
self._impl.unset_config_multi(key, self._impl.unset_config_multi(key,
@ -249,13 +278,27 @@ class CachedGitConfigState(object):
else: else:
self._impl.unset_config(key, scope=scope, missing_ok=missing_ok) self._impl.unset_config(key, scope=scope, missing_ok=missing_ok)
else: else:
if modify_all: if value_pattern:
if not modify_all:
raise ValueError(
'SetConfig with (value_pattern) and (not modify_all) is invalid.'
)
if append:
raise ValueError(
'SetConfig with (value_pattern) and (append) is invalid.'
)
self._impl.set_config_multi(key, self._impl.set_config_multi(key,
value, value,
append=append,
value_pattern=value_pattern, value_pattern=value_pattern,
scope=scope) scope=scope)
else: else:
if modify_all:
self._impl.set_config_multi(key,
value,
value_pattern=None,
scope=scope)
self._impl.set_config(key, value, append=append, scope=scope) self._impl.set_config(key, value, append=append, scope=scope)
# Once the underlying storage has set the value, we clear our cache so # Once the underlying storage has set the value, we clear our cache so
@ -297,13 +340,11 @@ class GitConfigStateReal(GitConfigStateBase):
args.append('--add') args.append('--add')
GIT.Capture(args, cwd=self.root) GIT.Capture(args, cwd=self.root)
def set_config_multi(self, key: str, value: str, *, append: bool, def set_config_multi(self, key: str, value: str, *,
value_pattern: Optional[str], scope: GitConfigScope): value_pattern: Optional[str], scope: GitConfigScope):
args = ['config', f'--{scope}', '--replace-all', key, value] args = ['config', f'--{scope}', '--replace-all', key, value]
if value_pattern is not None: if value_pattern is not None:
args.append(value_pattern) args.append(value_pattern)
if append:
args.append('--add')
GIT.Capture(args, cwd=self.root) GIT.Capture(args, cwd=self.root)
def unset_config(self, key: str, *, scope: GitConfigScope, def unset_config(self, key: str, *, scope: GitConfigScope,
@ -315,6 +356,8 @@ class GitConfigStateReal(GitConfigStateBase):
accepted_retcodes=accepted_retcodes) accepted_retcodes=accepted_retcodes)
except subprocess2.CalledProcessError as cpe: except subprocess2.CalledProcessError as cpe:
if cpe.returncode == 5: if cpe.returncode == 5:
if b'multiple values' in cpe.stderr:
raise GitConfigUnsetMultipleValues(key, scope)
raise GitConfigUnsetMissingValue(key, scope) raise GitConfigUnsetMissingValue(key, scope)
raise raise
@ -335,98 +378,151 @@ class GitConfigStateReal(GitConfigStateBase):
class GitConfigStateTest(GitConfigStateBase): class GitConfigStateTest(GitConfigStateBase):
"""A fake implementation of GitConfigStateBase for testing.""" """A fake implementation of GitConfigStateBase for testing.
To properly initialize this, see tests/scm_mock.py.
"""
def __init__(self, def __init__(self,
initial_state: Optional[Dict[GitConfigScope, global_state_lock: threading.Lock,
GitFlatConfigData]] = None): global_state: dict[str, list[str]],
self.state: Dict[GitConfigScope, Dict[str, list[str]]] = {} *,
if initial_state is not None: system_state: Optional[GitFlatConfigData] = None,
# We want to copy initial_state to make it mutable inside our class. local_state: Optional[GitFlatConfigData] = None,
for scope, data in initial_state.items(): worktree_state: Optional[GitFlatConfigData] = None):
self.state[scope] = {k: list(v) for k, v in data.items()} """Initializes a new (local, worktree) config state, with a reference to
a single global `global` state and an optional immutable `system` state.
The caller must supply a single shared Lock, plus a mutable reference to
the global-state dictionary.
Optionally, the caller may supply an initial local/worktree
configuration state.
This implementation will hold global_state_lock during all read/write
operations on the 'global' scope.
"""
self.system_state: GitFlatConfigData = system_state or {}
self.global_state_lock = global_state_lock
self.global_state = global_state
self.worktree_state: dict[str, list[str]] = {}
if worktree_state is not None:
self.worktree_state = {
k: list(v)
for k, v in worktree_state.items()
}
self.local_state: dict[str, list[str]] = {}
if local_state is not None:
self.local_state = {k: list(v) for k, v in local_state.items()}
super().__init__() super().__init__()
def _get_scope(self, scope: GitConfigScope) -> Dict[str, list[str]]: @contextlib.contextmanager
ret = self.state.get(scope, None) def _editable_scope(
if ret is None: self, scope: GitConfigScope) -> Iterator[dict[str, list[str]]]:
ret = {} if scope == 'system':
self.state[scope] = ret # This is also checked in CachedGitConfigState.SetConfig, but double
return ret # check here.
raise GitConfigUneditableScope(scope)
if scope == 'global':
with self.global_state_lock:
yield self.global_state
elif scope == 'local':
yield self.local_state
elif scope == 'worktree':
yield self.worktree_state
else:
# This is also checked in CachedGitConfigState.SetConfig, but double
# check here.
raise GitConfigUnknownScope(scope)
def load_config(self) -> GitFlatConfigData: def load_config(self) -> GitFlatConfigData:
ret = {} ret = {k: list(v) for k, v in self.system_state.items()}
for scope in GitScopeOrder: for scope in GitScopeOrder:
for key, value in self._get_scope(scope).items(): if scope == 'system':
curvals = ret.get(key, None) continue
if curvals is None: with self._editable_scope(scope) as cfg:
curvals = [] for key, value in cfg.items():
ret[key] = curvals curvals = ret.get(key, None)
curvals.extend(value) if curvals is None:
curvals = []
ret[key] = curvals
curvals.extend(value)
return ret return ret
def set_config(self, key: str, value: str, *, append: bool, def set_config(self, key: str, value: str, *, append: bool,
scope: GitConfigScope): scope: GitConfigScope):
cfg = self._get_scope(scope) with self._editable_scope(scope) as cfg:
cur = cfg.get(key) cur = cfg.get(key)
if cur is None or len(cur) == 1: if cur is None or len(cur) == 1:
if append: if append:
cfg[key] = (cur or []) + [value] cfg[key] = (cur or []) + [value]
else: else:
cfg[key] = [value] cfg[key] = [value]
return return
raise ValueError(f'GitConfigStateTest: Cannot set key {key} ' raise ValueError(f'GitConfigStateTest: Cannot set key {key} '
f'- current value {cur!r} is multiple.') f'- current value {cur!r} is multiple.')
def set_config_multi(self, key: str, value: str, *, append: bool, def set_config_multi(self, key: str, value: str, *,
value_pattern: Optional[str], scope: GitConfigScope): value_pattern: Optional[str], scope: GitConfigScope):
cfg = self._get_scope(scope) with self._editable_scope(scope) as cfg:
cur = cfg.get(key) cur = cfg.get(key)
if value_pattern is None or cur is None: if value_pattern is None or cur is None:
if append:
cfg[key] = (cur or []) + [value]
else:
cfg[key] = [value] cfg[key] = [value]
return return
pat = re.compile(value_pattern) # We want to insert `value` in place of the first pattern match - if
newval = [v for v in cur if pat.match(v)] # multiple values match, they will all be removed.
newval.append(value) pat = re.compile(value_pattern)
cfg[key] = newval newval = []
added = False
for val in cur:
if pat.match(val):
if not added:
newval.append(value)
added = True
else:
newval.append(val)
if not added:
newval.append(value)
cfg[key] = newval
def unset_config(self, key: str, *, scope: GitConfigScope, def unset_config(self, key: str, *, scope: GitConfigScope,
missing_ok: bool): missing_ok: bool):
cfg = self._get_scope(scope) with self._editable_scope(scope) as cfg:
cur = cfg.get(key) cur = cfg.get(key)
if cur is None: if cur is None:
if missing_ok: if missing_ok:
return
raise GitConfigUnsetMissingValue(key, scope)
if len(cur) == 1:
del cfg[key]
return return
raise GitConfigUnsetMissingValue(key, scope) raise GitConfigUnsetMultipleValues(key, scope)
if len(cur) == 1:
del cfg[key]
return
raise ValueError(f'GitConfigStateTest: Cannot unset key {key} '
f'- current value {cur!r} is multiple.')
def unset_config_multi(self, key: str, *, value_pattern: Optional[str], def unset_config_multi(self, key: str, *, value_pattern: Optional[str],
scope: GitConfigScope, missing_ok: bool): scope: GitConfigScope, missing_ok: bool):
cfg = self._get_scope(scope) with self._editable_scope(scope) as cfg:
cur = cfg.get(key) cur = cfg.get(key)
if cur is None: if cur is None:
if not missing_ok: if not missing_ok:
raise GitConfigUnsetMissingValue(key, scope) raise GitConfigUnsetMissingValue(key, scope)
return return
if value_pattern is None: if value_pattern is None:
del cfg[key] del cfg[key]
return return
if cur is None: if cur is None:
del cfg[key] del cfg[key]
return return
pat = re.compile(value_pattern) pat = re.compile(value_pattern)
cfg[key] = [v for v in cur if not pat.match(v)] cfg[key] = [v for v in cur if not pat.match(v)]
class GIT(object): class GIT(object):
@ -608,17 +704,19 @@ class GIT(object):
key: The specific config key to affect. key: The specific config key to affect.
value: The value to set. If this is None, `key` will be unset. value: The value to set. If this is None, `key` will be unset.
append: If True and `value` is not None, this will append append: If True and `value` is not None, this will append
the value instead of replacing an existing one. the value instead of replacing an existing one. Must not be
specified with value_pattern.
missing_ok: If `value` is None (i.e. this is an unset operation), missing_ok: If `value` is None (i.e. this is an unset operation),
ignore retcode=5 from `git config` (meaning that the value is ignore retcode=5 from `git config` (meaning that the value is
not present). If `value` is not None, then this option has no not present). If `value` is not None, then this option has no
effect. effect. If this is false and the key is missing, this will raise
GitConfigUnsetMissingValue.
modify_all: If True, this will change a set operation to modify_all: If True, this will change a set operation to
`--replace-all`, and will change an unset operation to `--replace-all`, and will change an unset operation to
`--unset-all`. `--unset-all`. Must not be specified with value_pattern.
scope: By default this is the local scope, but could be `system`, scope: By default this is the `local` scope, but could be `global`
`global`, or `worktree`, depending on which config scope you or `worktree`, depending on which config scope you want to affect.
want to affect. Note that the `system` scope cannot be modified.
value_pattern: For use with `modify_all=True`, allows value_pattern: For use with `modify_all=True`, allows
further filtering of the set or unset operation based on further filtering of the set or unset operation based on
the currently configured value. Ignored for the currently configured value. Ignored for
@ -989,3 +1087,6 @@ class DIFF(object):
dirnames[:] = [d for d in dirnames if should_recurse(dirpath, d)] dirnames[:] = [d for d in dirnames if should_recurse(dirpath, d)]
return [os.path.relpath(p, cwd) for p in paths] return [os.path.relpath(p, cwd) for p in paths]
# vim: sts=4:ts=4:sw=4:tw=80:et:

@ -4,6 +4,7 @@
import os import os
import sys import sys
import threading
from typing import Dict, List, Optional from typing import Dict, List, Optional
from unittest import mock from unittest import mock
@ -29,21 +30,24 @@ def GIT(test: unittest.TestCase,
NOTE: The dependency on git_new_branch.create_new_branch seems pretty NOTE: The dependency on git_new_branch.create_new_branch seems pretty
circular - this functionality should probably move to scm.GIT? circular - this functionality should probably move to scm.GIT?
""" """
# TODO - remove `config` - have callers just directly call SetConfig with
# whatever config state they need.
# TODO - add `system_config` - this will be configuration which exists at
# the 'system installation' level and is immutable.
_branchref = [branchref or 'refs/heads/main'] _branchref = [branchref or 'refs/heads/main']
initial_state = {} global_lock = threading.Lock()
if config is not None: global_state = {}
initial_state['local'] = config
def _newBranch(branchref): def _newBranch(branchref):
_branchref[0] = branchref _branchref[0] = branchref
patches: List[mock._patch] = [ patches: List[mock._patch] = [
mock.patch( mock.patch('scm.GIT._new_config_state',
'scm.GIT._new_config_state', side_effect=lambda _: scm.GitConfigStateTest(
side_effect=lambda root: scm.GitConfigStateTest(initial_state)), global_lock, global_state, local_state=config)),
mock.patch('scm.GIT.GetBranchRef', mock.patch('scm.GIT.GetBranchRef', side_effect=lambda _: _branchref[0]),
side_effect=lambda _root: _branchref[0]),
mock.patch('git_new_branch.create_new_branch', side_effect=_newBranch) mock.patch('git_new_branch.create_new_branch', side_effect=_newBranch)
] ]

@ -4,10 +4,13 @@
# found in the LICENSE file. # found in the LICENSE file.
"""Unit tests for scm.py.""" """Unit tests for scm.py."""
from __future__ import annotations
import logging import logging
import os import os
import sys import sys
import tempfile import tempfile
import threading
import unittest import unittest
from unittest import mock from unittest import mock
@ -418,9 +421,256 @@ class DiffTestCase(unittest.TestCase):
files, ["foo/file.txt", "foo/dir/file.txt", "baz_repo"]) files, ["foo/file.txt", "foo/dir/file.txt", "baz_repo"])
class GitConfigStateTestTest(unittest.TestCase):
@staticmethod
def _make(*,
global_state: dict[str, list[str]] | None = None,
system_state: dict[str, list[str]] | None = None,
local_state: dict[str, list[str]] | None = None,
worktree_state: dict[str, list[str]] | None = None):
"""_make constructs a GitConfigStateTest with an internal Lock.
If global_state is None, an empty dictionary will be constructed and
returned, otherwise the caller's provided global_state is returned,
unmodified.
Returns (GitConfigStateTest, global_state) - access to global_state must
be manually synchronized with access to GitConfigStateTest, or at least
with GitConfigStateTest.global_state_lock.
"""
global_state = global_state or {}
m = scm.GitConfigStateTest(threading.Lock(),
global_state,
system_state=system_state,
local_state=local_state,
worktree_state=worktree_state)
return m, global_state
def test_construction_empty(self):
m, gs = self._make()
self.assertDictEqual(gs, {})
self.assertDictEqual(m.load_config(), {})
gs['key'] = ['override']
self.assertDictEqual(m.load_config(), {'key': ['override']})
def test_construction_global(self):
m, gs = self._make(global_state={'key': ['global']})
self.assertDictEqual(gs, {'key': ['global']})
self.assertDictEqual(m.load_config(), {'key': ['global']})
gs['key'] = ['override']
self.assertDictEqual(m.load_config(), {'key': ['override']})
def test_construction_system(self):
m, gs = self._make(
global_state={'key': ['global']},
system_state={'key': ['system']},
)
self.assertDictEqual(gs, {'key': ['global']})
self.assertDictEqual(m.load_config(), {'key': ['system', 'global']})
gs['key'] = ['override']
self.assertDictEqual(m.load_config(), {'key': ['system', 'override']})
def test_construction_local(self):
m, gs = self._make(
global_state={'key': ['global']},
system_state={'key': ['system']},
local_state={'key': ['local']},
)
self.assertDictEqual(gs, {'key': ['global']})
self.assertDictEqual(m.load_config(), {
'key': ['system', 'global', 'local'],
})
gs['key'] = ['override']
self.assertDictEqual(m.load_config(), {
'key': ['system', 'override', 'local'],
})
def test_construction_worktree(self):
m, gs = self._make(
global_state={'key': ['global']},
system_state={'key': ['system']},
local_state={'key': ['local']},
worktree_state={'key': ['worktree']},
)
self.assertDictEqual(gs, {'key': ['global']})
self.assertDictEqual(m.load_config(), {
'key': ['system', 'global', 'local', 'worktree'],
})
gs['key'] = ['override']
self.assertDictEqual(m.load_config(), {
'key': ['system', 'override', 'local', 'worktree'],
})
def test_set_config_system(self):
m, _ = self._make()
with self.assertRaises(scm.GitConfigUneditableScope):
m.set_config('key', 'new_global', append=False, scope='system')
def test_set_config_unkown(self):
m, _ = self._make()
with self.assertRaises(scm.GitConfigUnknownScope):
m.set_config('key', 'new_global', append=False, scope='meepmorp')
def test_set_config_global(self):
m, gs = self._make()
self.assertDictEqual(gs, {})
self.assertDictEqual(m.load_config(), {})
m.set_config('key', 'new_global', append=False, scope='global')
self.assertDictEqual(m.load_config(), {
'key': ['new_global'],
})
m.set_config('key', 'new_global2', append=True, scope='global')
self.assertDictEqual(m.load_config(), {
'key': ['new_global', 'new_global2'],
})
self.assertDictEqual(gs, {
'key': ['new_global', 'new_global2'],
})
def test_set_config_multi_global(self):
m, gs = self._make(global_state={
'key': ['1', '2'],
})
m.set_config_multi('key',
'new_global',
value_pattern=None,
scope='global')
self.assertDictEqual(m.load_config(), {
'key': ['new_global'],
})
self.assertDictEqual(gs, {
'key': ['new_global'],
})
m.set_config_multi('other',
'newval',
value_pattern=None,
scope='global')
self.assertDictEqual(m.load_config(), {
'key': ['new_global'],
'other': ['newval'],
})
self.assertDictEqual(gs, {
'key': ['new_global'],
'other': ['newval'],
})
def test_set_config_multi_global_pattern(self):
m, _ = self._make(global_state={
'key': ['1', '1', '2', '2', '2', '3'],
})
m.set_config_multi('key',
'new_global',
value_pattern='2',
scope='global')
self.assertDictEqual(m.load_config(), {
'key': ['1', '1', 'new_global', '3'],
})
m.set_config_multi('key',
'additional',
value_pattern='narp',
scope='global')
self.assertDictEqual(m.load_config(), {
'key': ['1', '1', 'new_global', '3', 'additional'],
})
def test_unset_config_global(self):
m, _ = self._make(global_state={
'key': ['someval'],
})
m.unset_config('key', scope='global', missing_ok=False)
self.assertDictEqual(m.load_config(), {})
with self.assertRaises(scm.GitConfigUnsetMissingValue):
m.unset_config('key', scope='global', missing_ok=False)
self.assertDictEqual(m.load_config(), {})
m.unset_config('key', scope='global', missing_ok=True)
self.assertDictEqual(m.load_config(), {})
def test_unset_config_global_extra(self):
m, _ = self._make(global_state={
'key': ['someval'],
'extra': ['another'],
})
m.unset_config('key', scope='global', missing_ok=False)
self.assertDictEqual(m.load_config(), {
'extra': ['another'],
})
with self.assertRaises(scm.GitConfigUnsetMissingValue):
m.unset_config('key', scope='global', missing_ok=False)
self.assertDictEqual(m.load_config(), {
'extra': ['another'],
})
m.unset_config('key', scope='global', missing_ok=True)
self.assertDictEqual(m.load_config(), {
'extra': ['another'],
})
def test_unset_config_global_multi(self):
m, _ = self._make(global_state={
'key': ['1', '2'],
})
with self.assertRaises(scm.GitConfigUnsetMultipleValues):
m.unset_config('key', scope='global', missing_ok=True)
def test_unset_config_multi_global(self):
m, _ = self._make(global_state={
'key': ['1', '2'],
})
m.unset_config_multi('key',
value_pattern=None,
scope='global',
missing_ok=False)
self.assertDictEqual(m.load_config(), {})
with self.assertRaises(scm.GitConfigUnsetMissingValue):
m.unset_config_multi('key',
value_pattern=None,
scope='global',
missing_ok=False)
def test_unset_config_multi_global_pattern(self):
m, _ = self._make(global_state={
'key': ['1', '2', '3', '1', '2'],
})
m.unset_config_multi('key',
value_pattern='2',
scope='global',
missing_ok=False)
self.assertDictEqual(m.load_config(), {
'key': ['1', '3', '1'],
})
if __name__ == '__main__': if __name__ == '__main__':
if '-v' in sys.argv: if '-v' in sys.argv:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
unittest.main() unittest.main()
# vim: ts=2:sw=2:tw=80:et: # vim: ts=4:sw=4:tw=80:et:

Loading…
Cancel
Save