mirror of
https://github.com/python/cpython.git
synced 2025-09-27 10:50:04 +00:00
Issue 7832: renaming unittest.TestCase.assertSameElements to assertItemsEqual and changing behaviour
This commit is contained in:
parent
2e6d2622bd
commit
98e7b7644b
6 changed files with 144 additions and 68 deletions
|
@ -786,7 +786,7 @@ Test cases
|
||||||
will be included in the error message. This method is used by default
|
will be included in the error message. This method is used by default
|
||||||
when comparing Unicode strings with :meth:`assertEqual`.
|
when comparing Unicode strings with :meth:`assertEqual`.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
@ -807,22 +807,24 @@ Test cases
|
||||||
Tests that *first* is or is not in *second* with an explanatory error
|
Tests that *first* is or is not in *second* with an explanatory error
|
||||||
message as appropriate.
|
message as appropriate.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
|
||||||
.. method:: assertSameElements(actual, expected, msg=None)
|
.. method:: assertItemsEqual(actual, expected, msg=None)
|
||||||
|
|
||||||
Test that sequence *expected* contains the same elements as *actual*,
|
Test that sequence *expected* contains the same elements as *actual*,
|
||||||
regardless of their order. When they don't, an error message listing
|
regardless of their order. When they don't, an error message listing the
|
||||||
the differences between the sequences will be generated.
|
differences between the sequences will be generated.
|
||||||
|
|
||||||
Duplicate elements are ignored when comparing *actual* and *expected*.
|
Duplicate elements are *not* ignored when comparing *actual* and
|
||||||
It is the equivalent of ``assertEqual(set(expected), set(actual))``
|
*expected*. It verifies if each element has the same count in both
|
||||||
but it works with sequences of unhashable objects as well.
|
sequences. It is the equivalent of ``assertEqual(sorted(expected),
|
||||||
|
sorted(actual))`` but it works with sequences of unhashable objects as
|
||||||
|
well.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
@ -836,7 +838,7 @@ Test cases
|
||||||
Fails if either of *set1* or *set2* does not have a :meth:`set.difference`
|
Fails if either of *set1* or *set2* does not have a :meth:`set.difference`
|
||||||
method.
|
method.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
@ -848,7 +850,7 @@ Test cases
|
||||||
method will be used by default to compare dictionaries in
|
method will be used by default to compare dictionaries in
|
||||||
calls to :meth:`assertEqual`.
|
calls to :meth:`assertEqual`.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
@ -859,7 +861,7 @@ Test cases
|
||||||
superset of those in *expected*. If not, an error message listing
|
superset of those in *expected*. If not, an error message listing
|
||||||
the missing keys and mismatched values is generated.
|
the missing keys and mismatched values is generated.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
@ -873,7 +875,7 @@ Test cases
|
||||||
These methods are used by default when comparing lists or tuples with
|
These methods are used by default when comparing lists or tuples with
|
||||||
:meth:`assertEqual`.
|
:meth:`assertEqual`.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
.. versionadded:: 2.7
|
||||||
|
|
||||||
|
@ -885,7 +887,7 @@ Test cases
|
||||||
be raised. If the sequences are different an error message is
|
be raised. If the sequences are different an error message is
|
||||||
constructed that shows the difference between the two.
|
constructed that shows the difference between the two.
|
||||||
|
|
||||||
If specified *msg* will be used as the error message on failure.
|
If specified, *msg* will be used as the error message on failure.
|
||||||
|
|
||||||
This method is used to implement :meth:`assertListEqual` and
|
This method is used to implement :meth:`assertListEqual` and
|
||||||
:meth:`assertTupleEqual`.
|
:meth:`assertTupleEqual`.
|
||||||
|
|
|
@ -1086,7 +1086,7 @@ GvR worked on merging them into Python's version of :mod:`unittest`.
|
||||||
* :meth:`assertIn` and :meth:`assertNotIn` tests whether
|
* :meth:`assertIn` and :meth:`assertNotIn` tests whether
|
||||||
*first* is or is not in *second*.
|
*first* is or is not in *second*.
|
||||||
|
|
||||||
* :meth:`assertSameElements` tests whether two provided sequences
|
* :meth:`assertItemsEqual` tests whether two provided sequences
|
||||||
contain the same elements.
|
contain the same elements.
|
||||||
|
|
||||||
* :meth:`assertSetEqual` compares whether two sets are equal, and
|
* :meth:`assertSetEqual` compares whether two sets are equal, and
|
||||||
|
|
|
@ -135,18 +135,18 @@ class CgiTests(unittest.TestCase):
|
||||||
if isinstance(expect, dict):
|
if isinstance(expect, dict):
|
||||||
# test dict interface
|
# test dict interface
|
||||||
self.assertEqual(len(expect), len(fcd))
|
self.assertEqual(len(expect), len(fcd))
|
||||||
self.assertSameElements(expect.keys(), fcd.keys())
|
self.assertItemsEqual(expect.keys(), fcd.keys())
|
||||||
self.assertSameElements(expect.values(), fcd.values())
|
self.assertItemsEqual(expect.values(), fcd.values())
|
||||||
self.assertSameElements(expect.items(), fcd.items())
|
self.assertItemsEqual(expect.items(), fcd.items())
|
||||||
self.assertEqual(fcd.get("nonexistent field", "default"), "default")
|
self.assertEqual(fcd.get("nonexistent field", "default"), "default")
|
||||||
self.assertEqual(len(sd), len(fs))
|
self.assertEqual(len(sd), len(fs))
|
||||||
self.assertSameElements(sd.keys(), fs.keys())
|
self.assertItemsEqual(sd.keys(), fs.keys())
|
||||||
self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
|
self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
|
||||||
# test individual fields
|
# test individual fields
|
||||||
for key in expect.keys():
|
for key in expect.keys():
|
||||||
expect_val = expect[key]
|
expect_val = expect[key]
|
||||||
self.assertTrue(fcd.has_key(key))
|
self.assertTrue(fcd.has_key(key))
|
||||||
self.assertSameElements(fcd[key], expect[key])
|
self.assertItemsEqual(fcd[key], expect[key])
|
||||||
self.assertEqual(fcd.get(key, "default"), fcd[key])
|
self.assertEqual(fcd.get(key, "default"), fcd[key])
|
||||||
self.assertTrue(fs.has_key(key))
|
self.assertTrue(fs.has_key(key))
|
||||||
if len(expect_val) > 1:
|
if len(expect_val) > 1:
|
||||||
|
@ -162,11 +162,11 @@ class CgiTests(unittest.TestCase):
|
||||||
self.assertTrue(single_value)
|
self.assertTrue(single_value)
|
||||||
self.assertEqual(val, expect_val[0])
|
self.assertEqual(val, expect_val[0])
|
||||||
self.assertEqual(fs.getvalue(key), expect_val[0])
|
self.assertEqual(fs.getvalue(key), expect_val[0])
|
||||||
self.assertSameElements(sd.getlist(key), expect_val)
|
self.assertItemsEqual(sd.getlist(key), expect_val)
|
||||||
if single_value:
|
if single_value:
|
||||||
self.assertSameElements(sd.values(),
|
self.assertItemsEqual(sd.values(),
|
||||||
first_elts(expect.values()))
|
first_elts(expect.values()))
|
||||||
self.assertSameElements(sd.items(),
|
self.assertItemsEqual(sd.items(),
|
||||||
first_second_elts(expect.items()))
|
first_second_elts(expect.items()))
|
||||||
|
|
||||||
def test_weird_formcontentdict(self):
|
def test_weird_formcontentdict(self):
|
||||||
|
@ -178,7 +178,7 @@ class CgiTests(unittest.TestCase):
|
||||||
self.assertEqual(d[k], v)
|
self.assertEqual(d[k], v)
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
self.assertEqual(expect[k], v)
|
self.assertEqual(expect[k], v)
|
||||||
self.assertSameElements(expect.values(), d.values())
|
self.assertItemsEqual(expect.values(), d.values())
|
||||||
|
|
||||||
def test_log(self):
|
def test_log(self):
|
||||||
cgi.log("Testing")
|
cgi.log("Testing")
|
||||||
|
|
|
@ -2575,9 +2575,9 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
||||||
class SadSnake(object):
|
class SadSnake(object):
|
||||||
"""Dummy class for test_addTypeEqualityFunc."""
|
"""Dummy class for test_addTypeEqualityFunc."""
|
||||||
s1, s2 = SadSnake(), SadSnake()
|
s1, s2 = SadSnake(), SadSnake()
|
||||||
self.assertFalse(s1 == s2)
|
self.assertNotEqual(s1, s2)
|
||||||
def AllSnakesCreatedEqual(a, b, msg=None):
|
def AllSnakesCreatedEqual(a, b, msg=None):
|
||||||
return type(a) == type(b) == SadSnake
|
return type(a) is type(b) is SadSnake
|
||||||
self.addTypeEqualityFunc(SadSnake, AllSnakesCreatedEqual)
|
self.addTypeEqualityFunc(SadSnake, AllSnakesCreatedEqual)
|
||||||
self.assertEqual(s1, s2)
|
self.assertEqual(s1, s2)
|
||||||
# No this doesn't clean up and remove the SadSnake equality func
|
# No this doesn't clean up and remove the SadSnake equality func
|
||||||
|
@ -2745,21 +2745,51 @@ class Test_TestCase(TestCase, TestEquality, TestHashing):
|
||||||
self.assertRaises(self.failureException, self.assertDictEqual, [], d)
|
self.assertRaises(self.failureException, self.assertDictEqual, [], d)
|
||||||
self.assertRaises(self.failureException, self.assertDictEqual, 1, 1)
|
self.assertRaises(self.failureException, self.assertDictEqual, 1, 1)
|
||||||
|
|
||||||
self.assertSameElements([1, 2, 3], [3, 2, 1])
|
def testAssertItemsEqual(self):
|
||||||
self.assertSameElements([1, 2] + [3] * 100, [1] * 100 + [2, 3])
|
a = object()
|
||||||
self.assertSameElements(['foo', 'bar', 'baz'], ['bar', 'baz', 'foo'])
|
self.assertItemsEqual([1, 2, 3], [3, 2, 1])
|
||||||
self.assertRaises(self.failureException, self.assertSameElements,
|
self.assertItemsEqual(['foo', 'bar', 'baz'], ['bar', 'baz', 'foo'])
|
||||||
|
self.assertItemsEqual([a, a, 2, 2, 3], (a, 2, 3, a, 2))
|
||||||
|
self.assertItemsEqual([1, "2", "a", "a"], ["a", "2", True, "a"])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[1, 2] + [3] * 100, [1] * 100 + [2, 3])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[1, "2", "a", "a"], ["a", "2", True, 1])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
[10], [10, 11])
|
[10], [10, 11])
|
||||||
self.assertRaises(self.failureException, self.assertSameElements,
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
[10, 11], [10])
|
[10, 11], [10])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[10, 11, 10], [10, 11])
|
||||||
|
|
||||||
# Test that sequences of unhashable objects can be tested for sameness:
|
# Test that sequences of unhashable objects can be tested for sameness:
|
||||||
self.assertSameElements([[1, 2], [3, 4]], [[3, 4], [1, 2]])
|
self.assertItemsEqual([[1, 2], [3, 4], 0], [False, [3, 4], [1, 2]])
|
||||||
|
with test_support.check_warnings(quiet=True) as w:
|
||||||
self.assertSameElements([{'a': 1}, {'b': 2}], [{'b': 2}, {'a': 1}])
|
# hashable types, but not orderable
|
||||||
self.assertRaises(self.failureException, self.assertSameElements,
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[], [divmod, 'x', 1, 5j, 2j, frozenset()])
|
||||||
|
# comparing dicts raises a py3k warning
|
||||||
|
self.assertItemsEqual([{'a': 1}, {'b': 2}], [{'b': 2}, {'a': 1}])
|
||||||
|
# comparing heterogenous non-hashable sequences raises a py3k warning
|
||||||
|
self.assertItemsEqual([1, 'x', divmod, []], [divmod, [], 'x', 1])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[], [divmod, [], 'x', 1, 5j, 2j, set()])
|
||||||
|
# fail the test if warnings are not silenced
|
||||||
|
if w.warnings:
|
||||||
|
self.fail('assertItemsEqual raised a warning: ' +
|
||||||
|
str(w.warnings[0]))
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
[[1]], [[2]])
|
[[1]], [[2]])
|
||||||
|
|
||||||
|
# Same elements, but not same sequence length
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[1, 1, 2], [2, 1])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[1, 1, "2", "a", "a"], ["2", "2", True, "a"])
|
||||||
|
self.assertRaises(self.failureException, self.assertItemsEqual,
|
||||||
|
[1, {'b': 2}, None, True], [{'b': 2}, True, None])
|
||||||
|
|
||||||
|
|
||||||
def testAssertSetEqual(self):
|
def testAssertSetEqual(self):
|
||||||
set1 = set()
|
set1 = set()
|
||||||
set2 = set()
|
set2 = set()
|
||||||
|
@ -3009,13 +3039,14 @@ test case
|
||||||
|
|
||||||
Do not use these methods. They will go away in 3.3.
|
Do not use these methods. They will go away in 3.3.
|
||||||
"""
|
"""
|
||||||
self.failIfEqual(3, 5)
|
with test_support.check_warnings():
|
||||||
self.failUnlessEqual(3, 3)
|
self.failIfEqual(3, 5)
|
||||||
self.failUnlessAlmostEqual(2.0, 2.0)
|
self.failUnlessEqual(3, 3)
|
||||||
self.failIfAlmostEqual(3.0, 5.0)
|
self.failUnlessAlmostEqual(2.0, 2.0)
|
||||||
self.failUnless(True)
|
self.failIfAlmostEqual(3.0, 5.0)
|
||||||
self.failUnlessRaises(TypeError, lambda _: 3.14 + u'spam')
|
self.failUnless(True)
|
||||||
self.failIf(False)
|
self.failUnlessRaises(TypeError, lambda _: 3.14 + u'spam')
|
||||||
|
self.failIf(False)
|
||||||
|
|
||||||
def testDeepcopy(self):
|
def testDeepcopy(self):
|
||||||
# Issue: 5660
|
# Issue: 5660
|
||||||
|
@ -3355,8 +3386,8 @@ class TestLongMessage(TestCase):
|
||||||
"^Missing: 'key'$",
|
"^Missing: 'key'$",
|
||||||
"^Missing: 'key' : oops$"])
|
"^Missing: 'key' : oops$"])
|
||||||
|
|
||||||
def testAssertSameElements(self):
|
def testAssertItemsEqual(self):
|
||||||
self.assertMessages('assertSameElements', ([], [None]),
|
self.assertMessages('assertItemsEqual', ([], [None]),
|
||||||
[r"\[None\]$", "^oops$",
|
[r"\[None\]$", "^oops$",
|
||||||
r"\[None\]$",
|
r"\[None\]$",
|
||||||
r"\[None\] : oops$"])
|
r"\[None\] : oops$"])
|
||||||
|
|
|
@ -8,8 +8,9 @@ import re
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from . import result
|
from . import result
|
||||||
from .util import strclass, safe_repr, sorted_list_difference
|
from .util import (
|
||||||
|
strclass, safe_repr, sorted_list_difference, unorderable_list_difference
|
||||||
|
)
|
||||||
|
|
||||||
class SkipTest(Exception):
|
class SkipTest(Exception):
|
||||||
"""
|
"""
|
||||||
|
@ -686,10 +687,9 @@ class TestCase(object):
|
||||||
msg: Optional message to use on failure instead of a list of
|
msg: Optional message to use on failure instead of a list of
|
||||||
differences.
|
differences.
|
||||||
|
|
||||||
For more general containership equality, assertSameElements will work
|
assertSetEqual uses ducktyping to support different types of sets, and
|
||||||
with things other than sets. This uses ducktyping to support
|
is optimized for sets specifically (parameters must support a
|
||||||
different types of sets, and is optimized for sets specifically
|
difference method).
|
||||||
(parameters must support a difference method).
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
difference1 = set1.difference(set2)
|
difference1 = set1.difference(set2)
|
||||||
|
@ -784,42 +784,48 @@ class TestCase(object):
|
||||||
|
|
||||||
self.fail(self._formatMessage(msg, standardMsg))
|
self.fail(self._formatMessage(msg, standardMsg))
|
||||||
|
|
||||||
def assertSameElements(self, expected_seq, actual_seq, msg=None):
|
def assertItemsEqual(self, expected_seq, actual_seq, msg=None):
|
||||||
"""An unordered sequence specific comparison.
|
"""An unordered sequence / set specific comparison. It asserts that
|
||||||
|
expected_seq and actual_seq contain the same elements. It is
|
||||||
|
the equivalent of::
|
||||||
|
|
||||||
|
self.assertEqual(sorted(expected_seq), sorted(actual_seq))
|
||||||
|
|
||||||
Raises with an error message listing which elements of expected_seq
|
Raises with an error message listing which elements of expected_seq
|
||||||
are missing from actual_seq and vice versa if any.
|
are missing from actual_seq and vice versa if any.
|
||||||
|
|
||||||
Duplicate elements are ignored when comparing *expected_seq* and
|
Asserts that each element has the same count in both sequences.
|
||||||
*actual_seq*. It is the equivalent of ``assertEqual(set(expected),
|
Example:
|
||||||
set(actual))`` but it works with sequences of unhashable objects as
|
- [0, 1, 1] and [1, 0, 1] compare equal.
|
||||||
well.
|
- [0, 0, 1] and [0, 1] compare unequal.
|
||||||
"""
|
"""
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
if sys.py3kwarning:
|
if sys.py3kwarning:
|
||||||
# Silence Py3k warning raised during the sorting
|
# Silence Py3k warning raised during the sorting
|
||||||
for _msg in ["dict inequality comparisons",
|
for _msg in ["dict inequality comparisons",
|
||||||
"builtin_function_or_method order comparisons",
|
"builtin_function_or_method order comparisons",
|
||||||
"comparing unequal types"]:
|
"comparing unequal types"]:
|
||||||
warnings.filterwarnings("ignore", _msg, DeprecationWarning)
|
warnings.filterwarnings("ignore", _msg, DeprecationWarning)
|
||||||
try:
|
try:
|
||||||
expected = set(expected_seq)
|
|
||||||
actual = set(actual_seq)
|
|
||||||
missing = sorted(expected.difference(actual))
|
|
||||||
unexpected = sorted(actual.difference(expected))
|
|
||||||
except TypeError:
|
|
||||||
# Fall back to slower list-compare if any of the objects are
|
|
||||||
# not hashable.
|
|
||||||
expected = sorted(expected_seq)
|
expected = sorted(expected_seq)
|
||||||
actual = sorted(actual_seq)
|
actual = sorted(actual_seq)
|
||||||
missing, unexpected = sorted_list_difference(expected, actual)
|
except TypeError:
|
||||||
|
# Unsortable items (example: set(), complex(), ...)
|
||||||
|
expected = list(expected_seq)
|
||||||
|
actual = list(actual_seq)
|
||||||
|
missing, unexpected = unorderable_list_difference(
|
||||||
|
expected, actual, ignore_duplicate=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.assertSequenceEqual(expected, actual, msg=msg)
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
if missing:
|
if missing:
|
||||||
errors.append('Expected, but missing:\n %s' %
|
errors.append('Expected, but missing:\n %s' %
|
||||||
safe_repr(missing))
|
safe_repr(missing))
|
||||||
if unexpected:
|
if unexpected:
|
||||||
errors.append('Unexpected, but present:\n %s' %
|
errors.append('Unexpected, but present:\n %s' %
|
||||||
safe_repr(unexpected))
|
safe_repr(unexpected))
|
||||||
if errors:
|
if errors:
|
||||||
standardMsg = '\n'.join(errors)
|
standardMsg = '\n'.join(errors)
|
||||||
self.fail(self._formatMessage(msg, standardMsg))
|
self.fail(self._formatMessage(msg, standardMsg))
|
||||||
|
|
|
@ -48,3 +48,40 @@ def sorted_list_difference(expected, actual):
|
||||||
unexpected.extend(actual[j:])
|
unexpected.extend(actual[j:])
|
||||||
break
|
break
|
||||||
return missing, unexpected
|
return missing, unexpected
|
||||||
|
|
||||||
|
|
||||||
|
def unorderable_list_difference(expected, actual, ignore_duplicate=False):
|
||||||
|
"""Same behavior as sorted_list_difference but
|
||||||
|
for lists of unorderable items (like dicts).
|
||||||
|
|
||||||
|
As it does a linear search per item (remove) it
|
||||||
|
has O(n*n) performance.
|
||||||
|
"""
|
||||||
|
missing = []
|
||||||
|
unexpected = []
|
||||||
|
while expected:
|
||||||
|
item = expected.pop()
|
||||||
|
try:
|
||||||
|
actual.remove(item)
|
||||||
|
except ValueError:
|
||||||
|
missing.append(item)
|
||||||
|
if ignore_duplicate:
|
||||||
|
for lst in expected, actual:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
lst.remove(item)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if ignore_duplicate:
|
||||||
|
while actual:
|
||||||
|
item = actual.pop()
|
||||||
|
unexpected.append(item)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
actual.remove(item)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return missing, unexpected
|
||||||
|
|
||||||
|
# anything left in actual is unexpected
|
||||||
|
return missing, actual
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue