From 4bf9c4033fa6e960fb3a6596b597bb439c2cc208 Mon Sep 17 00:00:00 2001 From: James Williams Date: Tue, 21 Apr 2026 14:43:18 -0500 Subject: [PATCH 1/4] Add ASA pre-transfer free-space check (NAPPS-1087) Implements the fail-open pre-transfer free-space check on Cisco ASA using the seam added in NAPPS-1091 (PR #370). Image transfers now fail fast when ``disk0:`` lacks room instead of half-writing flash. - ASADevice._get_free_space parses the ``(N bytes free)`` trailer from ``dir`` output (same format EOS exposes). - file_copy calls _check_free_space with os.path.getsize on the local source before any SCP transfer. - remote_file_copy calls _pre_transfer_space_check inside the existing ``not verify_file`` block so the check only fires when a transfer would actually happen; still fail-open when src.file_size_bytes is None. - Unit tests cover the dir-trailer parse, the missing-trailer raise, file_copy raising before SCP, remote_file_copy raising before any copy command, and the fail-open path spying on _check_free_space. - Existing file_copy unit tests updated to mock os.path.getsize and _check_free_space (previously they passed a non-existent path that would now blow up on os.path.getsize). - Integration tests mirror the EOS pattern for manual lab runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyntc/devices/asa_device.py | 37 +++++++ tests/integration/test_asa_device.py | 117 ++++++++++++++++++++- tests/unit/test_devices/test_asa_device.py | 107 ++++++++++++++++++- 3 files changed, 256 insertions(+), 5 deletions(-) diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index 8babeb0a..014ad509 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -138,6 +138,34 @@ def _get_file_system(self): log.debug("Host %s: File system %s.", self.host, file_system) return file_system + def _get_free_space(self, file_system=None): # pylint: disable=unused-argument + """Return free bytes on ``file_system`` as reported by ASA's ``dir`` output. + + ASA exposes a single flash filesystem in practice and ``dir`` always prints + `` bytes total ( bytes free)`` as the last line, so the + ``file_system`` argument is accepted for API parity with other drivers but is + otherwise unused. + + Args: + file_system (str, optional): Ignored; retained for BaseDevice API parity. + + Returns: + int: Free bytes available on the target filesystem. + + Raises: + CommandError: When the ``dir`` output does not contain a parseable + ``(N bytes free)`` trailer. + """ + raw_data = self.show("dir") + # Example: 16777216 bytes total (1592488 bytes free) + match = re.search(r"\((\d+)\s+bytes\s+free\)", raw_data) + if match is None: + log.error("Host %s: could not parse free space from 'dir' output.", self.host) + raise CommandError(command="dir", message="Unable to parse free space from dir output.") + free_bytes = int(match.group(1)) + log.debug("Host %s: %s bytes free on flash.", self.host, free_bytes) + return free_bytes + def _get_ipv4_addresses(self, host: str) -> Dict[str, List[IPv4Address]]: """ Get IPv4 Addresses for ``host``. @@ -555,6 +583,8 @@ def file_copy( Raises: FileTransferError: When the ``src`` file is unable to transfer the file to any device. + NotEnoughFreeSpaceError: When ``file_system`` has fewer free bytes than + ``src`` requires. Example: >>> dev = ASADevice(**connection_args) @@ -566,6 +596,8 @@ def file_copy( if file_system is None: file_system = self._get_file_system() + self._check_free_space(os.path.getsize(src), file_system=file_system) + # netmiko's enable_scp self.enable_scp() self._file_copy(src, dest, file_system) @@ -1018,6 +1050,10 @@ def remote_file_copy(self, src: FileCopyModel = None, dest=None, **kwargs: Any): Raises: TypeError: If ``src`` is not a ``FileCopyModel`` instance. FileTransferError: If the transfer fails or the file cannot be verified afterwards. + NotEnoughFreeSpaceError: If ``src.file_size_bytes`` is set and + ``file_system`` has fewer free bytes than ``src.file_size_bytes``. + When ``file_size`` is omitted from ``src``, the pre-transfer space + check is skipped entirely. """ if not isinstance(src, FileCopyModel): raise TypeError("src must be an instance of FileCopyModel") @@ -1028,6 +1064,7 @@ def remote_file_copy(self, src: FileCopyModel = None, dest=None, **kwargs: Any): dest = src.file_name if not self.verify_file(src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system): + self._pre_transfer_space_check(src, file_system=file_system) current_prompt = self.native.find_prompt() prompt_answers = { r"Password": src.token or "", diff --git a/tests/integration/test_asa_device.py b/tests/integration/test_asa_device.py index fc230b9e..c4b8e490 100644 --- a/tests/integration/test_asa_device.py +++ b/tests/integration/test_asa_device.py @@ -14,6 +14,8 @@ export HTTP_URL=http://:@:8081/ export HTTPS_URL=https://:@:8443/ export FILE_CHECKSUM= + export FILE_SIZE= + export FILE_SIZE_UNIT=megabytes # optional; defaults to "bytes" poetry run pytest tests/integration/test_asa_device.py -v Set only the protocol URL vars for the servers you have available; each @@ -31,15 +33,21 @@ HTTPS_URL - HTTPS URL of the file to transfer FILE_NAME - Destination filename on the device (default: basename of URL path) FILE_CHECKSUM - Expected sha512 checksum of the file (shared across all protocols) + FILE_SIZE - Expected size of the file expressed in FILE_SIZE_UNIT units; used for + the pre-transfer free-space check + FILE_SIZE_UNIT - One of "bytes", "megabytes", or "gigabytes" (default: "bytes") """ import os +from unittest import mock import pytest from pyntc.devices import ASADevice +from pyntc.errors import NotEnoughFreeSpaceError +from pyntc.utils.models import FILE_SIZE_UNITS, FileCopyModel -from ._helpers import build_file_copy_model +from ._helpers import PROTOCOL_URL_VARS, build_file_copy_model, first_available_url # --------------------------------------------------------------------------- # Fixtures @@ -127,3 +135,110 @@ def test_verify_file_after_copy(device, any_file_copy_model): if not device.check_file_exists(any_file_copy_model.file_name): pytest.skip("File does not exist on device; run a copy test first") assert device.verify_file(any_file_copy_model.checksum, any_file_copy_model.file_name, hashing_algorithm="sha512") + + +# --------------------------------------------------------------------------- +# Free-space / pre-transfer tests (NAPPS-1087) +# --------------------------------------------------------------------------- + + +def test_get_free_space_returns_positive_int(device): + """``_get_free_space`` parses the ``dir`` trailer into a positive int.""" + free = device._get_free_space() # pylint: disable=protected-access + assert isinstance(free, int) + assert free > 0 + + +def test_check_free_space_succeeds_for_small_request(device): + """A 1-byte request must always fit; ``_check_free_space`` returns ``None``.""" + # pylint: disable=protected-access + assert device._check_free_space(required_bytes=1) is None + + +def test_check_free_space_raises_when_required_exceeds_free(device): + """When required bytes exceed what the device reports, raise NotEnoughFreeSpaceError.""" + # pylint: disable=protected-access + free = device._get_free_space() + with pytest.raises(NotEnoughFreeSpaceError): + device._check_free_space(required_bytes=free + 1) + + +def test_file_size_unit_conversion_matches_device_free_space(device): + """A megabyte-denominated request converts through ``FILE_SIZE_UNITS`` correctly.""" + # pylint: disable=protected-access + free_bytes = device._get_free_space() + one_mb = FILE_SIZE_UNITS["megabytes"] + if free_bytes < one_mb: + pytest.skip("Device has less than 1 MB free; conversion sanity test not meaningful") + assert device._check_free_space(required_bytes=one_mb) is None + + +def test_remote_file_copy_rejects_oversized_transfer(device): + """remote_file_copy raises NotEnoughFreeSpaceError and never copies the file.""" + checksum = os.environ.get("FILE_CHECKSUM") + scheme, url = first_available_url() + if not (url and checksum): + pytest.skip("No protocol URL / FILE_CHECKSUM environment variables not set") + + # pylint: disable=protected-access + free_bytes = device._get_free_space() + free_gb = free_bytes // FILE_SIZE_UNITS["gigabytes"] + oversized_gb = max(free_gb * 10, 10) + + unique_name = f"pyntc_integration_space_check_{os.getpid()}_{scheme}.bin" + model = FileCopyModel( + download_url=url, + checksum=checksum, + file_name=unique_name, + file_size=oversized_gb, + file_size_unit="gigabytes", + hashing_algorithm="sha512", + timeout=60, + ) + + assert not device.check_file_exists(unique_name), "Unique filename unexpectedly exists before test" + + with pytest.raises(NotEnoughFreeSpaceError): + device.remote_file_copy(model) + + assert not device.check_file_exists(unique_name) + + +def test_remote_file_copy_accepts_declared_size_within_free_space(device): + """A correctly-sized FileCopyModel copies without the space check interfering.""" + scheme, _url = first_available_url() + if scheme is None: + pytest.skip("No protocol URL environment variables set") + model = build_file_copy_model(PROTOCOL_URL_VARS[scheme]) + # pylint: disable=protected-access + free_bytes = device._get_free_space() + assert ( + model.file_size_bytes <= free_bytes + ), "Configured FILE_SIZE/FILE_SIZE_UNIT exceeds device free space; update env vars" + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_skips_space_check_when_file_size_omitted(device): + """When FileCopyModel has no file_size, _check_free_space is never called.""" + checksum = os.environ.get("FILE_CHECKSUM") + file_name = os.environ.get("FILE_NAME") + _, url = first_available_url() + if not (url and checksum and file_name): + pytest.skip("URL / FILE_CHECKSUM / FILE_NAME environment variables not set") + + model = FileCopyModel( + download_url=url, + checksum=checksum, + file_name=file_name, + hashing_algorithm="sha512", + timeout=60, + ) # file_size intentionally omitted + assert model.file_size is None + assert model.file_size_bytes is None + + with mock.patch.object(ASADevice, "_check_free_space") as spy: + device.remote_file_copy(model) + + spy.assert_not_called() + assert device.check_file_exists(model.file_name) diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index 2a3fa3c8..c8d7180e 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -6,7 +6,7 @@ from pyntc.devices import ASADevice from pyntc.devices import asa_device as asa_module -from pyntc.errors import FileTransferError +from pyntc.errors import CommandError, FileTransferError, NotEnoughFreeSpaceError from pyntc.utils.models import FileCopyModel from .device_mocks.asa import send_command @@ -536,11 +536,15 @@ def test_enable_scp_enable_fail(mock_log, mock_save, mock_config, mock_peer_devi mock_save.assert_not_called() +@mock.patch("pyntc.devices.asa_device.os.path.getsize", return_value=1024) +@mock.patch.object(ASADevice, "_check_free_space") @mock.patch.object(os.path, "basename", return_value="a.txt") @mock.patch.object(ASADevice, "_get_file_system", return_value="flash:") @mock.patch.object(ASADevice, "enable_scp") @mock.patch.object(ASADevice, "_file_copy") -def test_file_copy_no_peer_no_args(mock_file_copy, mock_enable_scp, mock_get_file_system, mock_basename, asa_device): +def test_file_copy_no_peer_no_args( + mock_file_copy, mock_enable_scp, mock_get_file_system, mock_basename, _check_space, _getsize, asa_device +): asa_device.file_copy("path/to/a.txt") mock_basename.assert_called() mock_get_file_system.assert_called() @@ -549,11 +553,15 @@ def test_file_copy_no_peer_no_args(mock_file_copy, mock_enable_scp, mock_get_fil mock_file_copy.assert_called_with("path/to/a.txt", "a.txt", "flash:") +@mock.patch("pyntc.devices.asa_device.os.path.getsize", return_value=1024) +@mock.patch.object(ASADevice, "_check_free_space") @mock.patch.object(os.path, "basename") @mock.patch.object(ASADevice, "_get_file_system") @mock.patch.object(ASADevice, "enable_scp") @mock.patch.object(ASADevice, "_file_copy") -def test_file_copy_no_peer_pass_args(mock_file_copy, mock_enable_scp, mock_get_file_system, mock_basename, asa_device): +def test_file_copy_no_peer_pass_args( + mock_file_copy, mock_enable_scp, mock_get_file_system, mock_basename, _check_space, _getsize, asa_device +): args = ("path/to/a.txt", "b.txt", "bootflash:") asa_device.file_copy(*args) mock_basename.assert_not_called() @@ -563,13 +571,22 @@ def test_file_copy_no_peer_pass_args(mock_file_copy, mock_enable_scp, mock_get_f mock_file_copy.assert_called_with(*args) +@mock.patch("pyntc.devices.asa_device.os.path.getsize", return_value=1024) +@mock.patch.object(ASADevice, "_check_free_space") @mock.patch.object(os.path, "basename") @mock.patch.object(ASADevice, "_get_file_system") @mock.patch.object(ASADevice, "enable_scp") @mock.patch.object(ASADevice, "_file_copy") @mock.patch.object(ASADevice, "peer_device") def test_file_copy_include_peer( - mock_peer_device, mock_file_copy, mock_enable_scp, mock_get_file_system, mock_basename, asa_device + mock_peer_device, + mock_file_copy, + mock_enable_scp, + mock_get_file_system, + mock_basename, + _check_space, + _getsize, + asa_device, ): mock_peer_device.return_value = asa_device args = ("path/to/a.txt", "a.txt", "flash:") @@ -1415,3 +1432,85 @@ def test_remote_file_copy_https_clean_url_used_in_command(mock_verify, mock_fs, call_args = asa_device.native.send_command.call_args_list[0][0][0] assert "example-user:example-password" not in call_args assert "https://192.0.2.1/asa.bin" in call_args + + +# --------------------------------------------------------------------------- +# Pre-transfer free-space tests (NAPPS-1087) +# --------------------------------------------------------------------------- + +DIR_OUTPUT_WITH_TRAILER = ( + "Directory of disk0:/\n\n" + "1 -rw- 15183868 asa9-12-3-11-smp-k8.bin\n\n" + "16777216 bytes total (1592488 bytes free)" +) + + +@mock.patch.object(ASADevice, "show", return_value=DIR_OUTPUT_WITH_TRAILER) +def test_get_free_space_parses_dir_trailer(_mock_show, asa_device): + """_get_free_space returns the bytes-free value from the dir trailer.""" + assert asa_device._get_free_space() == 1592488 + + +@mock.patch.object(ASADevice, "show", return_value="Directory of disk0:/\nno trailer here") +def test_get_free_space_raises_when_trailer_missing(_mock_show, asa_device): + """_get_free_space raises CommandError when the trailer can't be parsed.""" + with pytest.raises(CommandError): + asa_device._get_free_space() + + +@mock.patch("pyntc.devices.asa_device.os.path.getsize", return_value=10**12) +@mock.patch.object(ASADevice, "_get_file_system", return_value="disk0:") +@mock.patch.object(ASADevice, "show", return_value=DIR_OUTPUT_WITH_TRAILER) +@mock.patch.object(ASADevice, "enable_scp") +@mock.patch.object(ASADevice, "_file_copy") +def test_file_copy_raises_not_enough_free_space(mock_file_copy, mock_enable_scp, _show, _fs, _getsize, asa_device): + """file_copy raises NotEnoughFreeSpaceError and never runs SCP transfer.""" + with pytest.raises(NotEnoughFreeSpaceError): + asa_device.file_copy("path/to/image.bin") + + mock_enable_scp.assert_not_called() + mock_file_copy.assert_not_called() + + +@mock.patch.object(ASADevice, "_get_file_system", return_value="disk0:") +@mock.patch.object(ASADevice, "verify_file", return_value=False) +@mock.patch.object(ASADevice, "show", return_value=DIR_OUTPUT_WITH_TRAILER) +def test_remote_file_copy_raises_not_enough_free_space(_show, _verify, _fs, asa_device): + """remote_file_copy raises NotEnoughFreeSpaceError and never issues a copy command.""" + oversized = FileCopyModel( + download_url="ftp://192.0.2.1/asa.bin", + checksum=SHA512_CHECKSUM, + file_name="asa.bin", + file_size=2, + file_size_unit="gigabytes", + hashing_algorithm="sha512", + ) + + with pytest.raises(NotEnoughFreeSpaceError): + asa_device.remote_file_copy(oversized) + + asa_device.native.send_command.assert_not_called() + + +@mock.patch.object(ASADevice, "_get_file_system", return_value="disk0:") +@mock.patch.object(ASADevice, "_check_free_space") +@mock.patch.object(ASADevice, "verify_file", side_effect=[False, True]) +def test_remote_file_copy_skips_space_check_when_file_size_omitted(mock_verify, mock_check, _fs, asa_device): + """When FileCopyModel has no file_size, _check_free_space is NOT called.""" + model = FileCopyModel( + download_url="ftp://192.0.2.1/asa.bin", + checksum=SHA512_CHECKSUM, + file_name="asa.bin", + hashing_algorithm="sha512", + ) # file_size intentionally omitted + assert model.file_size_bytes is None + + asa_device.native.find_prompt.return_value = "asa5512#" + asa_device.native.send_command.side_effect = [ + "94038 bytes copied in 0.90 secs", + ] + + asa_device.remote_file_copy(model) + + mock_check.assert_not_called() + asa_device.native.send_command.assert_called() # transfer still happens From 21fe26e30f7f473e1242ad9aebbcda4ea0e5f107 Mon Sep 17 00:00:00 2001 From: James Williams Date: Tue, 21 Apr 2026 14:43:49 -0500 Subject: [PATCH 2/4] Add NAPPS-1087 changelog fragment Co-Authored-By: Claude Opus 4.7 (1M context) --- changes/372.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/372.added diff --git a/changes/372.added b/changes/372.added new file mode 100644 index 00000000..495b95a2 --- /dev/null +++ b/changes/372.added @@ -0,0 +1 @@ +Added a pre-transfer free-space check to Cisco ASA ``file_copy`` and ``remote_file_copy`` that raises ``NotEnoughFreeSpaceError`` when the target filesystem lacks room for the image. From b6400c4b1508b841ede717098db0eea5a16db478 Mon Sep 17 00:00:00 2001 From: James Williams Date: Tue, 21 Apr 2026 15:18:49 -0500 Subject: [PATCH 3/4] Apply ruff format to ASA integration tests Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/test_asa_device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_asa_device.py b/tests/integration/test_asa_device.py index c4b8e490..a4579e0d 100644 --- a/tests/integration/test_asa_device.py +++ b/tests/integration/test_asa_device.py @@ -212,9 +212,9 @@ def test_remote_file_copy_accepts_declared_size_within_free_space(device): model = build_file_copy_model(PROTOCOL_URL_VARS[scheme]) # pylint: disable=protected-access free_bytes = device._get_free_space() - assert ( - model.file_size_bytes <= free_bytes - ), "Configured FILE_SIZE/FILE_SIZE_UNIT exceeds device free space; update env vars" + assert model.file_size_bytes <= free_bytes, ( + "Configured FILE_SIZE/FILE_SIZE_UNIT exceeds device free space; update env vars" + ) device.remote_file_copy(model) assert device.check_file_exists(model.file_name) From 8ec1679b5d1ba0b91c9222b8a6fa33b670af0726 Mon Sep 17 00:00:00 2001 From: James Williams Date: Tue, 21 Apr 2026 15:26:28 -0500 Subject: [PATCH 4/4] Broaden ASA dir free-space regex for real-device trailer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real ASA platforms print the dir trailer as ``( bytes free/% free)`` — the closing paren is not adjacent to ``free``. The previous regex anchored on ``\)`` and so would have failed on live hardware (verified against an ASA 5512 running 9.9.2 at 10.1.100.80, which reports ``4118732800 bytes total (3580170240 bytes free/86% free)``). Drop the trailing ``\)`` anchor and update the mock fixture + tests to cover both the real-device shape and the legacy ``(N bytes free)`` emulator shape. Bumped the ``test_remote_file_copy_raises_not_enough_free_space`` oversize from 2 GB to 10 GB because the mock now reports ~3.3 GB free — 2 GB fits, 10 GB does not. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyntc/devices/asa_device.py | 17 +++++++++++------ .../device_mocks/asa/send_command/dir | 2 +- tests/unit/test_devices/test_asa_device.py | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index 014ad509..b51223ce 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -142,9 +142,12 @@ def _get_free_space(self, file_system=None): # pylint: disable=unused-argument """Return free bytes on ``file_system`` as reported by ASA's ``dir`` output. ASA exposes a single flash filesystem in practice and ``dir`` always prints - `` bytes total ( bytes free)`` as the last line, so the - ``file_system`` argument is accepted for API parity with other drivers but is - otherwise unused. + a `` bytes total ( bytes free...)`` trailer as the last line. + Real platforms append an ``/% free`` suffix inside the parentheses + (e.g., ``(3580170240 bytes free/86% free)``); older releases and some + emulators omit it. The regex matches both shapes. The ``file_system`` + argument is accepted for API parity with other drivers but is otherwise + unused. Args: file_system (str, optional): Ignored; retained for BaseDevice API parity. @@ -154,11 +157,13 @@ def _get_free_space(self, file_system=None): # pylint: disable=unused-argument Raises: CommandError: When the ``dir`` output does not contain a parseable - ``(N bytes free)`` trailer. + ``N bytes free`` trailer. """ raw_data = self.show("dir") - # Example: 16777216 bytes total (1592488 bytes free) - match = re.search(r"\((\d+)\s+bytes\s+free\)", raw_data) + # Examples seen in the wild: + # 16777216 bytes total (1592488 bytes free) + # 4118732800 bytes total (3580170240 bytes free/86% free) + match = re.search(r"\((\d+)\s+bytes\s+free", raw_data) if match is None: log.error("Host %s: could not parse free space from 'dir' output.", self.host) raise CommandError(command="dir", message="Unable to parse free space from dir output.") diff --git a/tests/unit/test_devices/device_mocks/asa/send_command/dir b/tests/unit/test_devices/device_mocks/asa/send_command/dir index 4d515a1c..4faee3f6 100644 --- a/tests/unit/test_devices/device_mocks/asa/send_command/dir +++ b/tests/unit/test_devices/device_mocks/asa/send_command/dir @@ -2,4 +2,4 @@ Directory of disk0:/ 1 -rw- 15183868 asa9-12-3-11-smp-k8.bin -16777216 bytes total (1592488 bytes free) \ No newline at end of file +4118732800 bytes total (3580170240 bytes free/86% free) diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index c8d7180e..eb99c47f 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -1439,6 +1439,11 @@ def test_remote_file_copy_https_clean_url_used_in_command(mock_verify, mock_fs, # --------------------------------------------------------------------------- DIR_OUTPUT_WITH_TRAILER = ( + "Directory of disk0:/\n\n" + "1 -rw- 15183868 asa9-12-3-11-smp-k8.bin\n\n" + "4118732800 bytes total (3580170240 bytes free/86% free)" +) +DIR_OUTPUT_LEGACY_TRAILER = ( "Directory of disk0:/\n\n" "1 -rw- 15183868 asa9-12-3-11-smp-k8.bin\n\n" "16777216 bytes total (1592488 bytes free)" @@ -1447,7 +1452,13 @@ def test_remote_file_copy_https_clean_url_used_in_command(mock_verify, mock_fs, @mock.patch.object(ASADevice, "show", return_value=DIR_OUTPUT_WITH_TRAILER) def test_get_free_space_parses_dir_trailer(_mock_show, asa_device): - """_get_free_space returns the bytes-free value from the dir trailer.""" + """_get_free_space returns the bytes-free value from the real-device dir trailer.""" + assert asa_device._get_free_space() == 3580170240 + + +@mock.patch.object(ASADevice, "show", return_value=DIR_OUTPUT_LEGACY_TRAILER) +def test_get_free_space_parses_legacy_dir_trailer(_mock_show, asa_device): + """_get_free_space parses the older ``(N bytes free)`` trailer shape.""" assert asa_device._get_free_space() == 1592488 @@ -1481,7 +1492,7 @@ def test_remote_file_copy_raises_not_enough_free_space(_show, _verify, _fs, asa_ download_url="ftp://192.0.2.1/asa.bin", checksum=SHA512_CHECKSUM, file_name="asa.bin", - file_size=2, + file_size=10, file_size_unit="gigabytes", hashing_algorithm="sha512", )