mirror of https://github.com/ansible/ansible.git
Add ansible-galaxy collection list command (#65022)
* Start adding ansible-galaxy collection list options * Working list all collections and list a specific collection * Nuke debugging cruft * Use to_text to get a string of the FQCN for sorting * Improve collection output formatting - add header - display collection name and version in separate columns - width of columns is dynamic based on collection name and version length * Make role list output match collection output list - add header - add columns for role name and version - make column width dynamic based on name and version length * remove debug statemnt and extra header * Revert "Make role list output match collection output list" This reverts commit a0b3db47bb3b198aafd34c1f1be5b6561af2f928. * Add validate_collection_path function Utility function for ensuring a collection target ends with 'ansible_collection' * Use validate_collection_path * Do not warn if a specific collection in found in any search path * Fix extraneous warning and remove duplicate code Do not warn when listing a specific collection and it does not exist in other collection paths. Restructure the code that loops through collection paths to remove duplicate conditional code. * Indicate role path was found * Use new function name * WIP Save Point * Use separate functions for role and collection list * Wrap error message There may be a better way to do this besides hard coding a line break, but this does make the message a lot more readable. * Add validate_collection_path function (#66441) * Add validate_collection_path function Utility function for ensuring a collection target ends with 'ansible_collection' * Fix bad syntax * Correct docstring * Bikeshed the names * Properly list a single role * Simplify _display_warnings() Only display warnings. Move exception raise back to each caller. * Move private methods to private functions They don't need self, so it makes sense to have them as functions Get rid of _display_warnings() function since it doesn't do anything worthy of an independent function. * Add integration tests for ansible-galaxy collection list * Fix docs sanity test * Fix bug where ansible_collections dir does not exist The path may exist, but if there is no ansible_collections dir inside that path, an exception was raised in find_existing_collections(). Add integration test for this scenario * Put execute_list() method back * Add some informational messages for debugging * Add unit tests Units tests for the various private methods in support of collection list * Start adding unit tests for test_execute_list * Display collection path when listing specific collection * Add unit tests for listing all collections and specific collection - Create fixture for creating test objects - Add function for controlling os.path.isdir results * Set defaults for minimum collection widths Ensure that collections with small FQCNs display correctly. Add unit tests * Split up unit tests and fix fixtures Add more fixtures for mocking objects during the specific collection tests * Change help message for -p in list subcommand Give accurate description of what it actually does rather than trying to use language shared between sub commands. * Disable colorized output in unit test * Add docs for collection list * Fix integration test on macOS The temp file path is really long on macOS, so the warning message gets wrapped across multiple lines. That make seth grep fail. Switch to matching on a smaller part of the warning. * Recreate common path options for collections Improve help about what the '-p' option does and how it works. * Remove unnecessary elif after continue statements * Account for duplicate paths in collections_searh_paths If someone specifies the same path via '-p' that is the COLLECTIONS_PATHS, do not list the collections twice. * Docs updatespull/67436/head
parent
f35293c052
commit
f506fd4730
@ -0,0 +1,2 @@
|
||||
minor_changes:
|
||||
- ansible-galaxy - add ``collection list`` command for listing installed collections (https://github.com/ansible/ansible/pull/65022)
|
@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.cli.galaxy import _display_collection
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def collection_object(mocker):
|
||||
def _cobj(fqcn='sandwiches.ham'):
|
||||
cobj = mocker.MagicMock(latest_version='1.5.0')
|
||||
cobj.__str__.return_value = fqcn
|
||||
return cobj
|
||||
return _cobj
|
||||
|
||||
|
||||
def test_display_collection(capsys, collection_object):
|
||||
_display_collection(collection_object())
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert out == 'sandwiches.ham 1.5.0 \n'
|
||||
|
||||
|
||||
def test_display_collections_small_max_widths(capsys, collection_object):
|
||||
_display_collection(collection_object(), 1, 1)
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert out == 'sandwiches.ham 1.5.0 \n'
|
||||
|
||||
|
||||
def test_display_collections_large_max_widths(capsys, collection_object):
|
||||
_display_collection(collection_object(), 20, 20)
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert out == 'sandwiches.ham 1.5.0 \n'
|
||||
|
||||
|
||||
def test_display_collection_small_minimum_widths(capsys, collection_object):
|
||||
_display_collection(collection_object('a.b'), min_cwidth=0, min_vwidth=0)
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert out == 'a.b 1.5.0 \n'
|
@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.cli.galaxy import _display_header
|
||||
|
||||
|
||||
def test_display_header_default(capsys):
|
||||
_display_header('/collections/path', 'h1', 'h2')
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /collections/path'
|
||||
assert out_lines[2] == 'h1 h2 '
|
||||
assert out_lines[3] == '---------- -------'
|
||||
|
||||
|
||||
def test_display_header_widths(capsys):
|
||||
_display_header('/collections/path', 'Collection', 'Version', 18, 18)
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /collections/path'
|
||||
assert out_lines[2] == 'Collection Version '
|
||||
assert out_lines[3] == '------------------ ------------------'
|
||||
|
||||
|
||||
def test_display_header_small_widths(capsys):
|
||||
_display_header('/collections/path', 'Col', 'Ver', 1, 1)
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /collections/path'
|
||||
assert out_lines[2] == 'Col Ver'
|
||||
assert out_lines[3] == '--- ---'
|
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.cli.galaxy import _display_role
|
||||
|
||||
|
||||
def test_display_role(mocker, capsys):
|
||||
mocked_galaxy_role = mocker.Mock(install_info=None)
|
||||
mocked_galaxy_role.name = 'testrole'
|
||||
_display_role(mocked_galaxy_role)
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert out_lines[0] == '- testrole, (unknown version)'
|
||||
|
||||
|
||||
def test_display_role_known_version(mocker, capsys):
|
||||
mocked_galaxy_role = mocker.Mock(install_info={'version': '1.0.0'})
|
||||
mocked_galaxy_role.name = 'testrole'
|
||||
_display_role(mocked_galaxy_role)
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert out_lines[0] == '- testrole, 1.0.0'
|
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible import context
|
||||
from ansible.cli.galaxy import GalaxyCLI
|
||||
|
||||
|
||||
def test_execute_list_role_called(mocker):
|
||||
"""Make sure the correct method is called for a role"""
|
||||
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'role', 'list'])
|
||||
context.CLIARGS._store = {'type': 'role'}
|
||||
execute_list_role_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_role', side_effect=AttributeError('raised intentionally'))
|
||||
execute_list_collection_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_collection', side_effect=AttributeError('raised intentionally'))
|
||||
with pytest.raises(AttributeError):
|
||||
gc.execute_list()
|
||||
|
||||
assert execute_list_role_mock.call_count == 1
|
||||
assert execute_list_collection_mock.call_count == 0
|
||||
|
||||
|
||||
def test_execute_list_collection_called(mocker):
|
||||
"""Make sure the correct method is called for a collection"""
|
||||
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
|
||||
context.CLIARGS._store = {'type': 'collection'}
|
||||
execute_list_role_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_role', side_effect=AttributeError('raised intentionally'))
|
||||
execute_list_collection_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_collection', side_effect=AttributeError('raised intentionally'))
|
||||
with pytest.raises(AttributeError):
|
||||
gc.execute_list()
|
||||
|
||||
assert execute_list_role_mock.call_count == 0
|
||||
assert execute_list_collection_mock.call_count == 1
|
@ -0,0 +1,276 @@
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible import context
|
||||
from ansible.cli.galaxy import GalaxyCLI
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.galaxy.collection import CollectionRequirement
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
def path_exists(path):
|
||||
if to_native(path) == '/root/.ansible/collections/ansible_collections/sandwiches/ham':
|
||||
return False
|
||||
elif to_native(path) == '/usr/share/ansible/collections/ansible_collections/sandwiches/reuben':
|
||||
return False
|
||||
elif to_native(path) == 'nope':
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def isdir(path):
|
||||
if to_native(path) == 'nope':
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def cliargs(collections_paths=None, collection_name=None):
|
||||
if collections_paths is None:
|
||||
collections_paths = ['~/root/.ansible/collections', '/usr/share/ansible/collections']
|
||||
|
||||
context.CLIARGS._store = {
|
||||
'collections_path': collections_paths,
|
||||
'collection': collection_name,
|
||||
'type': 'collection',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_collection_objects(mocker):
|
||||
mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', '/usr/share/ansible/collections'])
|
||||
mocker.patch('ansible.cli.galaxy.validate_collection_path',
|
||||
side_effect=['/root/.ansible/collections/ansible_collections', '/usr/share/ansible/collections/ansible_collections'])
|
||||
|
||||
collection_args = (
|
||||
(
|
||||
'sandwiches',
|
||||
'pbj',
|
||||
b'/usr/share/ansible/collections/ansible_collections/sandwiches/pbj',
|
||||
mocker.Mock(),
|
||||
['1.0.0', '1.5.0'],
|
||||
'1.0.0',
|
||||
False,
|
||||
),
|
||||
(
|
||||
'sandwiches',
|
||||
'pbj',
|
||||
b'/root/.ansible/collections/ansible_collections/sandwiches/pbj',
|
||||
mocker.Mock(),
|
||||
['1.0.0', '1.5.0'],
|
||||
'1.5.0',
|
||||
False,
|
||||
),
|
||||
(
|
||||
'sandwiches',
|
||||
'ham',
|
||||
b'/usr/share/ansible/collections/ansible_collections/sandwiches/ham',
|
||||
mocker.Mock(),
|
||||
['1.0.0'],
|
||||
'1.0.0',
|
||||
False,
|
||||
),
|
||||
(
|
||||
'sandwiches',
|
||||
'reuben',
|
||||
b'/root/.ansible/collections/ansible_collections/sandwiches/reuben',
|
||||
mocker.Mock(),
|
||||
['1.0.0', '2.5.0'],
|
||||
'2.5.0',
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
collections_path_1 = [CollectionRequirement(*cargs) for cargs in collection_args if to_native(cargs[2]).startswith('/root')]
|
||||
collections_path_2 = [CollectionRequirement(*cargs) for cargs in collection_args if to_native(cargs[2]).startswith('/usr/share')]
|
||||
mocker.patch('ansible.cli.galaxy.find_existing_collections', side_effect=[collections_path_1, collections_path_2])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_from_path(mocker):
|
||||
def _from_path(collection_name='pbj'):
|
||||
collection_args = {
|
||||
'sandwiches.pbj': (
|
||||
(
|
||||
'sandwiches',
|
||||
'pbj',
|
||||
b'/root/.ansible/collections/ansible_collections/sandwiches/pbj',
|
||||
mocker.Mock(),
|
||||
['1.0.0', '1.5.0'],
|
||||
'1.5.0',
|
||||
False,
|
||||
),
|
||||
(
|
||||
'sandwiches',
|
||||
'pbj',
|
||||
b'/usr/share/ansible/collections/ansible_collections/sandwiches/pbj',
|
||||
mocker.Mock(),
|
||||
['1.0.0', '1.5.0'],
|
||||
'1.0.0',
|
||||
False,
|
||||
),
|
||||
),
|
||||
'sandwiches.ham': (
|
||||
(
|
||||
'sandwiches',
|
||||
'ham',
|
||||
b'/usr/share/ansible/collections/ansible_collections/sandwiches/ham',
|
||||
mocker.Mock(),
|
||||
['1.0.0'],
|
||||
'1.0.0',
|
||||
False,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
from_path_objects = [CollectionRequirement(*args) for args in collection_args[collection_name]]
|
||||
mocker.patch('ansible.galaxy.collection.CollectionRequirement.from_path', side_effect=from_path_objects)
|
||||
|
||||
return _from_path
|
||||
|
||||
|
||||
def test_execute_list_collection_all(mocker, capsys, mock_collection_objects):
|
||||
"""Test listing all collections from multiple paths"""
|
||||
|
||||
cliargs()
|
||||
|
||||
mocker.patch('os.path.exists', return_value=True)
|
||||
mocker.patch('os.path.isdir', return_value=True)
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
|
||||
gc.execute_list_collection()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert len(out_lines) == 12
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /root/.ansible/collections/ansible_collections'
|
||||
assert out_lines[2] == 'Collection Version'
|
||||
assert out_lines[3] == '----------------- -------'
|
||||
assert out_lines[4] == 'sandwiches.pbj 1.5.0 '
|
||||
assert out_lines[5] == 'sandwiches.reuben 2.5.0 '
|
||||
assert out_lines[6] == ''
|
||||
assert out_lines[7] == '# /usr/share/ansible/collections/ansible_collections'
|
||||
assert out_lines[8] == 'Collection Version'
|
||||
assert out_lines[9] == '-------------- -------'
|
||||
assert out_lines[10] == 'sandwiches.ham 1.0.0 '
|
||||
assert out_lines[11] == 'sandwiches.pbj 1.0.0 '
|
||||
|
||||
|
||||
def test_execute_list_collection_specific(mocker, capsys, mock_collection_objects, mock_from_path):
|
||||
"""Test listing a specific collection"""
|
||||
|
||||
collection_name = 'sandwiches.ham'
|
||||
mock_from_path(collection_name)
|
||||
|
||||
cliargs(collection_name=collection_name)
|
||||
mocker.patch('os.path.exists', path_exists)
|
||||
mocker.patch('os.path.isdir', return_value=True)
|
||||
mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name)
|
||||
mocker.patch('ansible.cli.galaxy._get_collection_widths', return_value=(14, 5))
|
||||
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
|
||||
gc.execute_list_collection()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert len(out_lines) == 5
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /usr/share/ansible/collections/ansible_collections'
|
||||
assert out_lines[2] == 'Collection Version'
|
||||
assert out_lines[3] == '-------------- -------'
|
||||
assert out_lines[4] == 'sandwiches.ham 1.0.0 '
|
||||
|
||||
|
||||
def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_collection_objects, mock_from_path):
|
||||
"""Test listing a specific collection that exists at multiple paths"""
|
||||
|
||||
collection_name = 'sandwiches.pbj'
|
||||
mock_from_path(collection_name)
|
||||
|
||||
cliargs(collection_name=collection_name)
|
||||
mocker.patch('os.path.exists', path_exists)
|
||||
mocker.patch('os.path.isdir', return_value=True)
|
||||
mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name)
|
||||
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
|
||||
gc.execute_list_collection()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert len(out_lines) == 10
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /root/.ansible/collections/ansible_collections'
|
||||
assert out_lines[2] == 'Collection Version'
|
||||
assert out_lines[3] == '-------------- -------'
|
||||
assert out_lines[4] == 'sandwiches.pbj 1.5.0 '
|
||||
assert out_lines[5] == ''
|
||||
assert out_lines[6] == '# /usr/share/ansible/collections/ansible_collections'
|
||||
assert out_lines[7] == 'Collection Version'
|
||||
assert out_lines[8] == '-------------- -------'
|
||||
assert out_lines[9] == 'sandwiches.pbj 1.0.0 '
|
||||
|
||||
|
||||
def test_execute_list_collection_specific_invalid_fqcn(mocker):
|
||||
"""Test an invalid fully qualified collection name (FQCN)"""
|
||||
|
||||
collection_name = 'no.good.name'
|
||||
|
||||
cliargs(collection_name=collection_name)
|
||||
mocker.patch('os.path.exists', return_value=True)
|
||||
mocker.patch('os.path.isdir', return_value=True)
|
||||
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
|
||||
with pytest.raises(AnsibleError, match='Invalid collection name'):
|
||||
gc.execute_list_collection()
|
||||
|
||||
|
||||
def test_execute_list_collection_no_valid_paths(mocker, capsys):
|
||||
"""Test listing collections when no valid paths are given"""
|
||||
|
||||
cliargs()
|
||||
|
||||
mocker.patch('os.path.exists', return_value=True)
|
||||
mocker.patch('os.path.isdir', return_value=False)
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
|
||||
|
||||
with pytest.raises(AnsibleOptionsError, match=r'None of the provided paths were usable.'):
|
||||
gc.execute_list_collection()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert '[WARNING]: - the configured path' in err
|
||||
assert 'exists, but it is not a directory.' in err
|
||||
|
||||
|
||||
def test_execute_list_collection_one_invalid_path(mocker, capsys, mock_collection_objects):
|
||||
"""Test listing all collections when one invalid path is given"""
|
||||
|
||||
cliargs()
|
||||
mocker.patch('os.path.exists', return_value=True)
|
||||
mocker.patch('os.path.isdir', isdir)
|
||||
mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', 'nope'])
|
||||
mocker.patch('ansible.utils.color.ANSIBLE_COLOR', False)
|
||||
|
||||
gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', '-p', 'nope'])
|
||||
gc.execute_list_collection()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
out_lines = out.splitlines()
|
||||
|
||||
assert out_lines[0] == ''
|
||||
assert out_lines[1] == '# /root/.ansible/collections/ansible_collections'
|
||||
assert out_lines[2] == 'Collection Version'
|
||||
assert out_lines[3] == '----------------- -------'
|
||||
assert out_lines[4] == 'sandwiches.pbj 1.5.0 '
|
||||
# Only a partial test of the output
|
||||
|
||||
assert err == '[WARNING]: - the configured path nope, exists, but it is not a directory.\n'
|
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.cli.galaxy import _get_collection_widths
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def collection_objects(mocker):
|
||||
collection_ham = mocker.MagicMock(latest_version='1.5.0')
|
||||
collection_ham.__str__.return_value = 'sandwiches.ham'
|
||||
|
||||
collection_pbj = mocker.MagicMock(latest_version='2.5')
|
||||
collection_pbj.__str__.return_value = 'sandwiches.pbj'
|
||||
|
||||
collection_reuben = mocker.MagicMock(latest_version='4')
|
||||
collection_reuben.__str__.return_value = 'sandwiches.reuben'
|
||||
|
||||
return [collection_ham, collection_pbj, collection_reuben]
|
||||
|
||||
|
||||
def test_get_collection_widths(collection_objects):
|
||||
assert _get_collection_widths(collection_objects) == (17, 5)
|
||||
|
||||
|
||||
def test_get_collection_widths_single_collection(mocker):
|
||||
mocked_collection = mocker.MagicMock(latest_version='3.0.0')
|
||||
mocked_collection.__str__.return_value = 'sandwiches.club'
|
||||
# Make this look like it is not iterable
|
||||
mocker.patch('ansible.cli.galaxy.is_iterable', return_value=False)
|
||||
|
||||
assert _get_collection_widths(mocked_collection) == (15, 5)
|
Loading…
Reference in New Issue