Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions b2/_internal/console_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1898,8 +1898,6 @@ def _print_download_info(
self._print_file_attribute(label, attr_value)
for name in sorted(download_version.file_info):
self._print_file_attribute(f'INFO {name}', download_version.file_info[name])
if download_version.content_sha1 != 'none':
self._print('Checksum matches')
return 0

@classmethod
Expand Down Expand Up @@ -2041,6 +2039,12 @@ def _run(self, args):
progress_listener.change_description(output_filepath.name)

downloaded_file.save_to(output_filepath)
# only claim a match once the bytes were actually hashed and compared
if (
not args.skip_hash_verification
and downloaded_file.download_version.content_sha1 != 'none'
):
self._print('Checksum matches')
self._print('Download finished')

return 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Print `Checksum matches` after a download has been verified, and not at all when `--skip-hash-verification` is used.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unit tests no longer swallow exceptions raised by commands that use the progress bar (the `_TqdmCloser.__exit__` mock returned a truthy value).
3 changes: 2 additions & 1 deletion test/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ def bg_executor():

@pytest.fixture(autouse=True)
def disable_tqdm_closer_cleanup():
with mock.patch.object(_TqdmCloser, '__exit__'):
# return_value=None: a bare MagicMock returns a truthy MagicMock, which would swallow every exception
with mock.patch.object(_TqdmCloser, '__exit__', return_value=None):
yield


Expand Down
55 changes: 52 additions & 3 deletions test/unit/console_tool/test_download_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
######################################################################
import os
import pathlib
from unittest import mock

import pytest
from b2sdk.v2.exception import ChecksumMismatch

from test.helpers import skip_on_windows

Expand Down Expand Up @@ -88,13 +90,12 @@ def test_download_file_by_name(b2_cli, local_file, uploaded_file, tmp_path, flag
b2_cli.run(
[
'download-file-by-name',
flag,
uploaded_file['bucket'],
uploaded_file['fileName'],
str(output_path),
],
expected_stdout=EXPECTED_STDOUT_DOWNLOAD.format(
output_path=pathlib.Path(output_path).resolve()
),
expected_stdout=expected_stdout.format(output_path=pathlib.Path(output_path).resolve()),
expected_stderr='WARNING: `download-file-by-name` command is deprecated. Use `file download` instead.\n',
)
assert output_path.read_text() == uploaded_file['content']
Expand Down Expand Up @@ -283,3 +284,51 @@ def test_download_file_by_uri__explicit_file_target_allows_unsupported_server_na
)

assert output_path.read_text() == 'hello world'


def test_download_file__skipped_hash_verification_is_not_reported_as_matching(
b2_cli, uploaded_file, tmp_path
):
output_path = tmp_path / 'output.txt'

b2_cli.run(
['file', 'download', '--skip-hash-verification', 'b2id://9999', str(output_path)],
expected_stdout=None,
expected_part_of_stdout='Download finished',
unexpected_part_of_stdout='Checksum matches',
)
assert output_path.read_text() == uploaded_file['content']


def test_download_file__checksum_mismatch_is_not_reported_as_matching(
b2_cli, uploaded_file, tmp_path
):
output_path = tmp_path / 'output.txt'

with mock.patch(
'b2sdk.v2.DownloadedFile.save_to',
side_effect=ChecksumMismatch(checksum_type='sha1', expected='a' * 40, actual='b' * 40),
):
b2_cli.run(
['file', 'download', '--no-progress', 'b2id://9999', str(output_path)],
expected_stdout=None,
expected_stderr='ERROR: sha1 checksum mismatch -- bad data\n',
expected_status=1,
unexpected_part_of_stdout='Checksum matches',
)


def test_download_file__error_is_reported_when_progress_is_enabled(b2_cli, uploaded_file, tmp_path):
"""A failing download must give status 1 and an ERROR line also with the default progress bar"""
output_path = tmp_path / 'output.txt'

with mock.patch(
'b2sdk.v2.DownloadedFile.save_to',
side_effect=ChecksumMismatch(checksum_type='sha1', expected='a' * 40, actual='b' * 40),
):
b2_cli.run(
['file', 'download', 'b2id://9999', str(output_path)],
expected_stdout=None,
expected_stderr='ERROR: sha1 checksum mismatch -- bad data\n',
expected_status=1,
)
44 changes: 44 additions & 0 deletions test/unit/console_tool/test_upload_unbound_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
import inspect
import os
from unittest import mock

from b2sdk.v3 import DEFAULT_MIN_PART_SIZE

Expand Down Expand Up @@ -110,6 +112,48 @@ def test_upload_unbound_stream__with_part_size_options(
writer.result(timeout=1)


@skip_on_windows
def test_upload_unbound_stream__part_size_option_is_applied(
b2_cli, bucket, tmpdir, mock_stdin, bg_executor
):
"""--part-size must change the size of the uploaded parts (and the upload must be multipart)"""
part_size = (
DEFAULT_MIN_PART_SIZE + 1024
) # differs from the default, so a dropped flag is visible
expected_size = part_size * 2 + 500

fifo_file = tmpdir.join('fifo_file.txt')
os.mkfifo(str(fifo_file))
writer = bg_executor.submit(lambda: fifo_file.write('x' * expected_size))

with mock.patch.object(
b2_cli.raw_simulator, 'upload_part', wraps=b2_cli.raw_simulator.upload_part
) as upload_part:
b2_cli.run(
[
'upload-unbound-stream',
'--min-part-size',
str(DEFAULT_MIN_PART_SIZE),
'--part-size',
str(part_size),
'--no-progress',
'my-bucket',
str(fifo_file),
'named_pipe.txt',
],
expected_part_of_stdout='URL by file name:',
expected_stderr=UUS_DEPRECATION_WARNING,
)
writer.result(timeout=1)

signature = inspect.signature(b2_cli.raw_simulator.upload_part)
part_lengths = sorted(
signature.bind(*call.args, **call.kwargs).arguments['content_length']
for call in upload_part.call_args_list
)
assert part_lengths == sorted([part_size, part_size, 500])


def test_upload_unbound_stream__regular_file(b2_cli, bucket, tmpdir):
"""Test upload_unbound_stream regular file support"""
content = 'stdin input'
Expand Down
19 changes: 19 additions & 0 deletions test/unit/test_arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,22 @@ def test_help_in_non_utf8_terminal(command_name: str, monkeypatch):
pytest.fail(
f'Failed to encode help message for command "{command_name}" on a non-UTF-8 terminal: {e}'
)


@pytest.mark.parametrize('command_name', COMMAND_NAMES_CLASSES_MAPPING)
def test_real_command_help_in_non_utf8_terminal(command_name: str, monkeypatch, capsys):
"""Render each command's real parser help (options and subcommands), not only its docstring."""
from b2._internal.console_tool import ConsoleTool

with capsys.disabled():
monkeypatch.setattr(sys, 'stdout', _ASCIIEncodedStream(sys.stdout))
monkeypatch.setattr(sys, 'stderr', _ASCIIEncodedStream(sys.stderr))
argv = command_name.split()
try:
ConsoleTool(sys.stdout, sys.stderr).run_command([*argv, '--help'])
except SystemExit:
pass # argparse exits after printing help
except UnicodeEncodeError as e:
pytest.fail(
f'Failed to encode the real help of "{command_name}" on a non-UTF-8 terminal: {e}'
)
Loading
Loading