Skip to content

Commit

Permalink
Merge pull request #40 from nolar/test-diffs
Browse files Browse the repository at this point in the history
Test diffs calculation and manipulation
  • Loading branch information
Sergey Vasilyev committed Apr 26, 2019
2 parents 3a04596 + 6616650 commit 96c2787
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 6 deletions.
17 changes: 12 additions & 5 deletions kopf/structs/diffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ def resolve(d: Mapping, path: DiffPath):

def reduce_iter(d: Diff, path: DiffPath) -> Generator[DiffItem, None, None]:
for op, field, old, new in d:
if field[:len(path)] == path:
yield (op, field[len(path):], old, new)
if not path or tuple(field[:len(path)]) == tuple(path):
yield (op, tuple(field[len(path):]), old, new)


def reduce(d: Diff, path: DiffPath) -> Diff:
return type(d)(reduce_iter(d, path))
return tuple(reduce_iter(d, path))


def diff(a: Any, b: Any, path: DiffPath = ()) -> Generator[DiffItem, None, None]:
def diff_iter(a: Any, b: Any, path: DiffPath = ()) -> Generator[DiffItem, None, None]:
"""
Calculate the diff between two dicts.
Expand Down Expand Up @@ -60,6 +60,13 @@ def diff(a: Any, b: Any, path: DiffPath = ()) -> Generator[DiffItem, None, None]
for key in a_keys - b_keys:
yield ('remove', path+(key,), a[key], None)
for key in a_keys & b_keys:
yield from diff(a[key], b[key], path=path+(key,))
yield from diff_iter(a[key], b[key], path=path+(key,))
else:
yield ('change', path, a, b)


def diff(a: Any, b: Any, path: DiffPath = ()) -> Diff:
"""
Same as `diff`, but returns the whole tuple instead of iterator.
"""
return tuple(diff_iter(a, b, path=path))
2 changes: 1 addition & 1 deletion kopf/structs/lastseen.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def is_state_changed(body):
def get_state_diffs(body):
old = retreive_state(body)
new = get_state(body)
return old, new, list(diff(old, new))
return old, new, diff(old, new)


def retreive_state(body):
Expand Down
78 changes: 78 additions & 0 deletions tests/diffs/test_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from kopf.structs.diffs import diff


def test_scalars_equal():
a = 100
b = 100
d = diff(a, b)
assert d == ()


def test_scalars_unequal():
a = 100
b = 200
d = diff(a, b)
assert d == (('change', (), 100, 200),)


def test_strings_equal():
a = 'hello'
b = 'hello'
d = diff(a, b)
assert d == ()


def test_strings_unequal():
a = 'hello'
b = 'world'
d = diff(a, b)
assert d == (('change', (), 'hello', 'world'),)


def test_lists_equal():
a = [100, 200, 300]
b = [100, 200, 300]
d = diff(a, b)
assert d == ()


def test_lists_unequal():
a = [100, 200, 300]
b = [100, 666, 300]
d = diff(a, b)
assert d == (('change', (), [100, 200, 300], [100, 666, 300]),)


def test_dicts_equal():
a = {'hello': 'world', 'key': 'val'}
b = {'key': 'val', 'hello': 'world'}
d = diff(a, b)
assert d == ()


def test_dicts_with_keys_added():
a = {'hello': 'world'}
b = {'hello': 'world', 'key': 'val'}
d = diff(a, b)
assert d == (('add', ('key',), None, 'val'),)


def test_dicts_with_keys_removed():
a = {'hello': 'world', 'key': 'val'}
b = {'hello': 'world'}
d = diff(a, b)
assert d == (('remove', ('key',), 'val', None),)


def test_dicts_with_keys_changed():
a = {'hello': 'world', 'key': 'old'}
b = {'hello': 'world', 'key': 'new'}
d = diff(a, b)
assert d == (('change', ('key',), 'old', 'new'),)


def test_dicts_with_subkeys_changed():
a = {'main': {'hello': 'world', 'key': 'old'}}
b = {'main': {'hello': 'world', 'key': 'new'}}
d = diff(a, b)
assert d == (('change', ('main', 'key'), 'old', 'new'),)
54 changes: 54 additions & 0 deletions tests/diffs/test_reduction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pytest

from kopf.structs.diffs import reduce


DIFF = (
('op' , (), 'old', 'new'), # unknown operations should be passed through
('add' , ('key1',), None, 'new1'),
('change', ('key2',), 'old2', 'new2'),
('add' , ('key2', 'suba'), 'olda', 'newa'),
('remove', ('key2', 'subb'), 'oldb', 'newb'),
('remove', ('key3',), 'old3', None),
)


@pytest.mark.parametrize('diff', [
[['op', ['key', 'sub'], 'old', 'new']],
[['op', ('key', 'sub'), 'old', 'new']],
[('op', ['key', 'sub'], 'old', 'new')],
[('op', ('key', 'sub'), 'old', 'new')],
(['op', ['key', 'sub'], 'old', 'new'],),
(['op', ('key', 'sub'), 'old', 'new'],),
(('op', ['key', 'sub'], 'old', 'new'),),
(('op', ('key', 'sub'), 'old', 'new'),),
], ids=[
'lll-diff', 'llt-diff', 'ltl-diff', 'ltt-diff',
'tll-diff', 'tlt-diff', 'ttl-diff', 'ttt-diff',
])
@pytest.mark.parametrize('path', [
['key', 'sub'],
('key', 'sub'),
], ids=['list-path', 'tuple-path'])
def test_type_ignored_for_inputs_but_is_tuple_for_output(diff, path):
result = reduce(diff, path)
assert result == (('op', (), 'old', 'new'),)


def test_empty_path_selects_all_ops():
result = reduce(DIFF, [])
assert result == DIFF


def test_existent_path_selects_relevant_ops():
result = reduce(DIFF, ['key2'])
assert result == (
('change', (), 'old2', 'new2'),
('add' , ('suba',), 'olda', 'newa'),
('remove', ('subb',), 'oldb', 'newb'),
)


def test_nonexistent_path_selects_nothing():
result = reduce(DIFF, ['nonexistent-key'])
assert result == ()
28 changes: 28 additions & 0 deletions tests/diffs/test_resolving.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from kopf.structs.diffs import resolve


def test_existing_key():
d = {'abc': {'def': {'hij': 'val'}}}
r = resolve(d, ['abc', 'def', 'hij'])
assert r == 'val'


def test_unexisting_key():
d = {'abc': {'def': {'hij': 'val'}}}
with pytest.raises(KeyError):
resolve(d, ['abc', 'def', 'xyz'])


def test_nonmapping_key():
d = {'key': 'val'}
with pytest.raises(TypeError):
resolve(d, ['key', 'sub'])


def test_empty_path():
d = {'key': 'val'}
r = resolve(d, [])
assert r == d
assert r is d

0 comments on commit 96c2787

Please sign in to comment.