diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-10-15 20:30:47 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-10-15 20:32:01 +0000 |
commit | e45744e7c5b9916c398fe41273194ffb671fcdac (patch) | |
tree | 620ad07a959cf23c8fef76d2967d31eb9c29e6ec /tests/units/test_device.py | |
parent | Releasing debian version 1.0.0-1. (diff) | |
download | anta-e45744e7c5b9916c398fe41273194ffb671fcdac.tar.xz anta-e45744e7c5b9916c398fe41273194ffb671fcdac.zip |
Merging upstream version 1.1.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/units/test_device.py')
-rw-r--r-- | tests/units/test_device.py | 553 |
1 files changed, 205 insertions, 348 deletions
diff --git a/tests/units/test_device.py b/tests/units/test_device.py index e8a0c5f..faf6144 100644 --- a/tests/units/test_device.py +++ b/tests/units/test_device.py @@ -10,129 +10,51 @@ from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import patch -import httpx import pytest from asyncssh import SSHClientConnection, SSHClientConnectionOptions +from httpx import ConnectError, HTTPError from rich import print as rprint -import asynceapi from anta.device import AntaDevice, AsyncEOSDevice from anta.models import AntaCommand -from tests.lib.fixture import COMMAND_OUTPUT -from tests.lib.utils import generate_test_ids_list +from asynceapi import EapiCommandError +from tests.units.conftest import COMMAND_OUTPUT if TYPE_CHECKING: from _pytest.mark.structures import ParameterSet -INIT_DATA: list[dict[str, Any]] = [ - { - "name": "no name, no port", - "device": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - }, - "expected": {"name": "42.42.42.42"}, - }, - { - "name": "no name, port", - "device": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - "port": 666, - }, - "expected": {"name": "42.42.42.42:666"}, - }, - { - "name": "name", - "device": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - "name": "test.anta.ninja", - "disable_cache": True, - }, - "expected": {"name": "test.anta.ninja"}, - }, - { - "name": "insecure", - "device": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - "name": "test.anta.ninja", - "insecure": True, - }, - "expected": {"name": "test.anta.ninja"}, - }, +INIT_PARAMS: list[ParameterSet] = [ + pytest.param({"host": "42.42.42.42", "username": "anta", "password": "anta"}, {"name": "42.42.42.42"}, id="no name, no port"), + pytest.param({"host": "42.42.42.42", "username": "anta", "password": "anta", "port": 666}, {"name": "42.42.42.42:666"}, id="no name, port"), + pytest.param( + {"host": "42.42.42.42", "username": "anta", "password": "anta", "name": "test.anta.ninja", "disable_cache": True}, {"name": "test.anta.ninja"}, id="name" + ), + pytest.param( + {"host": "42.42.42.42", "username": "anta", "password": "anta", "name": "test.anta.ninja", "insecure": True}, {"name": "test.anta.ninja"}, id="insecure" + ), ] -EQUALITY_DATA: list[dict[str, Any]] = [ - { - "name": "equal", - "device1": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - }, - "device2": { - "host": "42.42.42.42", - "username": "anta", - "password": "blah", - }, - "expected": True, - }, - { - "name": "equals-name", - "device1": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - "name": "device1", - }, - "device2": { - "host": "42.42.42.42", - "username": "plop", - "password": "anta", - "name": "device2", - }, - "expected": True, - }, - { - "name": "not-equal-port", - "device1": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - }, - "device2": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - "port": 666, - }, - "expected": False, - }, - { - "name": "not-equal-host", - "device1": { - "host": "42.42.42.41", - "username": "anta", - "password": "anta", - }, - "device2": { - "host": "42.42.42.42", - "username": "anta", - "password": "anta", - }, - "expected": False, - }, +EQUALITY_PARAMS: list[ParameterSet] = [ + pytest.param({"host": "42.42.42.42", "username": "anta", "password": "anta"}, {"host": "42.42.42.42", "username": "anta", "password": "blah"}, True, id="equal"), + pytest.param( + {"host": "42.42.42.42", "username": "anta", "password": "anta", "name": "device1"}, + {"host": "42.42.42.42", "username": "plop", "password": "anta", "name": "device2"}, + True, + id="equals-name", + ), + pytest.param( + {"host": "42.42.42.42", "username": "anta", "password": "anta"}, + {"host": "42.42.42.42", "username": "anta", "password": "anta", "port": 666}, + False, + id="not-equal-port", + ), + pytest.param( + {"host": "42.42.42.41", "username": "anta", "password": "anta"}, {"host": "42.42.42.42", "username": "anta", "password": "anta"}, False, id="not-equal-host" + ), ] -ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ - { - "name": "command", - "device": {}, - "command": { +ASYNCEAPI_COLLECT_PARAMS: list[ParameterSet] = [ + pytest.param( + {}, + { "command": "show version", "patch_kwargs": { "return_value": [ @@ -155,11 +77,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ "memTotal": 8099732, "memFree": 4989568, "isIntlVersion": False, - }, - ], + } + ] }, }, - "expected": { + { "output": { "mfgName": "Arista", "modelName": "DCS-7280CR3-32P4-F", @@ -182,11 +104,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ }, "errors": [], }, - }, - { - "name": "enable", - "device": {"enable": True}, - "command": { + id="command", + ), + pytest.param( + {"enable": True}, + { "command": "show version", "patch_kwargs": { "return_value": [ @@ -211,10 +133,10 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ "memFree": 4989568, "isIntlVersion": False, }, - ], + ] }, }, - "expected": { + { "output": { "mfgName": "Arista", "modelName": "DCS-7280CR3-32P4-F", @@ -237,11 +159,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ }, "errors": [], }, - }, - { - "name": "enable password", - "device": {"enable": True, "enable_password": "anta"}, - "command": { + id="enable", + ), + pytest.param( + {"enable": True, "enable_password": "anta"}, + { "command": "show version", "patch_kwargs": { "return_value": [ @@ -266,10 +188,10 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ "memFree": 4989568, "isIntlVersion": False, }, - ], + ] }, }, - "expected": { + { "output": { "mfgName": "Arista", "modelName": "DCS-7280CR3-32P4-F", @@ -292,11 +214,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ }, "errors": [], }, - }, - { - "name": "revision", - "device": {}, - "command": { + id="enable password", + ), + pytest.param( + {}, + { "command": "show version", "revision": 3, "patch_kwargs": { @@ -322,10 +244,10 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ "memFree": 4989568, "isIntlVersion": False, }, - ], + ] }, }, - "expected": { + { "output": { "mfgName": "Arista", "modelName": "DCS-7280CR3-32P4-F", @@ -348,77 +270,47 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [ }, "errors": [], }, - }, - { - "name": "asynceapi.EapiCommandError", - "device": {}, - "command": { + id="revision", + ), + pytest.param( + {}, + { "command": "show version", "patch_kwargs": { - "side_effect": asynceapi.EapiCommandError( + "side_effect": EapiCommandError( passed=[], failed="show version", errors=["Authorization denied for command 'show version'"], errmsg="Invalid command", not_exec=[], - ), + ) }, }, - "expected": {"output": None, "errors": ["Authorization denied for command 'show version'"]}, - }, - { - "name": "httpx.HTTPError", - "device": {}, - "command": { - "command": "show version", - "patch_kwargs": {"side_effect": httpx.HTTPError(message="404")}, - }, - "expected": {"output": None, "errors": ["HTTPError: 404"]}, - }, - { - "name": "httpx.ConnectError", - "device": {}, - "command": { - "command": "show version", - "patch_kwargs": {"side_effect": httpx.ConnectError(message="Cannot open port")}, - }, - "expected": {"output": None, "errors": ["ConnectError: Cannot open port"]}, - }, + {"output": None, "errors": ["Authorization denied for command 'show version'"]}, + id="asynceapi.EapiCommandError", + ), + pytest.param( + {}, + {"command": "show version", "patch_kwargs": {"side_effect": HTTPError("404")}}, + {"output": None, "errors": ["HTTPError: 404"]}, + id="httpx.HTTPError", + ), + pytest.param( + {}, + {"command": "show version", "patch_kwargs": {"side_effect": ConnectError("Cannot open port")}}, + {"output": None, "errors": ["ConnectError: Cannot open port"]}, + id="httpx.ConnectError", + ), ] -ASYNCEAPI_COPY_DATA: list[dict[str, Any]] = [ - { - "name": "from", - "device": {}, - "copy": { - "sources": [Path("/mnt/flash"), Path("/var/log/agents")], - "destination": Path(), - "direction": "from", - }, - }, - { - "name": "to", - "device": {}, - "copy": { - "sources": [Path("/mnt/flash"), Path("/var/log/agents")], - "destination": Path(), - "direction": "to", - }, - }, - { - "name": "wrong", - "device": {}, - "copy": { - "sources": [Path("/mnt/flash"), Path("/var/log/agents")], - "destination": Path(), - "direction": "wrong", - }, - }, +ASYNCEAPI_COPY_PARAMS: list[ParameterSet] = [ + pytest.param({}, {"sources": [Path("/mnt/flash"), Path("/var/log/agents")], "destination": Path(), "direction": "from"}, id="from"), + pytest.param({}, {"sources": [Path("/mnt/flash"), Path("/var/log/agents")], "destination": Path(), "direction": "to"}, id="to"), + pytest.param({}, {"sources": [Path("/mnt/flash"), Path("/var/log/agents")], "destination": Path(), "direction": "wrong"}, id="wrong"), ] -REFRESH_DATA: list[dict[str, Any]] = [ - { - "name": "established", - "device": {}, - "patch_kwargs": ( +REFRESH_PARAMS: list[ParameterSet] = [ + pytest.param( + {}, + ( {"return_value": True}, { "return_value": [ @@ -442,15 +334,15 @@ REFRESH_DATA: list[dict[str, Any]] = [ "memFree": 4989568, "isIntlVersion": False, } - ], + ] }, ), - "expected": {"is_online": True, "established": True, "hw_model": "DCS-7280CR3-32P4-F"}, - }, - { - "name": "is not online", - "device": {}, - "patch_kwargs": ( + {"is_online": True, "established": True, "hw_model": "DCS-7280CR3-32P4-F"}, + id="established", + ), + pytest.param( + {}, + ( {"return_value": False}, { "return_value": { @@ -472,15 +364,15 @@ REFRESH_DATA: list[dict[str, Any]] = [ "memTotal": 8099732, "memFree": 4989568, "isIntlVersion": False, - }, + } }, ), - "expected": {"is_online": False, "established": False, "hw_model": None}, - }, - { - "name": "cannot parse command", - "device": {}, - "patch_kwargs": ( + {"is_online": False, "established": False, "hw_model": None}, + id="is not online", + ), + pytest.param( + {}, + ( {"return_value": True}, { "return_value": [ @@ -503,108 +395,87 @@ REFRESH_DATA: list[dict[str, Any]] = [ "memFree": 4989568, "isIntlVersion": False, } - ], + ] }, ), - "expected": {"is_online": True, "established": False, "hw_model": None}, - }, - { - "name": "asynceapi.EapiCommandError", - "device": {}, - "patch_kwargs": ( + {"is_online": True, "established": False, "hw_model": None}, + id="cannot parse command", + ), + pytest.param( + {}, + ( {"return_value": True}, { - "side_effect": asynceapi.EapiCommandError( + "side_effect": EapiCommandError( passed=[], failed="show version", errors=["Authorization denied for command 'show version'"], errmsg="Invalid command", not_exec=[], - ), + ) }, ), - "expected": {"is_online": True, "established": False, "hw_model": None}, - }, - { - "name": "httpx.HTTPError", - "device": {}, - "patch_kwargs": ( - {"return_value": True}, - {"side_effect": httpx.HTTPError(message="404")}, - ), - "expected": {"is_online": True, "established": False, "hw_model": None}, - }, - { - "name": "httpx.ConnectError", - "device": {}, - "patch_kwargs": ( + {"is_online": True, "established": False, "hw_model": None}, + id="asynceapi.EapiCommandError", + ), + pytest.param( + {}, + ({"return_value": True}, {"side_effect": HTTPError("404")}), + {"is_online": True, "established": False, "hw_model": None}, + id="httpx.HTTPError", + ), + pytest.param( + {}, + ({"return_value": True}, {"side_effect": ConnectError("Cannot open port")}), + {"is_online": True, "established": False, "hw_model": None}, + id="httpx.ConnectError", + ), + pytest.param( + {}, + ( {"return_value": True}, - {"side_effect": httpx.ConnectError(message="Cannot open port")}, + { + "return_value": [ + { + "mfgName": "Arista", + "modelName": "", + } + ] + }, ), - "expected": {"is_online": True, "established": False, "hw_model": None}, - }, + {"is_online": True, "established": False, "hw_model": ""}, + id="modelName empty string", + ), ] -COLLECT_DATA: list[dict[str, Any]] = [ - { - "name": "device cache enabled, command cache enabled, no cache hit", - "device": {"disable_cache": False}, - "command": { - "command": "show version", - "use_cache": True, - }, - "expected": {"cache_hit": False}, - }, - { - "name": "device cache enabled, command cache enabled, cache hit", - "device": {"disable_cache": False}, - "command": { - "command": "show version", - "use_cache": True, - }, - "expected": {"cache_hit": True}, - }, - { - "name": "device cache disabled, command cache enabled", - "device": {"disable_cache": True}, - "command": { - "command": "show version", - "use_cache": True, - }, - "expected": {}, - }, - { - "name": "device cache enabled, command cache disabled, cache has command", - "device": {"disable_cache": False}, - "command": { - "command": "show version", - "use_cache": False, - }, - "expected": {"cache_hit": True}, - }, - { - "name": "device cache enabled, command cache disabled, cache does not have data", - "device": { - "disable_cache": False, - }, - "command": { - "command": "show version", - "use_cache": False, - }, - "expected": {"cache_hit": False}, - }, - { - "name": "device cache disabled, command cache disabled", - "device": { - "disable_cache": True, - }, - "command": { - "command": "show version", - "use_cache": False, - }, - "expected": {}, - }, +COLLECT_PARAMS: list[ParameterSet] = [ + pytest.param( + {"disable_cache": False}, + {"command": "show version", "use_cache": True}, + {"cache_hit": False}, + id="device cache enabled, command cache enabled, no cache hit", + ), + pytest.param( + {"disable_cache": False}, + {"command": "show version", "use_cache": True}, + {"cache_hit": True}, + id="device cache enabled, command cache enabled, cache hit", + ), + pytest.param({"disable_cache": True}, {"command": "show version", "use_cache": True}, {}, id="device cache disabled, command cache enabled"), + pytest.param( + {"disable_cache": False}, + {"command": "show version", "use_cache": False}, + {"cache_hit": True}, + id="device cache enabled, command cache disabled, cache has command", + ), + pytest.param( + {"disable_cache": False}, + {"command": "show version", "use_cache": False}, + {"cache_hit": False}, + id="device cache enabled, command cache disabled, cache does not have data", + ), + pytest.param({"disable_cache": True}, {"command": "show version", "use_cache": False}, {}, id="device cache disabled, command cache disabled"), ] -CACHE_STATS_DATA: list[ParameterSet] = [ +CACHE_STATS_PARAMS: list[ParameterSet] = [ pytest.param({"disable_cache": False}, {"total_commands_sent": 0, "cache_hits": 0, "cache_hit_ratio": "0.00%"}, id="with_cache"), pytest.param({"disable_cache": True}, None, id="without_cache"), ] @@ -613,48 +484,42 @@ CACHE_STATS_DATA: list[ParameterSet] = [ class TestAntaDevice: """Test for anta.device.AntaDevice Abstract class.""" - @pytest.mark.asyncio() - @pytest.mark.parametrize( - ("device", "command_data", "expected_data"), - ((d["device"], d["command"], d["expected"]) for d in COLLECT_DATA), - indirect=["device"], - ids=generate_test_ids_list(COLLECT_DATA), - ) - async def test_collect(self, device: AntaDevice, command_data: dict[str, Any], expected_data: dict[str, Any]) -> None: + @pytest.mark.parametrize(("device", "command", "expected"), COLLECT_PARAMS, indirect=["device"]) + async def test_collect(self, device: AntaDevice, command: dict[str, Any], expected: dict[str, Any]) -> None: """Test AntaDevice.collect behavior.""" - command = AntaCommand(command=command_data["command"], use_cache=command_data["use_cache"]) + cmd = AntaCommand(command=command["command"], use_cache=command["use_cache"]) # Dummy output for cache hit cached_output = "cached_value" - if device.cache is not None and expected_data["cache_hit"] is True: - await device.cache.set(command.uid, cached_output) + if device.cache is not None and expected["cache_hit"] is True: + await device.cache.set(cmd.uid, cached_output) - await device.collect(command) + await device.collect(cmd) if device.cache is not None: # device_cache is enabled - current_cached_data = await device.cache.get(command.uid) - if command.use_cache is True: # command is allowed to use cache - if expected_data["cache_hit"] is True: - assert command.output == cached_output + current_cached_data = await device.cache.get(cmd.uid) + if cmd.use_cache is True: # command is allowed to use cache + if expected["cache_hit"] is True: + assert cmd.output == cached_output assert current_cached_data == cached_output assert device.cache.hit_miss_ratio["hits"] == 2 else: - assert command.output == COMMAND_OUTPUT + assert cmd.output == COMMAND_OUTPUT assert current_cached_data == COMMAND_OUTPUT assert device.cache.hit_miss_ratio["hits"] == 1 else: # command is not allowed to use cache - device._collect.assert_called_once_with(command=command, collection_id=None) # type: ignore[attr-defined] # pylint: disable=protected-access - assert command.output == COMMAND_OUTPUT - if expected_data["cache_hit"] is True: + device._collect.assert_called_once_with(command=cmd, collection_id=None) # type: ignore[attr-defined] + assert cmd.output == COMMAND_OUTPUT + if expected["cache_hit"] is True: assert current_cached_data == cached_output else: assert current_cached_data is None else: # device is disabled assert device.cache is None - device._collect.assert_called_once_with(command=command, collection_id=None) # type: ignore[attr-defined] # pylint: disable=protected-access + device._collect.assert_called_once_with(command=cmd, collection_id=None) # type: ignore[attr-defined] - @pytest.mark.parametrize(("device", "expected"), CACHE_STATS_DATA, indirect=["device"]) + @pytest.mark.parametrize(("device", "expected"), CACHE_STATS_PARAMS, indirect=["device"]) def test_cache_statistics(self, device: AntaDevice, expected: dict[str, Any] | None) -> None: """Verify that when cache statistics attribute does not exist. @@ -666,42 +531,39 @@ class TestAntaDevice: class TestAsyncEOSDevice: """Test for anta.device.AsyncEOSDevice.""" - @pytest.mark.parametrize("data", INIT_DATA, ids=generate_test_ids_list(INIT_DATA)) - def test__init__(self, data: dict[str, Any]) -> None: + @pytest.mark.parametrize(("device", "expected"), INIT_PARAMS) + def test__init__(self, device: dict[str, Any], expected: dict[str, Any]) -> None: """Test the AsyncEOSDevice constructor.""" - device = AsyncEOSDevice(**data["device"]) + dev = AsyncEOSDevice(**device) - assert device.name == data["expected"]["name"] - if data["device"].get("disable_cache") is True: - assert device.cache is None - assert device.cache_locks is None + assert dev.name == expected["name"] + if device.get("disable_cache") is True: + assert dev.cache is None + assert dev.cache_locks is None else: # False or None - assert device.cache is not None - assert device.cache_locks is not None - hash(device) + assert dev.cache is not None + assert dev.cache_locks is not None + hash(dev) with patch("anta.device.__DEBUG__", new=True): - rprint(device) + rprint(dev) - @pytest.mark.parametrize("data", EQUALITY_DATA, ids=generate_test_ids_list(EQUALITY_DATA)) - def test__eq(self, data: dict[str, Any]) -> None: + @pytest.mark.parametrize(("device1", "device2", "expected"), EQUALITY_PARAMS) + def test__eq(self, device1: dict[str, Any], device2: dict[str, Any], expected: bool) -> None: """Test the AsyncEOSDevice equality.""" - device1 = AsyncEOSDevice(**data["device1"]) - device2 = AsyncEOSDevice(**data["device2"]) - if data["expected"]: - assert device1 == device2 + dev1 = AsyncEOSDevice(**device1) + dev2 = AsyncEOSDevice(**device2) + if expected: + assert dev1 == dev2 else: - assert device1 != device2 + assert dev1 != dev2 - @pytest.mark.asyncio() @pytest.mark.parametrize( ("async_device", "patch_kwargs", "expected"), - ((d["device"], d["patch_kwargs"], d["expected"]) for d in REFRESH_DATA), - ids=generate_test_ids_list(REFRESH_DATA), + REFRESH_PARAMS, indirect=["async_device"], ) async def test_refresh(self, async_device: AsyncEOSDevice, patch_kwargs: list[dict[str, Any]], expected: dict[str, Any]) -> None: - # pylint: disable=protected-access """Test AsyncEOSDevice.refresh().""" with patch.object(async_device._session, "check_connection", **patch_kwargs[0]), patch.object(async_device._session, "cli", **patch_kwargs[1]): await async_device.refresh() @@ -712,15 +574,12 @@ class TestAsyncEOSDevice: assert async_device.established == expected["established"] assert async_device.hw_model == expected["hw_model"] - @pytest.mark.asyncio() @pytest.mark.parametrize( ("async_device", "command", "expected"), - ((d["device"], d["command"], d["expected"]) for d in ASYNCEAPI_COLLECT_DATA), - ids=generate_test_ids_list(ASYNCEAPI_COLLECT_DATA), + ASYNCEAPI_COLLECT_PARAMS, indirect=["async_device"], ) async def test__collect(self, async_device: AsyncEOSDevice, command: dict[str, Any], expected: dict[str, Any]) -> None: - # pylint: disable=protected-access """Test AsyncEOSDevice._collect().""" cmd = AntaCommand(command=command["command"], revision=command["revision"]) if "revision" in command else AntaCommand(command=command["command"]) with patch.object(async_device._session, "cli", **command["patch_kwargs"]): @@ -741,15 +600,13 @@ class TestAsyncEOSDevice: commands.append({"cmd": cmd.command, "revision": cmd.revision}) else: commands.append({"cmd": cmd.command}) - async_device._session.cli.assert_called_once_with(commands=commands, ofmt=cmd.ofmt, version=cmd.version, req_id=f"ANTA-{collection_id}-{id(cmd)}") # type: ignore[attr-defined] # asynceapi.Device.cli is patched # pylint: disable=line-too-long + async_device._session.cli.assert_called_once_with(commands=commands, ofmt=cmd.ofmt, version=cmd.version, req_id=f"ANTA-{collection_id}-{id(cmd)}") # type: ignore[attr-defined] # asynceapi.Device.cli is patched assert cmd.output == expected["output"] assert cmd.errors == expected["errors"] - @pytest.mark.asyncio() @pytest.mark.parametrize( ("async_device", "copy"), - ((d["device"], d["copy"]) for d in ASYNCEAPI_COPY_DATA), - ids=generate_test_ids_list(ASYNCEAPI_COPY_DATA), + ASYNCEAPI_COPY_PARAMS, indirect=["async_device"], ) async def test_copy(self, async_device: AsyncEOSDevice, copy: dict[str, Any]) -> None: |