# -*- coding: utf-8 -*- from xml.dom import minidom import py import sys import os from _pytest.junitxml import LogXML import pytest def runandparse(testdir, *args): resultpath = testdir.tmpdir.join("junit.xml") result = testdir.runpytest("--junitxml=%s" % resultpath, *args) xmldoc = minidom.parse(str(resultpath)) return result, DomNode(xmldoc) def assert_attr(node, **kwargs): __tracebackhide__ = True def nodeval(node, name): anode = node.getAttributeNode(name) if anode is not None: return anode.value expected = dict((name, str(value)) for name, value in kwargs.items()) on_node = dict((name, nodeval(node, name)) for name in expected) assert on_node == expected class DomNode(object): def __init__(self, dom): self.__node = dom def __repr__(self): return self.__node.toxml() def find_first_by_tag(self, tag): return self.find_nth_by_tag(tag, 0) def _by_tag(self, tag): return self.__node.getElementsByTagName(tag) def find_nth_by_tag(self, tag, n): items = self._by_tag(tag) try: nth = items[n] except IndexError: pass else: return type(self)(nth) def find_by_tag(self, tag): t = type(self) return [t(x) for x in self.__node.getElementsByTagName(tag)] def __getitem__(self, key): node = self.__node.getAttributeNode(key) if node is not None: return node.value def assert_attr(self, **kwargs): __tracebackhide__ = True return assert_attr(self.__node, **kwargs) def toxml(self): return self.__node.toxml() @property def text(self): return self.__node.childNodes[0].wholeText @property def tag(self): return self.__node.tagName @property def next_siebling(self): return type(self)(self.__node.nextSibling) class TestPython: def test_summing_simple(self, testdir): testdir.makepyfile(""" import pytest def test_pass(): pass def test_fail(): assert 0 def test_skip(): pytest.skip("") @pytest.mark.xfail def test_xfail(): assert 0 @pytest.mark.xfail def test_xpass(): assert 1 """) result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=0, failures=1, skips=2, tests=5) def test_summing_simple_with_errors(self, testdir): testdir.makepyfile(""" import pytest @pytest.fixture def fixture(): raise Exception() def test_pass(): pass def test_fail(): assert 0 def test_error(fixture): pass @pytest.mark.xfail def test_xfail(): assert False @pytest.mark.xfail(strict=True) def test_xpass(): assert True """) result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=1, failures=2, skips=1, tests=5) def test_timing_function(self, testdir): testdir.makepyfile(""" import time, pytest def setup_module(): time.sleep(0.01) def teardown_module(): time.sleep(0.01) def test_sleep(): time.sleep(0.01) """) result, dom = runandparse(testdir) node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") val = tnode["time"] assert round(float(val), 2) >= 0.03 def test_setup_error(self, testdir): testdir.makepyfile(""" import pytest @pytest.fixture def arg(request): raise ValueError() def test_function(arg): pass """) result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_setup_error.py", line="5", classname="test_setup_error", name="test_function") fnode = tnode.find_first_by_tag("error") fnode.assert_attr(message="test setup failure") assert "ValueError" in fnode.toxml() def test_skip_contains_name_reason(self, testdir): testdir.makepyfile(""" import pytest def test_skip(): pytest.skip("hello23") """) result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skips=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_skip_contains_name_reason.py", line="1", classname="test_skip_contains_name_reason", name="test_skip") snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello23", ) def test_mark_skip_contains_name_reason(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.skip(reason="hello24") def test_skip(): assert True """) result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skips=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_mark_skip_contains_name_reason.py", line="1", classname="test_mark_skip_contains_name_reason", name="test_skip") snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello24", ) def test_mark_skipif_contains_name_reason(self, testdir): testdir.makepyfile(""" import pytest GLOBAL_CONDITION = True @pytest.mark.skipif(GLOBAL_CONDITION, reason="hello25") def test_skip(): assert True """) result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skips=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_mark_skipif_contains_name_reason.py", line="2", classname="test_mark_skipif_contains_name_reason", name="test_skip") snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello25", ) def test_classname_instance(self, testdir): testdir.makepyfile(""" class TestClass: def test_method(self): assert 0 """) result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_classname_instance.py", line="1", classname="test_classname_instance.TestClass", name="test_method") def test_classname_nested_dir(self, testdir): p = testdir.tmpdir.ensure("sub", "test_hello.py") p.write("def test_func(): 0/0") result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file=os.path.join("sub", "test_hello.py"), line="0", classname="sub.test_hello", name="test_func") def test_internal_error(self, testdir): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makepyfile("def test_function(): pass") result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="pytest", name="internal") fnode = tnode.find_first_by_tag("error") fnode.assert_attr(message="internal error") assert "Division" in fnode.toxml() def test_failure_function(self, testdir): testdir.makepyfile(""" import sys def test_fail(): print ("hello-stdout") sys.stderr.write("hello-stderr\\n") raise ValueError(42) """) result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_failure_function.py", line="1", classname="test_failure_function", name="test_fail") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="ValueError: 42") assert "ValueError" in fnode.toxml() systemout = fnode.next_siebling assert systemout.tag == "system-out" assert "hello-stdout" in systemout.toxml() systemerr = systemout.next_siebling assert systemerr.tag == "system-err" assert "hello-stderr" in systemerr.toxml() def test_failure_verbose_message(self, testdir): testdir.makepyfile(""" import sys def test_fail(): assert 0, "An error" """) result, dom = runandparse(testdir) node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="AssertionError: An error assert 0") def test_failure_escape(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.parametrize('arg1', "<&'", ids="<&'") def test_func(arg1): print(arg1) assert 0 """) result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=3, tests=3) for index, char in enumerate("<&'"): tnode = node.find_nth_by_tag("testcase", index) tnode.assert_attr( file="test_failure_escape.py", line="1", classname="test_failure_escape", name="test_func[%s]" % char) sysout = tnode.find_first_by_tag('system-out') text = sysout.text assert text == '%s\n' % char def test_junit_prefixing(self, testdir): testdir.makepyfile(""" def test_func(): assert 0 class TestHello: def test_hello(self): pass """) result, dom = runandparse(testdir, "--junitprefix=xyz") assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=2) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_junit_prefixing.py", line="0", classname="xyz.test_junit_prefixing", name="test_func") tnode = node.find_nth_by_tag("testcase", 1) tnode.assert_attr( file="test_junit_prefixing.py", line="3", classname="xyz.test_junit_prefixing." "TestHello", name="test_hello") def test_xfailure_function(self, testdir): testdir.makepyfile(""" import pytest def test_xfail(): pytest.xfail("42") """) result, dom = runandparse(testdir) assert not result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skips=1, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_xfailure_function.py", line="1", classname="test_xfailure_function", name="test_xfail") fnode = tnode.find_first_by_tag("skipped") fnode.assert_attr(message="expected test failure") # assert "ValueError" in fnode.toxml() def test_xfailure_xpass(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.xfail def test_xpass(): pass """) result, dom = runandparse(testdir) # assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skips=0, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_xfailure_xpass.py", line="1", classname="test_xfailure_xpass", name="test_xpass") def test_xfailure_xpass_strict(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.xfail(strict=True, reason="This needs to fail!") def test_xpass(): pass """) result, dom = runandparse(testdir) # assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skips=0, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_xfailure_xpass_strict.py", line="1", classname="test_xfailure_xpass_strict", name="test_xpass") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") def test_collect_error(self, testdir): testdir.makepyfile("syntax error") result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_collect_error.py", name="test_collect_error") assert tnode["line"] is None fnode = tnode.find_first_by_tag("error") fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() def test_unicode(self, testdir): value = 'hx\xc4\x85\xc4\x87\n' testdir.makepyfile(""" # coding: latin1 def test_hello(): print (%r) assert 0 """ % value) result, dom = runandparse(testdir) assert result.ret == 1 tnode = dom.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") if not sys.platform.startswith("java"): assert "hx" in fnode.toxml() def test_assertion_binchars(self, testdir): """this test did fail when the escaping wasnt strict""" testdir.makepyfile(""" M1 = '\x01\x02\x03\x04' M2 = '\x01\x02\x03\x05' def test_str_compare(): assert M1 == M2 """) result, dom = runandparse(testdir) print(dom.toxml()) def test_pass_captures_stdout(self, testdir): testdir.makepyfile(""" def test_pass(): print('hello-stdout') """) result, dom = runandparse(testdir) node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml() def test_pass_captures_stderr(self, testdir): testdir.makepyfile(""" import sys def test_pass(): sys.stderr.write('hello-stderr') """) result, dom = runandparse(testdir) node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() def test_setup_error_captures_stdout(self, testdir): testdir.makepyfile(""" import pytest @pytest.fixture def arg(request): print('hello-stdout') raise ValueError() def test_function(arg): pass """) result, dom = runandparse(testdir) node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml() def test_setup_error_captures_stderr(self, testdir): testdir.makepyfile(""" import sys import pytest @pytest.fixture def arg(request): sys.stderr.write('hello-stderr') raise ValueError() def test_function(arg): pass """) result, dom = runandparse(testdir) node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() def test_mangle_test_address(): from _pytest.junitxml import mangle_test_address address = '::'.join( ["a/my.py.thing.py", "Class", "()", "method", "[a-1-::]"]) newnames = mangle_test_address(address) assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"] def test_dont_configure_on_slaves(tmpdir): gotten = [] class FakeConfig: def __init__(self): self.pluginmanager = self self.option = self junitprefix = None # XXX: shouldnt need tmpdir ? xmlpath = str(tmpdir.join('junix.xml')) register = gotten.append fake_config = FakeConfig() from _pytest import junitxml junitxml.pytest_configure(fake_config) assert len(gotten) == 1 FakeConfig.slaveinput = None junitxml.pytest_configure(fake_config) assert len(gotten) == 1 class TestNonPython: def test_summing_simple(self, testdir): testdir.makeconftest(""" import pytest def pytest_collect_file(path, parent): if path.ext == ".xyz": return MyItem(path, parent) class MyItem(pytest.Item): def __init__(self, path, parent): super(MyItem, self).__init__(path.basename, parent) self.fspath = path def runtest(self): raise ValueError(42) def repr_failure(self, excinfo): return "custom item runtest failed" """) testdir.tmpdir.join("myfile.xyz").write("hello") result, dom = runandparse(testdir) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=0, failures=1, skips=0, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(name="myfile.xyz") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="custom item runtest failed") assert "custom item runtest failed" in fnode.toxml() def test_nullbyte(testdir): # A null byte can not occur in XML (see section 2.2 of the spec) testdir.makepyfile(""" import sys def test_print_nullbyte(): sys.stdout.write('Here the null -->' + chr(0) + '<--') sys.stdout.write('In repr form -->' + repr(chr(0)) + '<--') assert False """) xmlf = testdir.tmpdir.join('junit.xml') testdir.runpytest('--junitxml=%s' % xmlf) text = xmlf.read() assert '\x00' not in text assert '#x00' in text def test_nullbyte_replace(testdir): # Check if the null byte gets replaced testdir.makepyfile(""" import sys def test_print_nullbyte(): sys.stdout.write('Here the null -->' + chr(0) + '<--') sys.stdout.write('In repr form -->' + repr(chr(0)) + '<--') assert False """) xmlf = testdir.tmpdir.join('junit.xml') testdir.runpytest('--junitxml=%s' % xmlf) text = xmlf.read() assert '#x0' in text def test_invalid_xml_escape(): # Test some more invalid xml chars, the full range should be # tested really but let's just thest the edges of the ranges # intead. # XXX This only tests low unicode character points for now as # there are some issues with the testing infrastructure for # the higher ones. # XXX Testing 0xD (\r) is tricky as it overwrites the just written # line in the output, so we skip it too. global unichr try: unichr(65) except NameError: unichr = chr invalid = (0x00, 0x1, 0xB, 0xC, 0xE, 0x19, 27, # issue #126 0xD800, 0xDFFF, 0xFFFE, 0x0FFFF) # , 0x110000) valid = (0x9, 0xA, 0x20, ) # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF) from _pytest.junitxml import bin_xml_escape for i in invalid: got = bin_xml_escape(unichr(i)).uniobj if i <= 0xFF: expected = '#x%02X' % i else: expected = '#x%04X' % i assert got == expected for i in valid: assert chr(i) == bin_xml_escape(unichr(i)).uniobj def test_logxml_path_expansion(tmpdir, monkeypatch): home_tilde = py.path.local(os.path.expanduser('~')).join('test.xml') xml_tilde = LogXML('~%stest.xml' % tmpdir.sep, None) assert xml_tilde.logfile == home_tilde # this is here for when $HOME is not set correct monkeypatch.setenv("HOME", tmpdir) home_var = os.path.normpath(os.path.expandvars('$HOME/test.xml')) xml_var = LogXML('$HOME%stest.xml' % tmpdir.sep, None) assert xml_var.logfile == home_var def test_logxml_changingdir(testdir): testdir.makepyfile(""" def test_func(): import os os.chdir("a") """) testdir.tmpdir.mkdir("a") result = testdir.runpytest("--junitxml=a/x.xml") assert result.ret == 0 assert testdir.tmpdir.join("a/x.xml").check() def test_logxml_makedir(testdir): """--junitxml should automatically create directories for the xml file""" testdir.makepyfile(""" def test_pass(): pass """) result = testdir.runpytest("--junitxml=path/to/results.xml") assert result.ret == 0 assert testdir.tmpdir.join("path/to/results.xml").check() def test_escaped_parametrized_names_xml(testdir): testdir.makepyfile(""" import pytest @pytest.mark.parametrize('char', [u"\\x00"]) def test_func(char): assert char """) result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(name="test_func[\\x00]") def test_double_colon_split_function_issue469(testdir): testdir.makepyfile(""" import pytest @pytest.mark.parametrize('param', ["double::colon"]) def test_func(param): pass """) result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_function_issue469") node.assert_attr(name='test_func[double::colon]') def test_double_colon_split_method_issue469(testdir): testdir.makepyfile(""" import pytest class TestClass: @pytest.mark.parametrize('param', ["double::colon"]) def test_func(self, param): pass """) result, dom = runandparse(testdir) assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr( classname="test_double_colon_split_method_issue469.TestClass") node.assert_attr(name='test_func[double::colon]') def test_unicode_issue368(testdir): path = testdir.tmpdir.join("test.xml") log = LogXML(str(path), None) ustr = py.builtin._totext("ВНИ!", "utf-8") from _pytest.runner import BaseReport class Report(BaseReport): longrepr = ustr sections = [] nodeid = "something" location = 'tests/filename.py', 42, 'TestClass.method' test_report = Report() # hopefully this is not too brittle ... log.pytest_sessionstart() node_reporter = log._opentestcase(test_report) node_reporter.append_failure(test_report) node_reporter.append_collect_error(test_report) node_reporter.append_collect_skipped(test_report) node_reporter.append_error(test_report) test_report.longrepr = "filename", 1, ustr node_reporter.append_skipped(test_report) test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣" node_reporter.append_skipped(test_report) test_report.wasxfail = ustr node_reporter.append_skipped(test_report) log.pytest_sessionfinish() def test_record_property(testdir): testdir.makepyfile(""" import pytest @pytest.fixture def other(record_xml_property): record_xml_property("bar", 1) def test_record(record_xml_property, other): record_xml_property("foo", "<1"); """) result, dom = runandparse(testdir, '-rw') node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag('properties') pnodes = psnode.find_by_tag('property') pnodes[0].assert_attr(name="bar", value="1") pnodes[1].assert_attr(name="foo", value="<1") result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*') def test_record_property_same_name(testdir): testdir.makepyfile(""" def test_record_with_same_name(record_xml_property): record_xml_property("foo", "bar") record_xml_property("foo", "baz") """) result, dom = runandparse(testdir, '-rw') node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag('properties') pnodes = psnode.find_by_tag('property') pnodes[0].assert_attr(name="foo", value="bar") pnodes[1].assert_attr(name="foo", value="baz") def test_random_report_log_xdist(testdir): """xdist calls pytest_runtest_logreport as they are executed by the slaves, with nodes from several nodes overlapping, so junitxml must cope with that to produce correct reports. #1064 """ pytest.importorskip('xdist') testdir.makepyfile(""" import pytest, time @pytest.mark.parametrize('i', list(range(30))) def test_x(i): assert i != 22 """) _, dom = runandparse(testdir, '-n2') suite_node = dom.find_first_by_tag("testsuite") failed = [] for case_node in suite_node.find_by_tag("testcase"): if case_node.find_first_by_tag('failure'): failed.append(case_node['name']) assert failed == ['test_x[22]'] def test_runs_twice(testdir): f = testdir.makepyfile(''' def test_pass(): pass ''') result, dom = runandparse(testdir, f, f) assert 'INTERNALERROR' not in result.stdout.str() first, second = [x['classname'] for x in dom.find_by_tag("testcase")] assert first == second @pytest.mark.xfail(reason='hangs', run=False) def test_runs_twice_xdist(testdir): pytest.importorskip('xdist') f = testdir.makepyfile(''' def test_pass(): pass ''') result, dom = runandparse( testdir, f, '--dist', 'each', '--tx', '2*popen',) assert 'INTERNALERROR' not in result.stdout.str() first, second = [x['classname'] for x in dom.find_by_tag("testcase")] assert first == second def test_fancy_items_regression(testdir): # issue 1259 testdir.makeconftest(""" import pytest class FunItem(pytest.Item): def runtest(self): pass class NoFunItem(pytest.Item): def runtest(self): pass class FunCollector(pytest.File): def collect(self): return [ FunItem('a', self), NoFunItem('a', self), NoFunItem('b', self), ] def pytest_collect_file(path, parent): if path.check(ext='.py'): return FunCollector(path, parent) """) testdir.makepyfile(''' def test_pass(): pass ''') result, dom = runandparse(testdir) assert 'INTERNALERROR' not in result.stdout.str() items = sorted( '%(classname)s %(name)s %(file)s' % x for x in dom.find_by_tag("testcase")) import pprint pprint.pprint(items) assert items == [ u'conftest a conftest.py', u'conftest a conftest.py', u'conftest b conftest.py', u'test_fancy_items_regression a test_fancy_items_regression.py', u'test_fancy_items_regression a test_fancy_items_regression.py', u'test_fancy_items_regression b test_fancy_items_regression.py', u'test_fancy_items_regression test_pass' u' test_fancy_items_regression.py', ] def test_global_properties(testdir): path = testdir.tmpdir.join("test_global_properties.xml") log = LogXML(str(path), None) from _pytest.runner import BaseReport class Report(BaseReport): sections = [] nodeid = "test_node_id" log.pytest_sessionstart() log.add_global_property('foo', 1) log.add_global_property('bar', 2) log.pytest_sessionfinish() dom = minidom.parse(str(path)) properties = dom.getElementsByTagName('properties') assert (properties.length == 1), "There must be one node" property_list = dom.getElementsByTagName('property') assert (property_list.length == 2), "There most be only 2 property nodes" expected = {'foo': '1', 'bar': '2'} actual = {} for p in property_list: k = str(p.getAttribute('name')) v = str(p.getAttribute('value')) actual[k] = v assert actual == expected