diff options
Diffstat (limited to '')
47 files changed, 3601 insertions, 0 deletions
diff --git a/test/units/module_utils/common/__init__.py b/test/units/module_utils/common/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/units/module_utils/common/__init__.py diff --git a/test/units/module_utils/common/arg_spec/__init__.py b/test/units/module_utils/common/arg_spec/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/units/module_utils/common/arg_spec/__init__.py diff --git a/test/units/module_utils/common/arg_spec/test_aliases.py b/test/units/module_utils/common/arg_spec/test_aliases.py new file mode 100644 index 0000000..7d30fb0 --- /dev/null +++ b/test/units/module_utils/common/arg_spec/test_aliases.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 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.module_utils.errors import AnsibleValidationError, AnsibleValidationErrorMultiple +from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult +from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages + +# id, argument spec, parameters, expected parameters, deprecation, warning +ALIAS_TEST_CASES = [ + ( + "alias", + {'path': {'aliases': ['dir', 'directory']}}, + {'dir': '/tmp'}, + { + 'dir': '/tmp', + 'path': '/tmp', + }, + "", + "", + ), + ( + "alias-duplicate-warning", + {'path': {'aliases': ['dir', 'directory']}}, + { + 'dir': '/tmp', + 'directory': '/tmp', + }, + { + 'dir': '/tmp', + 'directory': '/tmp', + 'path': '/tmp', + }, + "", + {'alias': 'directory', 'option': 'path'}, + ), + ( + "deprecated-alias", + { + 'path': { + 'aliases': ['not_yo_path'], + 'deprecated_aliases': [ + { + 'name': 'not_yo_path', + 'version': '1.7', + } + ] + } + }, + {'not_yo_path': '/tmp'}, + { + 'path': '/tmp', + 'not_yo_path': '/tmp', + }, + { + 'version': '1.7', + 'date': None, + 'collection_name': None, + 'msg': "Alias 'not_yo_path' is deprecated. See the module docs for more information", + }, + "", + ) +] + + +# id, argument spec, parameters, expected parameters, error +ALIAS_TEST_CASES_INVALID = [ + ( + "alias-invalid", + {'path': {'aliases': 'bad'}}, + {}, + {'path': None}, + "internal error: aliases must be a list or tuple", + ), + ( + # This isn't related to aliases, but it exists in the alias handling code + "default-and-required", + {'name': {'default': 'ray', 'required': True}}, + {}, + {'name': 'ray'}, + "internal error: required and default are mutually exclusive for name", + ), +] + + +@pytest.mark.parametrize( + ('arg_spec', 'parameters', 'expected', 'deprecation', 'warning'), + ((i[1:]) for i in ALIAS_TEST_CASES), + ids=[i[0] for i in ALIAS_TEST_CASES] +) +def test_aliases(arg_spec, parameters, expected, deprecation, warning): + v = ArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert isinstance(result, ValidationResult) + assert result.validated_parameters == expected + assert result.error_messages == [] + assert result._aliases == { + alias: param + for param, value in arg_spec.items() + for alias in value.get("aliases", []) + } + + if deprecation: + assert deprecation == result._deprecations[0] + else: + assert result._deprecations == [] + + if warning: + assert warning == result._warnings[0] + else: + assert result._warnings == [] + + +@pytest.mark.parametrize( + ('arg_spec', 'parameters', 'expected', 'error'), + ((i[1:]) for i in ALIAS_TEST_CASES_INVALID), + ids=[i[0] for i in ALIAS_TEST_CASES_INVALID] +) +def test_aliases_invalid(arg_spec, parameters, expected, error): + v = ArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert isinstance(result, ValidationResult) + assert error in result.error_messages + assert isinstance(result.errors.errors[0], AnsibleValidationError) + assert isinstance(result.errors, AnsibleValidationErrorMultiple) diff --git a/test/units/module_utils/common/arg_spec/test_module_validate.py b/test/units/module_utils/common/arg_spec/test_module_validate.py new file mode 100644 index 0000000..2c2211c --- /dev/null +++ b/test/units/module_utils/common/arg_spec/test_module_validate.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 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.module_utils.common import warnings + +from ansible.module_utils.common.arg_spec import ModuleArgumentSpecValidator, ValidationResult + + +def test_module_validate(): + arg_spec = {'name': {}} + parameters = {'name': 'larry'} + expected = {'name': 'larry'} + + v = ModuleArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert isinstance(result, ValidationResult) + assert result.error_messages == [] + assert result._deprecations == [] + assert result._warnings == [] + assert result.validated_parameters == expected + + +def test_module_alias_deprecations_warnings(monkeypatch): + monkeypatch.setattr(warnings, '_global_deprecations', []) + + arg_spec = { + 'path': { + 'aliases': ['source', 'src', 'flamethrower'], + 'deprecated_aliases': [{'name': 'flamethrower', 'date': '2020-03-04'}], + }, + } + parameters = {'flamethrower': '/tmp', 'source': '/tmp'} + expected = { + 'path': '/tmp', + 'flamethrower': '/tmp', + 'source': '/tmp', + } + + v = ModuleArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert result.validated_parameters == expected + assert result._deprecations == [ + { + 'collection_name': None, + 'date': '2020-03-04', + 'msg': "Alias 'flamethrower' is deprecated. See the module docs for more information", + 'version': None, + } + ] + assert "Alias 'flamethrower' is deprecated" in warnings._global_deprecations[0]['msg'] + assert result._warnings == [{'alias': 'flamethrower', 'option': 'path'}] + assert "Both option path and its alias flamethrower are set" in warnings._global_warnings[0] diff --git a/test/units/module_utils/common/arg_spec/test_sub_spec.py b/test/units/module_utils/common/arg_spec/test_sub_spec.py new file mode 100644 index 0000000..a6e7575 --- /dev/null +++ b/test/units/module_utils/common/arg_spec/test_sub_spec.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 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.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult + + +def test_sub_spec(): + arg_spec = { + 'state': {}, + 'user': { + 'type': 'dict', + 'options': { + 'first': {'no_log': True}, + 'last': {}, + 'age': {'type': 'int'}, + } + } + } + + parameters = { + 'state': 'present', + 'user': { + 'first': 'Rey', + 'last': 'Skywalker', + 'age': '19', + } + } + + expected = { + 'state': 'present', + 'user': { + 'first': 'Rey', + 'last': 'Skywalker', + 'age': 19, + } + } + + v = ArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert isinstance(result, ValidationResult) + assert result.validated_parameters == expected + assert result.error_messages == [] + + +def test_nested_sub_spec(): + arg_spec = { + 'type': {}, + 'car': { + 'type': 'dict', + 'options': { + 'make': {}, + 'model': {}, + 'customizations': { + 'type': 'dict', + 'options': { + 'engine': {}, + 'transmission': {}, + 'color': {}, + 'max_rpm': {'type': 'int'}, + } + } + } + } + } + + parameters = { + 'type': 'endurance', + 'car': { + 'make': 'Ford', + 'model': 'GT-40', + 'customizations': { + 'engine': '7.0 L', + 'transmission': '5-speed', + 'color': 'Ford blue', + 'max_rpm': '6000', + } + + } + } + + expected = { + 'type': 'endurance', + 'car': { + 'make': 'Ford', + 'model': 'GT-40', + 'customizations': { + 'engine': '7.0 L', + 'transmission': '5-speed', + 'color': 'Ford blue', + 'max_rpm': 6000, + } + + } + } + + v = ArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert isinstance(result, ValidationResult) + assert result.validated_parameters == expected + assert result.error_messages == [] diff --git a/test/units/module_utils/common/arg_spec/test_validate_invalid.py b/test/units/module_utils/common/arg_spec/test_validate_invalid.py new file mode 100644 index 0000000..7302e8a --- /dev/null +++ b/test/units/module_utils/common/arg_spec/test_validate_invalid.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 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.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult +from ansible.module_utils.errors import AnsibleValidationErrorMultiple +from ansible.module_utils.six import PY2 + + +# Each item is id, argument_spec, parameters, expected, unsupported parameters, error test string +INVALID_SPECS = [ + ( + 'invalid-list', + {'packages': {'type': 'list'}}, + {'packages': {'key': 'value'}}, + {'packages': {'key': 'value'}}, + set(), + "unable to convert to list: <class 'dict'> cannot be converted to a list", + ), + ( + 'invalid-dict', + {'users': {'type': 'dict'}}, + {'users': ['one', 'two']}, + {'users': ['one', 'two']}, + set(), + "unable to convert to dict: <class 'list'> cannot be converted to a dict", + ), + ( + 'invalid-bool', + {'bool': {'type': 'bool'}}, + {'bool': {'k': 'v'}}, + {'bool': {'k': 'v'}}, + set(), + "unable to convert to bool: <class 'dict'> cannot be converted to a bool", + ), + ( + 'invalid-float', + {'float': {'type': 'float'}}, + {'float': 'hello'}, + {'float': 'hello'}, + set(), + "unable to convert to float: <class 'str'> cannot be converted to a float", + ), + ( + 'invalid-bytes', + {'bytes': {'type': 'bytes'}}, + {'bytes': 'one'}, + {'bytes': 'one'}, + set(), + "unable to convert to bytes: <class 'str'> cannot be converted to a Byte value", + ), + ( + 'invalid-bits', + {'bits': {'type': 'bits'}}, + {'bits': 'one'}, + {'bits': 'one'}, + set(), + "unable to convert to bits: <class 'str'> cannot be converted to a Bit value", + ), + ( + 'invalid-jsonargs', + {'some_json': {'type': 'jsonarg'}}, + {'some_json': set()}, + {'some_json': set()}, + set(), + "unable to convert to jsonarg: <class 'set'> cannot be converted to a json string", + ), + ( + 'invalid-parameter', + {'name': {}}, + { + 'badparam': '', + 'another': '', + }, + { + 'name': None, + 'badparam': '', + 'another': '', + }, + set(('another', 'badparam')), + "another, badparam. Supported parameters include: name.", + ), + ( + 'invalid-elements', + {'numbers': {'type': 'list', 'elements': 'int'}}, + {'numbers': [55, 33, 34, {'key': 'value'}]}, + {'numbers': [55, 33, 34]}, + set(), + "Elements value for option 'numbers' is of type <class 'dict'> and we were unable to convert to int: <class 'dict'> cannot be converted to an int" + ), + ( + 'required', + {'req': {'required': True}}, + {}, + {'req': None}, + set(), + "missing required arguments: req" + ), + ( + 'blank_values', + {'ch_param': {'elements': 'str', 'type': 'list', 'choices': ['a', 'b']}}, + {'ch_param': ['']}, + {'ch_param': ['']}, + set(), + "value of ch_param must be one or more of" + ) +] + + +@pytest.mark.parametrize( + ('arg_spec', 'parameters', 'expected', 'unsupported', 'error'), + (i[1:] for i in INVALID_SPECS), + ids=[i[0] for i in INVALID_SPECS] +) +def test_invalid_spec(arg_spec, parameters, expected, unsupported, error): + v = ArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + with pytest.raises(AnsibleValidationErrorMultiple) as exc_info: + raise result.errors + + if PY2: + error = error.replace('class', 'type') + + assert isinstance(result, ValidationResult) + assert error in exc_info.value.msg + assert error in result.error_messages[0] + assert result.unsupported_parameters == unsupported + assert result.validated_parameters == expected diff --git a/test/units/module_utils/common/arg_spec/test_validate_valid.py b/test/units/module_utils/common/arg_spec/test_validate_valid.py new file mode 100644 index 0000000..7e41127 --- /dev/null +++ b/test/units/module_utils/common/arg_spec/test_validate_valid.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 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.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult + +# Each item is id, argument_spec, parameters, expected, valid parameter names +VALID_SPECS = [ + ( + 'str-no-type-specified', + {'name': {}}, + {'name': 'rey'}, + {'name': 'rey'}, + set(('name',)), + ), + ( + 'str', + {'name': {'type': 'str'}}, + {'name': 'rey'}, + {'name': 'rey'}, + set(('name',)), + ), + ( + 'str-convert', + {'name': {'type': 'str'}}, + {'name': 5}, + {'name': '5'}, + set(('name',)), + ), + ( + 'list', + {'packages': {'type': 'list'}}, + {'packages': ['vim', 'python']}, + {'packages': ['vim', 'python']}, + set(('packages',)), + ), + ( + 'list-comma-string', + {'packages': {'type': 'list'}}, + {'packages': 'vim,python'}, + {'packages': ['vim', 'python']}, + set(('packages',)), + ), + ( + 'list-comma-string-space', + {'packages': {'type': 'list'}}, + {'packages': 'vim, python'}, + {'packages': ['vim', ' python']}, + set(('packages',)), + ), + ( + 'dict', + {'user': {'type': 'dict'}}, + { + 'user': + { + 'first': 'rey', + 'last': 'skywalker', + } + }, + { + 'user': + { + 'first': 'rey', + 'last': 'skywalker', + } + }, + set(('user',)), + ), + ( + 'dict-k=v', + {'user': {'type': 'dict'}}, + {'user': 'first=rey,last=skywalker'}, + { + 'user': + { + 'first': 'rey', + 'last': 'skywalker', + } + }, + set(('user',)), + ), + ( + 'dict-k=v-spaces', + {'user': {'type': 'dict'}}, + {'user': 'first=rey, last=skywalker'}, + { + 'user': + { + 'first': 'rey', + 'last': 'skywalker', + } + }, + set(('user',)), + ), + ( + 'bool', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': True, + 'disabled': False, + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-ints', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': 1, + 'disabled': 0, + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-true-false', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': 'true', + 'disabled': 'false', + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-yes-no', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': 'yes', + 'disabled': 'no', + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-y-n', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': 'y', + 'disabled': 'n', + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-on-off', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': 'on', + 'disabled': 'off', + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-1-0', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': '1', + 'disabled': '0', + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'bool-float', + { + 'enabled': {'type': 'bool'}, + 'disabled': {'type': 'bool'}, + }, + { + 'enabled': 1.0, + 'disabled': 0.0, + }, + { + 'enabled': True, + 'disabled': False, + }, + set(('enabled', 'disabled')), + ), + ( + 'float', + {'digit': {'type': 'float'}}, + {'digit': 3.14159}, + {'digit': 3.14159}, + set(('digit',)), + ), + ( + 'float-str', + {'digit': {'type': 'float'}}, + {'digit': '3.14159'}, + {'digit': 3.14159}, + set(('digit',)), + ), + ( + 'path', + {'path': {'type': 'path'}}, + {'path': '~/bin'}, + {'path': '/home/ansible/bin'}, + set(('path',)), + ), + ( + 'raw', + {'raw': {'type': 'raw'}}, + {'raw': 0x644}, + {'raw': 0x644}, + set(('raw',)), + ), + ( + 'bytes', + {'bytes': {'type': 'bytes'}}, + {'bytes': '2K'}, + {'bytes': 2048}, + set(('bytes',)), + ), + ( + 'bits', + {'bits': {'type': 'bits'}}, + {'bits': '1Mb'}, + {'bits': 1048576}, + set(('bits',)), + ), + ( + 'jsonarg', + {'some_json': {'type': 'jsonarg'}}, + {'some_json': '{"users": {"bob": {"role": "accountant"}}}'}, + {'some_json': '{"users": {"bob": {"role": "accountant"}}}'}, + set(('some_json',)), + ), + ( + 'jsonarg-list', + {'some_json': {'type': 'jsonarg'}}, + {'some_json': ['one', 'two']}, + {'some_json': '["one", "two"]'}, + set(('some_json',)), + ), + ( + 'jsonarg-dict', + {'some_json': {'type': 'jsonarg'}}, + {'some_json': {"users": {"bob": {"role": "accountant"}}}}, + {'some_json': '{"users": {"bob": {"role": "accountant"}}}'}, + set(('some_json',)), + ), + ( + 'defaults', + {'param': {'default': 'DEFAULT'}}, + {}, + {'param': 'DEFAULT'}, + set(('param',)), + ), + ( + 'elements', + {'numbers': {'type': 'list', 'elements': 'int'}}, + {'numbers': [55, 33, 34, '22']}, + {'numbers': [55, 33, 34, 22]}, + set(('numbers',)), + ), + ( + 'aliases', + {'src': {'aliases': ['path', 'source']}}, + {'src': '/tmp'}, + {'src': '/tmp'}, + set(('src (path, source)',)), + ) +] + + +@pytest.mark.parametrize( + ('arg_spec', 'parameters', 'expected', 'valid_params'), + (i[1:] for i in VALID_SPECS), + ids=[i[0] for i in VALID_SPECS] +) +def test_valid_spec(arg_spec, parameters, expected, valid_params, mocker): + mocker.patch('ansible.module_utils.common.validation.os.path.expanduser', return_value='/home/ansible/bin') + mocker.patch('ansible.module_utils.common.validation.os.path.expandvars', return_value='/home/ansible/bin') + + v = ArgumentSpecValidator(arg_spec) + result = v.validate(parameters) + + assert isinstance(result, ValidationResult) + assert result.validated_parameters == expected + assert result.unsupported_parameters == set() + assert result.error_messages == [] + assert v._valid_parameter_names == valid_params + + # Again to check caching + assert v._valid_parameter_names == valid_params diff --git a/test/units/module_utils/common/parameters/test_check_arguments.py b/test/units/module_utils/common/parameters/test_check_arguments.py new file mode 100644 index 0000000..5311217 --- /dev/null +++ b/test/units/module_utils/common/parameters/test_check_arguments.py @@ -0,0 +1,38 @@ +# -*- 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.module_utils.common.parameters import _get_unsupported_parameters + + +@pytest.fixture +def argument_spec(): + return { + 'state': {'aliases': ['status']}, + 'enabled': {}, + } + + +@pytest.mark.parametrize( + ('module_parameters', 'legal_inputs', 'expected'), + ( + ({'fish': 'food'}, ['state', 'enabled'], set(['fish'])), + ({'state': 'enabled', 'path': '/var/lib/path'}, None, set(['path'])), + ({'state': 'enabled', 'path': '/var/lib/path'}, ['state', 'path'], set()), + ({'state': 'enabled', 'path': '/var/lib/path'}, ['state'], set(['path'])), + ({}, None, set()), + ({'state': 'enabled'}, None, set()), + ({'status': 'enabled', 'enabled': True, 'path': '/var/lib/path'}, None, set(['path'])), + ({'status': 'enabled', 'enabled': True}, None, set()), + ) +) +def test_check_arguments(argument_spec, module_parameters, legal_inputs, expected, mocker): + result = _get_unsupported_parameters(argument_spec, module_parameters, legal_inputs) + + assert result == expected diff --git a/test/units/module_utils/common/parameters/test_handle_aliases.py b/test/units/module_utils/common/parameters/test_handle_aliases.py new file mode 100644 index 0000000..e20a888 --- /dev/null +++ b/test/units/module_utils/common/parameters/test_handle_aliases.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.parameters import _handle_aliases +from ansible.module_utils._text import to_native + + +def test_handle_aliases_no_aliases(): + argument_spec = { + 'name': {'type': 'str'}, + } + + params = { + 'name': 'foo', + 'path': 'bar' + } + + expected = {} + result = _handle_aliases(argument_spec, params) + + assert expected == result + + +def test_handle_aliases_basic(): + argument_spec = { + 'name': {'type': 'str', 'aliases': ['surname', 'nick']}, + } + + params = { + 'name': 'foo', + 'path': 'bar', + 'surname': 'foo', + 'nick': 'foo', + } + + expected = {'surname': 'name', 'nick': 'name'} + result = _handle_aliases(argument_spec, params) + + assert expected == result + + +def test_handle_aliases_value_error(): + argument_spec = { + 'name': {'type': 'str', 'aliases': ['surname', 'nick'], 'default': 'bob', 'required': True}, + } + + params = { + 'name': 'foo', + } + + with pytest.raises(ValueError) as ve: + _handle_aliases(argument_spec, params) + assert 'internal error: aliases must be a list or tuple' == to_native(ve.error) + + +def test_handle_aliases_type_error(): + argument_spec = { + 'name': {'type': 'str', 'aliases': 'surname'}, + } + + params = { + 'name': 'foo', + } + + with pytest.raises(TypeError) as te: + _handle_aliases(argument_spec, params) + assert 'internal error: required and default are mutually exclusive' in to_native(te.error) diff --git a/test/units/module_utils/common/parameters/test_list_deprecations.py b/test/units/module_utils/common/parameters/test_list_deprecations.py new file mode 100644 index 0000000..6f0bb71 --- /dev/null +++ b/test/units/module_utils/common/parameters/test_list_deprecations.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.parameters import _list_deprecations + + +@pytest.fixture +def params(): + return { + 'name': 'bob', + 'dest': '/etc/hosts', + 'state': 'present', + 'value': 5, + } + + +def test_list_deprecations(): + argument_spec = { + 'old': {'type': 'str', 'removed_in_version': '2.5'}, + 'foo': {'type': 'dict', 'options': {'old': {'type': 'str', 'removed_in_version': 1.0}}}, + 'bar': {'type': 'list', 'elements': 'dict', 'options': {'old': {'type': 'str', 'removed_in_version': '2.10'}}}, + } + + params = { + 'name': 'rod', + 'old': 'option', + 'foo': {'old': 'value'}, + 'bar': [{'old': 'value'}, {}], + } + result = _list_deprecations(argument_spec, params) + assert len(result) == 3 + result.sort(key=lambda entry: entry['msg']) + assert result[0]['msg'] == """Param 'bar["old"]' is deprecated. See the module docs for more information""" + assert result[0]['version'] == '2.10' + assert result[1]['msg'] == """Param 'foo["old"]' is deprecated. See the module docs for more information""" + assert result[1]['version'] == 1.0 + assert result[2]['msg'] == "Param 'old' is deprecated. See the module docs for more information" + assert result[2]['version'] == '2.5' diff --git a/test/units/module_utils/common/parameters/test_list_no_log_values.py b/test/units/module_utils/common/parameters/test_list_no_log_values.py new file mode 100644 index 0000000..ac0e735 --- /dev/null +++ b/test/units/module_utils/common/parameters/test_list_no_log_values.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.parameters import _list_no_log_values + + +@pytest.fixture +def argument_spec(): + # Allow extra specs to be passed to the fixture, which will be added to the output + def _argument_spec(extra_opts=None): + spec = { + 'secret': {'type': 'str', 'no_log': True}, + 'other_secret': {'type': 'str', 'no_log': True}, + 'state': {'type': 'str'}, + 'value': {'type': 'int'}, + } + + if extra_opts: + spec.update(extra_opts) + + return spec + + return _argument_spec + + +@pytest.fixture +def module_parameters(): + # Allow extra parameters to be passed to the fixture, which will be added to the output + def _module_parameters(extra_params=None): + params = { + 'secret': 'under', + 'other_secret': 'makeshift', + 'state': 'present', + 'value': 5, + } + + if extra_params: + params.update(extra_params) + + return params + + return _module_parameters + + +def test_list_no_log_values_no_secrets(module_parameters): + argument_spec = { + 'other_secret': {'type': 'str', 'no_log': False}, + 'state': {'type': 'str'}, + 'value': {'type': 'int'}, + } + expected = set() + assert expected == _list_no_log_values(argument_spec, module_parameters) + + +def test_list_no_log_values(argument_spec, module_parameters): + expected = set(('under', 'makeshift')) + assert expected == _list_no_log_values(argument_spec(), module_parameters()) + + +@pytest.mark.parametrize('extra_params', [ + {'subopt1': 1}, + {'subopt1': 3.14159}, + {'subopt1': ['one', 'two']}, + {'subopt1': ('one', 'two')}, +]) +def test_list_no_log_values_invalid_suboptions(argument_spec, module_parameters, extra_params): + extra_opts = { + 'subopt1': { + 'type': 'dict', + 'options': { + 'sub_1_1': {}, + } + } + } + + with pytest.raises(TypeError, match=r"(Value '.*?' in the sub parameter field '.*?' must by a dict, not '.*?')" + r"|(dictionary requested, could not parse JSON or key=value)"): + _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_suboptions(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {'type': 'list'}, + } + } + } + + extra_params = { + 'subopt1': { + 'sub_1_1': 'bagel', + 'sub_1_2': ['pebble'], + } + } + + expected = set(('under', 'makeshift', 'bagel')) + assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_sub_suboptions(argument_spec, module_parameters): + extra_opts = { + 'sub_level_1': { + 'type': 'dict', + 'options': { + 'l1_1': {'no_log': True}, + 'l1_2': {}, + 'l1_3': { + 'type': 'dict', + 'options': { + 'l2_1': {'no_log': True}, + 'l2_2': {}, + } + } + } + } + } + + extra_params = { + 'sub_level_1': { + 'l1_1': 'saucy', + 'l1_2': 'napped', + 'l1_3': { + 'l2_1': 'corporate', + 'l2_2': 'tinsmith', + } + } + } + + expected = set(('under', 'makeshift', 'saucy', 'corporate')) + assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_suboptions_list(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {}, + } + } + } + + extra_params = { + 'subopt1': [ + { + 'sub_1_1': ['playroom', 'luxury'], + 'sub_1_2': 'deuce', + }, + { + 'sub_1_2': ['squishier', 'finished'], + } + ] + } + + expected = set(('under', 'makeshift', 'playroom', 'luxury')) + assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_sub_suboptions_list(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {}, + 'subopt2': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_2_1': {'no_log': True, 'type': 'list'}, + 'sub_2_2': {}, + } + } + } + } + } + + extra_params = { + 'subopt1': { + 'sub_1_1': ['playroom', 'luxury'], + 'sub_1_2': 'deuce', + 'subopt2': [ + { + 'sub_2_1': ['basis', 'gave'], + 'sub_2_2': 'liquid', + }, + { + 'sub_2_1': ['composure', 'thumping'] + }, + ] + } + } + + expected = set(('under', 'makeshift', 'playroom', 'luxury', 'basis', 'gave', 'composure', 'thumping')) + assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +@pytest.mark.parametrize('extra_params, expected', ( + ({'subopt_dict': 'dict_subopt1=rekindle-scandal,dict_subopt2=subgroupavenge'}, ('rekindle-scandal',)), + ({'subopt_dict': 'dict_subopt1=aversion-mutable dict_subopt2=subgroupavenge'}, ('aversion-mutable',)), + ({'subopt_dict': ['dict_subopt1=blip-marine,dict_subopt2=subgroupavenge', 'dict_subopt1=tipping,dict_subopt2=hardening']}, ('blip-marine', 'tipping')), +)) +def test_string_suboptions_as_string(argument_spec, module_parameters, extra_params, expected): + extra_opts = { + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'dict_subopt1': {'no_log': True}, + 'dict_subopt2': {}, + }, + }, + } + + result = set(('under', 'makeshift')) + result.update(expected) + assert result == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) diff --git a/test/units/module_utils/common/process/test_get_bin_path.py b/test/units/module_utils/common/process/test_get_bin_path.py new file mode 100644 index 0000000..7c0bd0a --- /dev/null +++ b/test/units/module_utils/common/process/test_get_bin_path.py @@ -0,0 +1,39 @@ +# -*- 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.module_utils.common.process import get_bin_path + + +def test_get_bin_path(mocker): + path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + mocker.patch.dict('os.environ', {'PATH': path}) + mocker.patch('os.pathsep', ':') + + mocker.patch('os.path.isdir', return_value=False) + mocker.patch('ansible.module_utils.common.process.is_executable', return_value=True) + + # pytest-mock 2.0.0 will throw when os.path.exists is messed with + # and then another method is patched afterwards. Likely + # something in the pytest-mock chain uses os.path.exists internally, and + # since pytest-mock prohibits context-specific patching, there's not a + # good solution. For now, just patch os.path.exists last. + mocker.patch('os.path.exists', side_effect=[False, True]) + + assert '/usr/local/bin/notacommand' == get_bin_path('notacommand') + + +def test_get_path_path_raise_valueerror(mocker): + mocker.patch.dict('os.environ', {'PATH': ''}) + + mocker.patch('os.path.exists', return_value=False) + mocker.patch('os.path.isdir', return_value=False) + mocker.patch('ansible.module_utils.common.process.is_executable', return_value=True) + + with pytest.raises(ValueError, match='Failed to find required executable "notacommand"'): + get_bin_path('notacommand') diff --git a/test/units/module_utils/common/test_collections.py b/test/units/module_utils/common/test_collections.py new file mode 100644 index 0000000..95b2a40 --- /dev/null +++ b/test/units/module_utils/common/test_collections.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018–2019, Sviatoslav Sydorenko <webknjaz@redhat.com> +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) +"""Test low-level utility functions from ``module_utils.common.collections``.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible.module_utils.six import Iterator +from ansible.module_utils.common._collections_compat import Sequence +from ansible.module_utils.common.collections import ImmutableDict, is_iterable, is_sequence + + +class SeqStub: + """Stub emulating a sequence type. + + >>> from collections.abc import Sequence + >>> assert issubclass(SeqStub, Sequence) + >>> assert isinstance(SeqStub(), Sequence) + """ + + +Sequence.register(SeqStub) + + +class IteratorStub(Iterator): + def __next__(self): + raise StopIteration + + +class IterableStub: + def __iter__(self): + return IteratorStub() + + +class FakeAnsibleVaultEncryptedUnicode(Sequence): + __ENCRYPTED__ = True + + def __init__(self, data): + self.data = data + + def __getitem__(self, index): + return self.data[index] + + def __len__(self): + return len(self.data) + + +TEST_STRINGS = u'he', u'Україна', u'Česká republika' +TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS) + (FakeAnsibleVaultEncryptedUnicode(u'foo'),) + +TEST_ITEMS_NON_SEQUENCES = ( + {}, object(), frozenset(), + 4, 0., +) + TEST_STRINGS + +TEST_ITEMS_SEQUENCES = ( + [], (), + SeqStub(), +) +TEST_ITEMS_SEQUENCES = TEST_ITEMS_SEQUENCES + ( + # Iterable effectively containing nested random data: + TEST_ITEMS_NON_SEQUENCES, +) + + +@pytest.mark.parametrize('sequence_input', TEST_ITEMS_SEQUENCES) +def test_sequence_positive(sequence_input): + """Test that non-string item sequences are identified correctly.""" + assert is_sequence(sequence_input) + assert is_sequence(sequence_input, include_strings=False) + + +@pytest.mark.parametrize('non_sequence_input', TEST_ITEMS_NON_SEQUENCES) +def test_sequence_negative(non_sequence_input): + """Test that non-sequences are identified correctly.""" + assert not is_sequence(non_sequence_input) + + +@pytest.mark.parametrize('string_input', TEST_STRINGS) +def test_sequence_string_types_with_strings(string_input): + """Test that ``is_sequence`` can separate string and non-string.""" + assert is_sequence(string_input, include_strings=True) + + +@pytest.mark.parametrize('string_input', TEST_STRINGS) +def test_sequence_string_types_without_strings(string_input): + """Test that ``is_sequence`` can separate string and non-string.""" + assert not is_sequence(string_input, include_strings=False) + + +@pytest.mark.parametrize( + 'seq', + ([], (), {}, set(), frozenset(), IterableStub()), +) +def test_iterable_positive(seq): + assert is_iterable(seq) + + +@pytest.mark.parametrize( + 'seq', (IteratorStub(), object(), 5, 9.) +) +def test_iterable_negative(seq): + assert not is_iterable(seq) + + +@pytest.mark.parametrize('string_input', TEST_STRINGS) +def test_iterable_including_strings(string_input): + assert is_iterable(string_input, include_strings=True) + + +@pytest.mark.parametrize('string_input', TEST_STRINGS) +def test_iterable_excluding_strings(string_input): + assert not is_iterable(string_input, include_strings=False) + + +class TestImmutableDict: + def test_scalar(self): + imdict = ImmutableDict({1: 2}) + assert imdict[1] == 2 + + def test_string(self): + imdict = ImmutableDict({u'café': u'くらとみ'}) + assert imdict[u'café'] == u'くらとみ' + + def test_container(self): + imdict = ImmutableDict({(1, 2): ['1', '2']}) + assert imdict[(1, 2)] == ['1', '2'] + + def test_from_tuples(self): + imdict = ImmutableDict((('a', 1), ('b', 2))) + assert frozenset(imdict.items()) == frozenset((('a', 1), ('b', 2))) + + def test_from_kwargs(self): + imdict = ImmutableDict(a=1, b=2) + assert frozenset(imdict.items()) == frozenset((('a', 1), ('b', 2))) + + def test_immutable(self): + imdict = ImmutableDict({1: 2}) + + expected_reason = r"^'ImmutableDict' object does not support item assignment$" + + with pytest.raises(TypeError, match=expected_reason): + imdict[1] = 3 + + with pytest.raises(TypeError, match=expected_reason): + imdict[5] = 3 + + def test_hashable(self): + # ImmutableDict is hashable when all of its values are hashable + imdict = ImmutableDict({u'café': u'くらとみ'}) + assert hash(imdict) + + def test_nonhashable(self): + # ImmutableDict is unhashable when one of its values is unhashable + imdict = ImmutableDict({u'café': u'くらとみ', 1: [1, 2]}) + + expected_reason = r"^unhashable type: 'list'$" + + with pytest.raises(TypeError, match=expected_reason): + hash(imdict) + + def test_len(self): + imdict = ImmutableDict({1: 2, 'a': 'b'}) + assert len(imdict) == 2 + + def test_repr(self): + initial_data = {1: 2, 'a': 'b'} + initial_data_repr = repr(initial_data) + imdict = ImmutableDict(initial_data) + actual_repr = repr(imdict) + expected_repr = "ImmutableDict({0})".format(initial_data_repr) + assert actual_repr == expected_repr diff --git a/test/units/module_utils/common/test_dict_transformations.py b/test/units/module_utils/common/test_dict_transformations.py new file mode 100644 index 0000000..ba55299 --- /dev/null +++ b/test/units/module_utils/common/test_dict_transformations.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2017, Will Thames <will.thames@xvt.com.au> +# 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.module_utils.common.dict_transformations import ( + _camel_to_snake, + _snake_to_camel, + camel_dict_to_snake_dict, + dict_merge, + recursive_diff, +) + + +EXPECTED_SNAKIFICATION = { + 'alllower': 'alllower', + 'TwoWords': 'two_words', + 'AllUpperAtEND': 'all_upper_at_end', + 'AllUpperButPLURALs': 'all_upper_but_plurals', + 'TargetGroupARNs': 'target_group_arns', + 'HTTPEndpoints': 'http_endpoints', + 'PLURALs': 'plurals' +} + +EXPECTED_REVERSIBLE = { + 'TwoWords': 'two_words', + 'AllUpperAtEND': 'all_upper_at_e_n_d', + 'AllUpperButPLURALs': 'all_upper_but_p_l_u_r_a_ls', + 'TargetGroupARNs': 'target_group_a_r_ns', + 'HTTPEndpoints': 'h_t_t_p_endpoints', + 'PLURALs': 'p_l_u_r_a_ls' +} + + +class TestCaseCamelToSnake: + + def test_camel_to_snake(self): + for (k, v) in EXPECTED_SNAKIFICATION.items(): + assert _camel_to_snake(k) == v + + def test_reversible_camel_to_snake(self): + for (k, v) in EXPECTED_REVERSIBLE.items(): + assert _camel_to_snake(k, reversible=True) == v + + +class TestCaseSnakeToCamel: + + def test_snake_to_camel_reversed(self): + for (k, v) in EXPECTED_REVERSIBLE.items(): + assert _snake_to_camel(v, capitalize_first=True) == k + + +class TestCaseCamelToSnakeAndBack: + def test_camel_to_snake_and_back(self): + for (k, v) in EXPECTED_REVERSIBLE.items(): + assert _snake_to_camel(_camel_to_snake(k, reversible=True), capitalize_first=True) == k + + +class TestCaseCamelDictToSnakeDict: + def test_ignore_list(self): + camel_dict = dict(Hello=dict(One='one', Two='two'), World=dict(Three='three', Four='four')) + snake_dict = camel_dict_to_snake_dict(camel_dict, ignore_list='World') + assert snake_dict['hello'] == dict(one='one', two='two') + assert snake_dict['world'] == dict(Three='three', Four='four') + + +class TestCaseDictMerge: + def test_dict_merge(self): + base = dict(obj2=dict(), b1=True, b2=False, b3=False, + one=1, two=2, three=3, obj1=dict(key1=1, key2=2), + l1=[1, 3], l2=[1, 2, 3], l4=[4], + nested=dict(n1=dict(n2=2))) + + other = dict(b1=True, b2=False, b3=True, b4=True, + one=1, three=4, four=4, obj1=dict(key1=2), + l1=[2, 1], l2=[3, 2, 1], l3=[1], + nested=dict(n1=dict(n2=2, n3=3))) + + result = dict_merge(base, other) + + # string assertions + assert 'one' in result + assert 'two' in result + assert result['three'] == 4 + assert result['four'] == 4 + + # dict assertions + assert 'obj1' in result + assert 'key1' in result['obj1'] + assert 'key2' in result['obj1'] + + # list assertions + # this line differs from the network_utils/common test of the function of the + # same name as this method does not merge lists + assert result['l1'], [2, 1] + assert 'l2' in result + assert result['l3'], [1] + assert 'l4' in result + + # nested assertions + assert 'obj1' in result + assert result['obj1']['key1'], 2 + assert 'key2' in result['obj1'] + + # bool assertions + assert 'b1' in result + assert 'b2' in result + assert result['b3'] + assert result['b4'] + + +class TestCaseAzureIncidental: + + def test_dict_merge_invalid_dict(self): + ''' if b is not a dict, return b ''' + res = dict_merge({}, None) + assert res is None + + def test_merge_sub_dicts(self): + '''merge sub dicts ''' + a = {'a': {'a1': 1}} + b = {'a': {'b1': 2}} + c = {'a': {'a1': 1, 'b1': 2}} + res = dict_merge(a, b) + assert res == c + + +class TestCaseRecursiveDiff: + def test_recursive_diff(self): + a = {'foo': {'bar': [{'baz': {'qux': 'ham_sandwich'}}]}} + c = {'foo': {'bar': [{'baz': {'qux': 'ham_sandwich'}}]}} + b = {'foo': {'bar': [{'baz': {'qux': 'turkey_sandwich'}}]}} + + assert recursive_diff(a, b) is not None + assert len(recursive_diff(a, b)) == 2 + assert recursive_diff(a, c) is None + + @pytest.mark.parametrize( + 'p1, p2', ( + ([1, 2], [2, 3]), + ({1: 2}, [2, 3]), + ([1, 2], {2: 3}), + ({2: 3}, 'notadict'), + ('notadict', {2: 3}), + ) + ) + def test_recursive_diff_negative(self, p1, p2): + with pytest.raises(TypeError, match="Unable to diff"): + recursive_diff(p1, p2) diff --git a/test/units/module_utils/common/test_locale.py b/test/units/module_utils/common/test_locale.py new file mode 100644 index 0000000..9d95986 --- /dev/null +++ b/test/units/module_utils/common/test_locale.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# (c) 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 units.compat.mock import MagicMock + +from ansible.module_utils.common.locale import get_best_parsable_locale + + +class TestLocale: + """Tests for get_best_paresable_locale""" + + mock_module = MagicMock() + mock_module.get_bin_path = MagicMock(return_value='/usr/bin/locale') + + def test_finding_best(self): + self.mock_module.run_command = MagicMock(return_value=(0, "C.utf8\nen_US.utf8\nC\nPOSIX\n", '')) + locale = get_best_parsable_locale(self.mock_module) + assert locale == 'C.utf8' + + def test_finding_last(self): + self.mock_module.run_command = MagicMock(return_value=(0, "fr_FR.utf8\nen_UK.utf8\nC\nPOSIX\n", '')) + locale = get_best_parsable_locale(self.mock_module) + assert locale == 'C' + + def test_finding_middle(self): + self.mock_module.run_command = MagicMock(return_value=(0, "fr_FR.utf8\nen_US.utf8\nC\nPOSIX\n", '')) + locale = get_best_parsable_locale(self.mock_module) + assert locale == 'en_US.utf8' + + def test_finding_prefered(self): + self.mock_module.run_command = MagicMock(return_value=(0, "es_ES.utf8\nMINE\nC\nPOSIX\n", '')) + locale = get_best_parsable_locale(self.mock_module, preferences=['MINE', 'C.utf8']) + assert locale == 'MINE' + + def test_finding_C_on_no_match(self): + self.mock_module.run_command = MagicMock(return_value=(0, "fr_FR.UTF8\nMINE\n", '')) + locale = get_best_parsable_locale(self.mock_module) + assert locale == 'C' diff --git a/test/units/module_utils/common/test_network.py b/test/units/module_utils/common/test_network.py new file mode 100644 index 0000000..27d9503 --- /dev/null +++ b/test/units/module_utils/common/test_network.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# (c) 2017 Red Hat, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible.module_utils.common.network import ( + to_bits, + to_masklen, + to_netmask, + to_subnet, + to_ipv6_network, + is_masklen, + is_netmask +) + + +def test_to_masklen(): + assert 24 == to_masklen('255.255.255.0') + + +def test_to_masklen_invalid(): + with pytest.raises(ValueError): + to_masklen('255') + + +def test_to_netmask(): + assert '255.0.0.0' == to_netmask(8) + assert '255.0.0.0' == to_netmask('8') + + +def test_to_netmask_invalid(): + with pytest.raises(ValueError): + to_netmask(128) + + +def test_to_subnet(): + result = to_subnet('192.168.1.1', 24) + assert '192.168.1.0/24' == result + + result = to_subnet('192.168.1.1', 24, dotted_notation=True) + assert '192.168.1.0 255.255.255.0' == result + + +def test_to_subnet_invalid(): + with pytest.raises(ValueError): + to_subnet('foo', 'bar') + + +def test_is_masklen(): + assert is_masklen(32) + assert not is_masklen(33) + assert not is_masklen('foo') + + +def test_is_netmask(): + assert is_netmask('255.255.255.255') + assert not is_netmask(24) + assert not is_netmask('foo') + + +def test_to_ipv6_network(): + assert '2001:db8::' == to_ipv6_network('2001:db8::') + assert '2001:0db8:85a3::' == to_ipv6_network('2001:0db8:85a3:0000:0000:8a2e:0370:7334') + assert '2001:0db8:85a3::' == to_ipv6_network('2001:0db8:85a3:0:0:8a2e:0370:7334') + + +def test_to_bits(): + assert to_bits('0') == '00000000' + assert to_bits('1') == '00000001' + assert to_bits('2') == '00000010' + assert to_bits('1337') == '10100111001' + assert to_bits('127.0.0.1') == '01111111000000000000000000000001' + assert to_bits('255.255.255.255') == '11111111111111111111111111111111' + assert to_bits('255.255.255.0') == '11111111111111111111111100000000' diff --git a/test/units/module_utils/common/test_sys_info.py b/test/units/module_utils/common/test_sys_info.py new file mode 100644 index 0000000..18aafe5 --- /dev/null +++ b/test/units/module_utils/common/test_sys_info.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> +# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com> +# (c) 2017-2018 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 units.compat.mock import patch + +from ansible.module_utils.six.moves import builtins + +# Functions being tested +from ansible.module_utils.common.sys_info import get_distribution +from ansible.module_utils.common.sys_info import get_distribution_version +from ansible.module_utils.common.sys_info import get_platform_subclass + + +realimport = builtins.__import__ + + +@pytest.fixture +def platform_linux(mocker): + mocker.patch('platform.system', return_value='Linux') + + +# +# get_distribution tests +# + +@pytest.mark.parametrize( + ('system', 'dist'), + ( + ('Darwin', 'Darwin'), + ('SunOS', 'Solaris'), + ('FreeBSD', 'Freebsd'), + ), +) +def test_get_distribution_not_linux(system, dist, mocker): + """For platforms other than Linux, return the distribution""" + mocker.patch('platform.system', return_value=system) + mocker.patch('ansible.module_utils.common.sys_info.distro.id', return_value=dist) + assert get_distribution() == dist + + +@pytest.mark.usefixtures("platform_linux") +class TestGetDistribution: + """Tests for get_distribution that have to find something""" + def test_distro_known(self): + with patch('ansible.module_utils.distro.id', return_value="alpine"): + assert get_distribution() == "Alpine" + + with patch('ansible.module_utils.distro.id', return_value="arch"): + assert get_distribution() == "Arch" + + with patch('ansible.module_utils.distro.id', return_value="centos"): + assert get_distribution() == "Centos" + + with patch('ansible.module_utils.distro.id', return_value="clear-linux-os"): + assert get_distribution() == "Clear-linux-os" + + with patch('ansible.module_utils.distro.id', return_value="coreos"): + assert get_distribution() == "Coreos" + + with patch('ansible.module_utils.distro.id', return_value="debian"): + assert get_distribution() == "Debian" + + with patch('ansible.module_utils.distro.id', return_value="flatcar"): + assert get_distribution() == "Flatcar" + + with patch('ansible.module_utils.distro.id', return_value="linuxmint"): + assert get_distribution() == "Linuxmint" + + with patch('ansible.module_utils.distro.id', return_value="opensuse"): + assert get_distribution() == "Opensuse" + + with patch('ansible.module_utils.distro.id', return_value="oracle"): + assert get_distribution() == "Oracle" + + with patch('ansible.module_utils.distro.id', return_value="raspian"): + assert get_distribution() == "Raspian" + + with patch('ansible.module_utils.distro.id', return_value="rhel"): + assert get_distribution() == "Redhat" + + with patch('ansible.module_utils.distro.id', return_value="ubuntu"): + assert get_distribution() == "Ubuntu" + + with patch('ansible.module_utils.distro.id', return_value="virtuozzo"): + assert get_distribution() == "Virtuozzo" + + with patch('ansible.module_utils.distro.id', return_value="foo"): + assert get_distribution() == "Foo" + + def test_distro_unknown(self): + with patch('ansible.module_utils.distro.id', return_value=""): + assert get_distribution() == "OtherLinux" + + def test_distro_amazon_linux_short(self): + with patch('ansible.module_utils.distro.id', return_value="amzn"): + assert get_distribution() == "Amazon" + + def test_distro_amazon_linux_long(self): + with patch('ansible.module_utils.distro.id', return_value="amazon"): + assert get_distribution() == "Amazon" + + +# +# get_distribution_version tests +# + +@pytest.mark.parametrize( + ('system', 'version'), + ( + ('Darwin', '19.6.0'), + ('SunOS', '11.4'), + ('FreeBSD', '12.1'), + ), +) +def test_get_distribution_version_not_linux(mocker, system, version): + """If it's not Linux, then it has no distribution""" + mocker.patch('platform.system', return_value=system) + mocker.patch('ansible.module_utils.common.sys_info.distro.version', return_value=version) + assert get_distribution_version() == version + + +@pytest.mark.usefixtures("platform_linux") +def test_distro_found(): + with patch('ansible.module_utils.distro.version', return_value="1"): + assert get_distribution_version() == "1" + + +# +# Tests for get_platform_subclass +# + +class TestGetPlatformSubclass: + class LinuxTest: + pass + + class Foo(LinuxTest): + platform = "Linux" + distribution = None + + class Bar(LinuxTest): + platform = "Linux" + distribution = "Bar" + + def test_not_linux(self): + # if neither match, the fallback should be the top-level class + with patch('platform.system', return_value="Foo"): + with patch('ansible.module_utils.common.sys_info.get_distribution', return_value=None): + assert get_platform_subclass(self.LinuxTest) is self.LinuxTest + + @pytest.mark.usefixtures("platform_linux") + def test_get_distribution_none(self): + # match just the platform class, not a specific distribution + with patch('ansible.module_utils.common.sys_info.get_distribution', return_value=None): + assert get_platform_subclass(self.LinuxTest) is self.Foo + + @pytest.mark.usefixtures("platform_linux") + def test_get_distribution_found(self): + # match both the distribution and platform class + with patch('ansible.module_utils.common.sys_info.get_distribution', return_value="Bar"): + assert get_platform_subclass(self.LinuxTest) is self.Bar diff --git a/test/units/module_utils/common/test_utils.py b/test/units/module_utils/common/test_utils.py new file mode 100644 index 0000000..ef95239 --- /dev/null +++ b/test/units/module_utils/common/test_utils.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# (c) 2018 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.module_utils.common.sys_info import get_all_subclasses + + +# +# Tests for get_all_subclasses +# + +class TestGetAllSubclasses: + class Base: + pass + + class BranchI(Base): + pass + + class BranchII(Base): + pass + + class BranchIA(BranchI): + pass + + class BranchIB(BranchI): + pass + + class BranchIIA(BranchII): + pass + + class BranchIIB(BranchII): + pass + + def test_bottom_level(self): + assert get_all_subclasses(self.BranchIIB) == set() + + def test_one_inheritance(self): + assert set(get_all_subclasses(self.BranchII)) == set([self.BranchIIA, self.BranchIIB]) + + def test_toplevel(self): + assert set(get_all_subclasses(self.Base)) == set([self.BranchI, self.BranchII, + self.BranchIA, self.BranchIB, + self.BranchIIA, self.BranchIIB]) diff --git a/test/units/module_utils/common/text/converters/test_container_to_bytes.py b/test/units/module_utils/common/text/converters/test_container_to_bytes.py new file mode 100644 index 0000000..091545e --- /dev/null +++ b/test/units/module_utils/common/text/converters/test_container_to_bytes.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible.module_utils.common.text.converters import container_to_bytes + + +DEFAULT_ENCODING = 'utf-8' +DEFAULT_ERR_HANDLER = 'surrogate_or_strict' + + +@pytest.mark.parametrize( + 'test_input,expected', + [ + ({1: 1}, {1: 1}), + ([1, 2], [1, 2]), + ((1, 2), (1, 2)), + (1, 1), + (1.1, 1.1), + (b'str', b'str'), + (u'str', b'str'), + ([u'str'], [b'str']), + ((u'str'), (b'str')), + ({u'str': u'str'}, {b'str': b'str'}), + ] +) +@pytest.mark.parametrize('encoding', ['utf-8', 'latin1', 'shift_jis', 'big5', 'koi8_r']) +@pytest.mark.parametrize('errors', ['strict', 'surrogate_or_strict', 'surrogate_then_replace']) +def test_container_to_bytes(test_input, expected, encoding, errors): + """Test for passing objects to container_to_bytes().""" + assert container_to_bytes(test_input, encoding=encoding, errors=errors) == expected + + +@pytest.mark.parametrize( + 'test_input,expected', + [ + ({1: 1}, {1: 1}), + ([1, 2], [1, 2]), + ((1, 2), (1, 2)), + (1, 1), + (1.1, 1.1), + (True, True), + (None, None), + (u'str', u'str'.encode(DEFAULT_ENCODING)), + (u'くらとみ', u'くらとみ'.encode(DEFAULT_ENCODING)), + (u'café', u'café'.encode(DEFAULT_ENCODING)), + (b'str', u'str'.encode(DEFAULT_ENCODING)), + (u'str', u'str'.encode(DEFAULT_ENCODING)), + ([u'str'], [u'str'.encode(DEFAULT_ENCODING)]), + ((u'str'), (u'str'.encode(DEFAULT_ENCODING))), + ({u'str': u'str'}, {u'str'.encode(DEFAULT_ENCODING): u'str'.encode(DEFAULT_ENCODING)}), + ] +) +def test_container_to_bytes_default_encoding_err(test_input, expected): + """ + Test for passing objects to container_to_bytes(). Default encoding and errors + """ + assert container_to_bytes(test_input, encoding=DEFAULT_ENCODING, + errors=DEFAULT_ERR_HANDLER) == expected + + +@pytest.mark.parametrize( + 'test_input,encoding', + [ + (u'くらとみ', 'latin1'), + (u'café', 'shift_jis'), + ] +) +@pytest.mark.parametrize('errors', ['surrogate_or_strict', 'strict']) +def test_container_to_bytes_incomp_chars_and_encod(test_input, encoding, errors): + """ + Test for passing incompatible characters and encodings container_to_bytes(). + """ + with pytest.raises(UnicodeEncodeError, match="codec can't encode"): + container_to_bytes(test_input, encoding=encoding, errors=errors) + + +@pytest.mark.parametrize( + 'test_input,encoding,expected', + [ + (u'くらとみ', 'latin1', b'????'), + (u'café', 'shift_jis', b'caf?'), + ] +) +def test_container_to_bytes_surrogate_then_replace(test_input, encoding, expected): + """ + Test for container_to_bytes() with surrogate_then_replace err handler. + """ + assert container_to_bytes(test_input, encoding=encoding, + errors='surrogate_then_replace') == expected diff --git a/test/units/module_utils/common/text/converters/test_container_to_text.py b/test/units/module_utils/common/text/converters/test_container_to_text.py new file mode 100644 index 0000000..39038f5 --- /dev/null +++ b/test/units/module_utils/common/text/converters/test_container_to_text.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible.module_utils.common.text.converters import container_to_text + + +DEFAULT_ENCODING = 'utf-8' +DEFAULT_ERR_HANDLER = 'surrogate_or_strict' + + +@pytest.mark.parametrize( + 'test_input,expected', + [ + ({1: 1}, {1: 1}), + ([1, 2], [1, 2]), + ((1, 2), (1, 2)), + (1, 1), + (1.1, 1.1), + (b'str', u'str'), + (u'str', u'str'), + ([b'str'], [u'str']), + ((b'str'), (u'str')), + ({b'str': b'str'}, {u'str': u'str'}), + ] +) +@pytest.mark.parametrize('encoding', ['utf-8', 'latin1', 'shift-jis', 'big5', 'koi8_r', ]) +@pytest.mark.parametrize('errors', ['strict', 'surrogate_or_strict', 'surrogate_then_replace', ]) +def test_container_to_text_different_types(test_input, expected, encoding, errors): + """Test for passing objects to container_to_text().""" + assert container_to_text(test_input, encoding=encoding, errors=errors) == expected + + +@pytest.mark.parametrize( + 'test_input,expected', + [ + ({1: 1}, {1: 1}), + ([1, 2], [1, 2]), + ((1, 2), (1, 2)), + (1, 1), + (1.1, 1.1), + (True, True), + (None, None), + (u'str', u'str'), + (u'くらとみ'.encode(DEFAULT_ENCODING), u'くらとみ'), + (u'café'.encode(DEFAULT_ENCODING), u'café'), + (u'str'.encode(DEFAULT_ENCODING), u'str'), + ([u'str'.encode(DEFAULT_ENCODING)], [u'str']), + ((u'str'.encode(DEFAULT_ENCODING)), (u'str')), + ({b'str': b'str'}, {u'str': u'str'}), + ] +) +def test_container_to_text_default_encoding_and_err(test_input, expected): + """ + Test for passing objects to container_to_text(). Default encoding and errors + """ + assert container_to_text(test_input, encoding=DEFAULT_ENCODING, + errors=DEFAULT_ERR_HANDLER) == expected + + +@pytest.mark.parametrize( + 'test_input,encoding,expected', + [ + (u'й'.encode('utf-8'), 'latin1', u'й'), + (u'café'.encode('utf-8'), 'shift_jis', u'cafテゥ'), + ] +) +@pytest.mark.parametrize('errors', ['strict', 'surrogate_or_strict', 'surrogate_then_replace', ]) +def test_container_to_text_incomp_encod_chars(test_input, encoding, errors, expected): + """ + Test for passing incompatible characters and encodings container_to_text(). + """ + assert container_to_text(test_input, encoding=encoding, errors=errors) == expected diff --git a/test/units/module_utils/common/text/converters/test_json_encode_fallback.py b/test/units/module_utils/common/text/converters/test_json_encode_fallback.py new file mode 100644 index 0000000..022f38f --- /dev/null +++ b/test/units/module_utils/common/text/converters/test_json_encode_fallback.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from datetime import datetime, timedelta, tzinfo + +from ansible.module_utils.common.text.converters import _json_encode_fallback + + +class timezone(tzinfo): + """Simple timezone implementation for use until we drop Python 2.7 support.""" + def __init__(self, offset): + self._offset = offset + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return timedelta(0) + + def tzname(self, dt): + return None + + +@pytest.mark.parametrize( + 'test_input,expected', + [ + (set([1]), [1]), + (datetime(2019, 5, 14, 13, 39, 38, 569047), '2019-05-14T13:39:38.569047'), + (datetime(2019, 5, 14, 13, 47, 16, 923866), '2019-05-14T13:47:16.923866'), + (datetime(2019, 6, 15, 14, 45, tzinfo=timezone(timedelta(0))), '2019-06-15T14:45:00+00:00'), + (datetime(2019, 6, 15, 14, 45, tzinfo=timezone(timedelta(hours=1, minutes=40))), '2019-06-15T14:45:00+01:40'), + ] +) +def test_json_encode_fallback(test_input, expected): + """ + Test for passing expected objects to _json_encode_fallback(). + """ + assert _json_encode_fallback(test_input) == expected + + +@pytest.mark.parametrize( + 'test_input', + [ + 1, + 1.1, + u'string', + b'string', + [1, 2], + True, + None, + {1: 1}, + (1, 2), + ] +) +def test_json_encode_fallback_default_behavior(test_input): + """ + Test for _json_encode_fallback() default behavior. + + It must fail with TypeError. + """ + with pytest.raises(TypeError, match='Cannot json serialize'): + _json_encode_fallback(test_input) diff --git a/test/units/module_utils/common/text/converters/test_jsonify.py b/test/units/module_utils/common/text/converters/test_jsonify.py new file mode 100644 index 0000000..a341531 --- /dev/null +++ b/test/units/module_utils/common/text/converters/test_jsonify.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible.module_utils.common.text.converters import jsonify + + +@pytest.mark.parametrize( + 'test_input,expected', + [ + (1, '1'), + (u'string', u'"string"'), + (u'くらとみ', u'"\\u304f\\u3089\\u3068\\u307f"'), + (u'café', u'"caf\\u00e9"'), + (b'string', u'"string"'), + (False, u'false'), + (u'string'.encode('utf-8'), u'"string"'), + ] +) +def test_jsonify(test_input, expected): + """Test for jsonify().""" + assert jsonify(test_input) == expected diff --git a/test/units/module_utils/common/text/converters/test_to_str.py b/test/units/module_utils/common/text/converters/test_to_str.py new file mode 100644 index 0000000..712ed85 --- /dev/null +++ b/test/units/module_utils/common/text/converters/test_to_str.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com> +# Copyright (c) 2017 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 itertools + +import pytest + +from ansible.module_utils.six import PY3 + +from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native + + +# Format: byte representation, text representation, encoding of byte representation +VALID_STRINGS = ( + (b'abcde', u'abcde', 'ascii'), + (b'caf\xc3\xa9', u'caf\xe9', 'utf-8'), + (b'caf\xe9', u'caf\xe9', 'latin-1'), + # u'くらとみ' + (b'\xe3\x81\x8f\xe3\x82\x89\xe3\x81\xa8\xe3\x81\xbf', u'\u304f\u3089\u3068\u307f', 'utf-8'), + (b'\x82\xad\x82\xe7\x82\xc6\x82\xdd', u'\u304f\u3089\u3068\u307f', 'shift-jis'), +) + + +@pytest.mark.parametrize('in_string, encoding, expected', + itertools.chain(((d[0], d[2], d[1]) for d in VALID_STRINGS), + ((d[1], d[2], d[1]) for d in VALID_STRINGS))) +def test_to_text(in_string, encoding, expected): + """test happy path of decoding to text""" + assert to_text(in_string, encoding) == expected + + +@pytest.mark.parametrize('in_string, encoding, expected', + itertools.chain(((d[0], d[2], d[0]) for d in VALID_STRINGS), + ((d[1], d[2], d[0]) for d in VALID_STRINGS))) +def test_to_bytes(in_string, encoding, expected): + """test happy path of encoding to bytes""" + assert to_bytes(in_string, encoding) == expected + + +@pytest.mark.parametrize('in_string, encoding, expected', + itertools.chain(((d[0], d[2], d[1] if PY3 else d[0]) for d in VALID_STRINGS), + ((d[1], d[2], d[1] if PY3 else d[0]) for d in VALID_STRINGS))) +def test_to_native(in_string, encoding, expected): + """test happy path of encoding to native strings""" + assert to_native(in_string, encoding) == expected diff --git a/test/units/module_utils/common/text/formatters/test_bytes_to_human.py b/test/units/module_utils/common/text/formatters/test_bytes_to_human.py new file mode 100644 index 0000000..41475f5 --- /dev/null +++ b/test/units/module_utils/common/text/formatters/test_bytes_to_human.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# 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.module_utils.common.text.formatters import bytes_to_human + + +@pytest.mark.parametrize( + 'input_data,expected', + [ + (0, u'0.00 Bytes'), + (0.5, u'0.50 Bytes'), + (0.54, u'0.54 Bytes'), + (1024, u'1.00 KB'), + (1025, u'1.00 KB'), + (1536, u'1.50 KB'), + (1790, u'1.75 KB'), + (1048576, u'1.00 MB'), + (1073741824, u'1.00 GB'), + (1099511627776, u'1.00 TB'), + (1125899906842624, u'1.00 PB'), + (1152921504606846976, u'1.00 EB'), + (1180591620717411303424, u'1.00 ZB'), + (1208925819614629174706176, u'1.00 YB'), + ] +) +def test_bytes_to_human(input_data, expected): + """Test of bytes_to_human function, only proper numbers are passed.""" + assert bytes_to_human(input_data) == expected + + +@pytest.mark.parametrize( + 'input_data,expected', + [ + (0, u'0.00 bits'), + (0.5, u'0.50 bits'), + (0.54, u'0.54 bits'), + (1024, u'1.00 Kb'), + (1025, u'1.00 Kb'), + (1536, u'1.50 Kb'), + (1790, u'1.75 Kb'), + (1048576, u'1.00 Mb'), + (1073741824, u'1.00 Gb'), + (1099511627776, u'1.00 Tb'), + (1125899906842624, u'1.00 Pb'), + (1152921504606846976, u'1.00 Eb'), + (1180591620717411303424, u'1.00 Zb'), + (1208925819614629174706176, u'1.00 Yb'), + ] +) +def test_bytes_to_human_isbits(input_data, expected): + """Test of bytes_to_human function with isbits=True proper results.""" + assert bytes_to_human(input_data, isbits=True) == expected + + +@pytest.mark.parametrize( + 'input_data,unit,expected', + [ + (0, u'B', u'0.00 Bytes'), + (0.5, u'B', u'0.50 Bytes'), + (0.54, u'B', u'0.54 Bytes'), + (1024, u'K', u'1.00 KB'), + (1536, u'K', u'1.50 KB'), + (1790, u'K', u'1.75 KB'), + (1048576, u'M', u'1.00 MB'), + (1099511627776, u'T', u'1.00 TB'), + (1152921504606846976, u'E', u'1.00 EB'), + (1180591620717411303424, u'Z', u'1.00 ZB'), + (1208925819614629174706176, u'Y', u'1.00 YB'), + (1025, u'KB', u'1025.00 Bytes'), + (1073741824, u'Gb', u'1073741824.00 Bytes'), + (1125899906842624, u'Pb', u'1125899906842624.00 Bytes'), + ] +) +def test_bytes_to_human_unit(input_data, unit, expected): + """Test unit argument of bytes_to_human function proper results.""" + assert bytes_to_human(input_data, unit=unit) == expected + + +@pytest.mark.parametrize( + 'input_data,unit,expected', + [ + (0, u'B', u'0.00 bits'), + (0.5, u'B', u'0.50 bits'), + (0.54, u'B', u'0.54 bits'), + (1024, u'K', u'1.00 Kb'), + (1536, u'K', u'1.50 Kb'), + (1790, u'K', u'1.75 Kb'), + (1048576, u'M', u'1.00 Mb'), + (1099511627776, u'T', u'1.00 Tb'), + (1152921504606846976, u'E', u'1.00 Eb'), + (1180591620717411303424, u'Z', u'1.00 Zb'), + (1208925819614629174706176, u'Y', u'1.00 Yb'), + (1025, u'KB', u'1025.00 bits'), + (1073741824, u'Gb', u'1073741824.00 bits'), + (1125899906842624, u'Pb', u'1125899906842624.00 bits'), + ] +) +def test_bytes_to_human_unit_isbits(input_data, unit, expected): + """Test unit argument of bytes_to_human function with isbits=True proper results.""" + assert bytes_to_human(input_data, isbits=True, unit=unit) == expected + + +@pytest.mark.parametrize('input_data', [0j, u'1B', [1], {1: 1}, None, b'1B']) +def test_bytes_to_human_illegal_size(input_data): + """Test of bytes_to_human function, illegal objects are passed as a size.""" + e_regexp = (r'(no ordering relation is defined for complex numbers)|' + r'(unsupported operand type\(s\) for /)|(unorderable types)|' + r'(not supported between instances of)') + with pytest.raises(TypeError, match=e_regexp): + bytes_to_human(input_data) diff --git a/test/units/module_utils/common/text/formatters/test_human_to_bytes.py b/test/units/module_utils/common/text/formatters/test_human_to_bytes.py new file mode 100644 index 0000000..d02699a --- /dev/null +++ b/test/units/module_utils/common/text/formatters/test_human_to_bytes.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# Copyright 2019, Sviatoslav Sydorenko <webknjaz@redhat.com> +# 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.module_utils.common.text.formatters import human_to_bytes + + +NUM_IN_METRIC = { + 'K': 2 ** 10, + 'M': 2 ** 20, + 'G': 2 ** 30, + 'T': 2 ** 40, + 'P': 2 ** 50, + 'E': 2 ** 60, + 'Z': 2 ** 70, + 'Y': 2 ** 80, +} + + +@pytest.mark.parametrize( + 'input_data,expected', + [ + (0, 0), + (u'0B', 0), + (1024, NUM_IN_METRIC['K']), + (u'1024B', NUM_IN_METRIC['K']), + (u'1K', NUM_IN_METRIC['K']), + (u'1KB', NUM_IN_METRIC['K']), + (u'1M', NUM_IN_METRIC['M']), + (u'1MB', NUM_IN_METRIC['M']), + (u'1G', NUM_IN_METRIC['G']), + (u'1GB', NUM_IN_METRIC['G']), + (u'1T', NUM_IN_METRIC['T']), + (u'1TB', NUM_IN_METRIC['T']), + (u'1P', NUM_IN_METRIC['P']), + (u'1PB', NUM_IN_METRIC['P']), + (u'1E', NUM_IN_METRIC['E']), + (u'1EB', NUM_IN_METRIC['E']), + (u'1Z', NUM_IN_METRIC['Z']), + (u'1ZB', NUM_IN_METRIC['Z']), + (u'1Y', NUM_IN_METRIC['Y']), + (u'1YB', NUM_IN_METRIC['Y']), + ] +) +def test_human_to_bytes_number(input_data, expected): + """Test of human_to_bytes function, only number arg is passed.""" + assert human_to_bytes(input_data) == expected + + +@pytest.mark.parametrize( + 'input_data,unit', + [ + (u'1024', 'B'), + (1, u'K'), + (1, u'KB'), + (u'1', u'M'), + (u'1', u'MB'), + (1, u'G'), + (1, u'GB'), + (1, u'T'), + (1, u'TB'), + (u'1', u'P'), + (u'1', u'PB'), + (u'1', u'E'), + (u'1', u'EB'), + (u'1', u'Z'), + (u'1', u'ZB'), + (u'1', u'Y'), + (u'1', u'YB'), + ] +) +def test_human_to_bytes_number_unit(input_data, unit): + """Test of human_to_bytes function, number and default_unit args are passed.""" + assert human_to_bytes(input_data, default_unit=unit) == NUM_IN_METRIC.get(unit[0], 1024) + + +@pytest.mark.parametrize('test_input', [u'1024s', u'1024w', ]) +def test_human_to_bytes_wrong_unit(test_input): + """Test of human_to_bytes function, wrong units.""" + with pytest.raises(ValueError, match="The suffix must be one of"): + human_to_bytes(test_input) + + +@pytest.mark.parametrize('test_input', [u'b1bbb', u'm2mmm', u'', u' ', -1]) +def test_human_to_bytes_wrong_number(test_input): + """Test of human_to_bytes function, number param is invalid string / number.""" + with pytest.raises(ValueError, match="can't interpret"): + human_to_bytes(test_input) + + +@pytest.mark.parametrize( + 'input_data,expected', + [ + (0, 0), + (u'0B', 0), + (u'1024b', 1024), + (u'1024B', 1024), + (u'1K', NUM_IN_METRIC['K']), + (u'1Kb', NUM_IN_METRIC['K']), + (u'1M', NUM_IN_METRIC['M']), + (u'1Mb', NUM_IN_METRIC['M']), + (u'1G', NUM_IN_METRIC['G']), + (u'1Gb', NUM_IN_METRIC['G']), + (u'1T', NUM_IN_METRIC['T']), + (u'1Tb', NUM_IN_METRIC['T']), + (u'1P', NUM_IN_METRIC['P']), + (u'1Pb', NUM_IN_METRIC['P']), + (u'1E', NUM_IN_METRIC['E']), + (u'1Eb', NUM_IN_METRIC['E']), + (u'1Z', NUM_IN_METRIC['Z']), + (u'1Zb', NUM_IN_METRIC['Z']), + (u'1Y', NUM_IN_METRIC['Y']), + (u'1Yb', NUM_IN_METRIC['Y']), + ] +) +def test_human_to_bytes_isbits(input_data, expected): + """Test of human_to_bytes function, isbits = True.""" + assert human_to_bytes(input_data, isbits=True) == expected + + +@pytest.mark.parametrize( + 'input_data,unit', + [ + (1024, 'b'), + (1024, 'B'), + (1, u'K'), + (1, u'Kb'), + (u'1', u'M'), + (u'1', u'Mb'), + (1, u'G'), + (1, u'Gb'), + (1, u'T'), + (1, u'Tb'), + (u'1', u'P'), + (u'1', u'Pb'), + (u'1', u'E'), + (u'1', u'Eb'), + (u'1', u'Z'), + (u'1', u'Zb'), + (u'1', u'Y'), + (u'1', u'Yb'), + ] +) +def test_human_to_bytes_isbits_default_unit(input_data, unit): + """Test of human_to_bytes function, isbits = True and default_unit args are passed.""" + assert human_to_bytes(input_data, default_unit=unit, isbits=True) == NUM_IN_METRIC.get(unit[0], 1024) + + +@pytest.mark.parametrize( + 'test_input,isbits', + [ + ('1024Kb', False), + ('10Mb', False), + ('1Gb', False), + ('10MB', True), + ('2KB', True), + ('4GB', True), + ] +) +def test_human_to_bytes_isbits_wrong_unit(test_input, isbits): + """Test of human_to_bytes function, unit identifier is in an invalid format for isbits value.""" + with pytest.raises(ValueError, match="Value is not a valid string"): + human_to_bytes(test_input, isbits=isbits) + + +@pytest.mark.parametrize( + 'test_input,unit,isbits', + [ + (1024, 'Kb', False), + ('10', 'Mb', False), + ('10', 'MB', True), + (2, 'KB', True), + ('4', 'GB', True), + ] +) +def test_human_to_bytes_isbits_wrong_default_unit(test_input, unit, isbits): + """Test of human_to_bytes function, default_unit is in an invalid format for isbits value.""" + with pytest.raises(ValueError, match="Value is not a valid string"): + human_to_bytes(test_input, default_unit=unit, isbits=isbits) diff --git a/test/units/module_utils/common/text/formatters/test_lenient_lowercase.py b/test/units/module_utils/common/text/formatters/test_lenient_lowercase.py new file mode 100644 index 0000000..1ecc013 --- /dev/null +++ b/test/units/module_utils/common/text/formatters/test_lenient_lowercase.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru> +# 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 datetime import datetime + +import pytest + +from ansible.module_utils.common.text.formatters import lenient_lowercase + + +INPUT_LIST = [ + u'HELLO', + u'Ёлка', + u'cafÉ', + u'くらとみ', + b'HELLO', + 1, + {1: 'Dict'}, + True, + [1], + 3.14159, +] + +EXPECTED_LIST = [ + u'hello', + u'ёлка', + u'café', + u'くらとみ', + b'hello', + 1, + {1: 'Dict'}, + True, + [1], + 3.14159, +] + +result_list = lenient_lowercase(INPUT_LIST) + + +@pytest.mark.parametrize( + 'input_value,expected_outcome', + [ + (result_list[0], EXPECTED_LIST[0]), + (result_list[1], EXPECTED_LIST[1]), + (result_list[2], EXPECTED_LIST[2]), + (result_list[3], EXPECTED_LIST[3]), + (result_list[4], EXPECTED_LIST[4]), + (result_list[5], EXPECTED_LIST[5]), + (result_list[6], EXPECTED_LIST[6]), + (result_list[7], EXPECTED_LIST[7]), + (result_list[8], EXPECTED_LIST[8]), + (result_list[9], EXPECTED_LIST[9]), + ] +) +def test_lenient_lowercase(input_value, expected_outcome): + """Test that lenient_lowercase() proper results.""" + assert input_value == expected_outcome + + +@pytest.mark.parametrize('input_data', [1, False, 1.001, 1j, datetime.now(), ]) +def test_lenient_lowercase_illegal_data_type(input_data): + """Test passing objects of illegal types to lenient_lowercase().""" + with pytest.raises(TypeError, match='object is not iterable'): + lenient_lowercase(input_data) diff --git a/test/units/module_utils/common/validation/test_check_missing_parameters.py b/test/units/module_utils/common/validation/test_check_missing_parameters.py new file mode 100644 index 0000000..6cbcb8b --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_missing_parameters.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_required_one_of +from ansible.module_utils.common.validation import check_missing_parameters + + +@pytest.fixture +def arguments_terms(): + return {"path": ""} + + +def test_check_missing_parameters(): + assert check_missing_parameters([], {}) == [] + + +def test_check_missing_parameters_list(): + expected = "missing required arguments: path" + + with pytest.raises(TypeError) as e: + check_missing_parameters({}, ["path"]) + + assert to_native(e.value) == expected + + +def test_check_missing_parameters_positive(): + assert check_missing_parameters({"path": "/foo"}, ["path"]) == [] diff --git a/test/units/module_utils/common/validation/test_check_mutually_exclusive.py b/test/units/module_utils/common/validation/test_check_mutually_exclusive.py new file mode 100644 index 0000000..7bf9076 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_mutually_exclusive.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_mutually_exclusive + + +@pytest.fixture +def mutually_exclusive_terms(): + return [ + ('string1', 'string2',), + ('box', 'fox', 'socks'), + ] + + +def test_check_mutually_exclusive(mutually_exclusive_terms): + params = { + 'string1': 'cat', + 'fox': 'hat', + } + assert check_mutually_exclusive(mutually_exclusive_terms, params) == [] + + +def test_check_mutually_exclusive_found(mutually_exclusive_terms): + params = { + 'string1': 'cat', + 'string2': 'hat', + 'fox': 'red', + 'socks': 'blue', + } + expected = "parameters are mutually exclusive: string1|string2, box|fox|socks" + + with pytest.raises(TypeError) as e: + check_mutually_exclusive(mutually_exclusive_terms, params) + + assert to_native(e.value) == expected + + +def test_check_mutually_exclusive_none(): + terms = None + params = { + 'string1': 'cat', + 'fox': 'hat', + } + assert check_mutually_exclusive(terms, params) == [] + + +def test_check_mutually_exclusive_no_params(mutually_exclusive_terms): + with pytest.raises(TypeError) as te: + check_mutually_exclusive(mutually_exclusive_terms, None) + assert "'NoneType' object is not iterable" in to_native(te.value) diff --git a/test/units/module_utils/common/validation/test_check_required_arguments.py b/test/units/module_utils/common/validation/test_check_required_arguments.py new file mode 100644 index 0000000..1dd5458 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_required_arguments.py @@ -0,0 +1,88 @@ +# -*- 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_required_arguments + + +@pytest.fixture +def arguments_terms(): + return { + 'foo': { + 'required': True, + }, + 'bar': { + 'required': False, + }, + 'tomato': { + 'irrelevant': 72, + }, + } + + +@pytest.fixture +def arguments_terms_multiple(): + return { + 'foo': { + 'required': True, + }, + 'bar': { + 'required': True, + }, + 'tomato': { + 'irrelevant': 72, + }, + } + + +def test_check_required_arguments(arguments_terms): + params = { + 'foo': 'hello', + 'bar': 'haha', + } + assert check_required_arguments(arguments_terms, params) == [] + + +def test_check_required_arguments_missing(arguments_terms): + params = { + 'apples': 'woohoo', + } + expected = "missing required arguments: foo" + + with pytest.raises(TypeError) as e: + check_required_arguments(arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_arguments_missing_multiple(arguments_terms_multiple): + params = { + 'apples': 'woohoo', + } + expected = "missing required arguments: bar, foo" + + with pytest.raises(TypeError) as e: + check_required_arguments(arguments_terms_multiple, params) + + assert to_native(e.value) == expected + + +def test_check_required_arguments_missing_none(): + terms = None + params = { + 'foo': 'bar', + 'baz': 'buzz', + } + assert check_required_arguments(terms, params) == [] + + +def test_check_required_arguments_no_params(arguments_terms): + with pytest.raises(TypeError) as te: + check_required_arguments(arguments_terms, None) + assert "'NoneType' is not iterable" in to_native(te.value) diff --git a/test/units/module_utils/common/validation/test_check_required_by.py b/test/units/module_utils/common/validation/test_check_required_by.py new file mode 100644 index 0000000..62cccff --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_required_by.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_required_by + + +@pytest.fixture +def path_arguments_terms(): + return { + "path": ["mode", "owner"], + } + + +def test_check_required_by(): + arguments_terms = {} + params = {} + assert check_required_by(arguments_terms, params) == {} + + +def test_check_required_by_missing(): + arguments_terms = { + "force": "force_reason", + } + params = {"force": True} + expected = "missing parameter(s) required by 'force': force_reason" + + with pytest.raises(TypeError) as e: + check_required_by(arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_by_multiple(path_arguments_terms): + params = { + "path": "/foo/bar", + } + expected = "missing parameter(s) required by 'path': mode, owner" + + with pytest.raises(TypeError) as e: + check_required_by(path_arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_by_single(path_arguments_terms): + params = {"path": "/foo/bar", "mode": "0700"} + expected = "missing parameter(s) required by 'path': owner" + + with pytest.raises(TypeError) as e: + check_required_by(path_arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_by_missing_none(path_arguments_terms): + params = { + "path": "/foo/bar", + "mode": "0700", + "owner": "root", + } + assert check_required_by(path_arguments_terms, params) + + +def test_check_required_by_options_context(path_arguments_terms): + params = {"path": "/foo/bar", "mode": "0700"} + + options_context = ["foo_context"] + + expected = "missing parameter(s) required by 'path': owner found in foo_context" + + with pytest.raises(TypeError) as e: + check_required_by(path_arguments_terms, params, options_context) + + assert to_native(e.value) == expected + + +def test_check_required_by_missing_multiple_options_context(path_arguments_terms): + params = { + "path": "/foo/bar", + } + options_context = ["foo_context"] + + expected = ( + "missing parameter(s) required by 'path': mode, owner found in foo_context" + ) + + with pytest.raises(TypeError) as e: + check_required_by(path_arguments_terms, params, options_context) + + assert to_native(e.value) == expected diff --git a/test/units/module_utils/common/validation/test_check_required_if.py b/test/units/module_utils/common/validation/test_check_required_if.py new file mode 100644 index 0000000..4189164 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_required_if.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_required_if + + +def test_check_required_if(): + arguments_terms = {} + params = {} + assert check_required_if(arguments_terms, params) == [] + + +def test_check_required_if_missing(): + arguments_terms = [["state", "present", ("path",)]] + params = {"state": "present"} + expected = "state is present but all of the following are missing: path" + + with pytest.raises(TypeError) as e: + check_required_if(arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_if_missing_required(): + arguments_terms = [["state", "present", ("path", "owner"), True]] + params = {"state": "present"} + expected = "state is present but any of the following are missing: path, owner" + + with pytest.raises(TypeError) as e: + check_required_if(arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_if_missing_multiple(): + arguments_terms = [["state", "present", ("path", "owner")]] + params = { + "state": "present", + } + expected = "state is present but all of the following are missing: path, owner" + + with pytest.raises(TypeError) as e: + check_required_if(arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_if_missing_multiple_with_context(): + arguments_terms = [["state", "present", ("path", "owner")]] + params = { + "state": "present", + } + options_context = ["foo_context"] + expected = "state is present but all of the following are missing: path, owner found in foo_context" + + with pytest.raises(TypeError) as e: + check_required_if(arguments_terms, params, options_context) + + assert to_native(e.value) == expected + + +def test_check_required_if_multiple(): + arguments_terms = [["state", "present", ("path", "owner")]] + params = { + "state": "present", + "path": "/foo", + "owner": "root", + } + options_context = ["foo_context"] + assert check_required_if(arguments_terms, params) == [] + assert check_required_if(arguments_terms, params, options_context) == [] diff --git a/test/units/module_utils/common/validation/test_check_required_one_of.py b/test/units/module_utils/common/validation/test_check_required_one_of.py new file mode 100644 index 0000000..b081889 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_required_one_of.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_required_one_of + + +@pytest.fixture +def arguments_terms(): + return [["path", "owner"]] + + +def test_check_required_one_of(): + assert check_required_one_of([], {}) == [] + + +def test_check_required_one_of_missing(arguments_terms): + params = {"state": "present"} + expected = "one of the following is required: path, owner" + + with pytest.raises(TypeError) as e: + check_required_one_of(arguments_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_one_of_provided(arguments_terms): + params = {"state": "present", "path": "/foo"} + assert check_required_one_of(arguments_terms, params) == [] + + +def test_check_required_one_of_context(arguments_terms): + params = {"state": "present"} + expected = "one of the following is required: path, owner found in foo_context" + option_context = ["foo_context"] + + with pytest.raises(TypeError) as e: + check_required_one_of(arguments_terms, params, option_context) + + assert to_native(e.value) == expected diff --git a/test/units/module_utils/common/validation/test_check_required_together.py b/test/units/module_utils/common/validation/test_check_required_together.py new file mode 100644 index 0000000..8a2daab --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_required_together.py @@ -0,0 +1,57 @@ +# -*- 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_required_together + + +@pytest.fixture +def together_terms(): + return [ + ['bananas', 'potatoes'], + ['cats', 'wolves'] + ] + + +def test_check_required_together(together_terms): + params = { + 'bananas': 'hello', + 'potatoes': 'this is here too', + 'dogs': 'haha', + } + assert check_required_together(together_terms, params) == [] + + +def test_check_required_together_missing(together_terms): + params = { + 'bananas': 'woohoo', + 'wolves': 'uh oh', + } + expected = "parameters are required together: bananas, potatoes" + + with pytest.raises(TypeError) as e: + check_required_together(together_terms, params) + + assert to_native(e.value) == expected + + +def test_check_required_together_missing_none(): + terms = None + params = { + 'foo': 'bar', + 'baz': 'buzz', + } + assert check_required_together(terms, params) == [] + + +def test_check_required_together_no_params(together_terms): + with pytest.raises(TypeError) as te: + check_required_together(together_terms, None) + + assert "'NoneType' object is not iterable" in to_native(te.value) diff --git a/test/units/module_utils/common/validation/test_check_type_bits.py b/test/units/module_utils/common/validation/test_check_type_bits.py new file mode 100644 index 0000000..7f6b11d --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_bits.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_bits + + +def test_check_type_bits(): + test_cases = ( + ('1', 1), + (99, 99), + (1.5, 2), + ('1.5', 2), + ('2b', 2), + ('2k', 2048), + ('2K', 2048), + ('1m', 1048576), + ('1M', 1048576), + ('1g', 1073741824), + ('1G', 1073741824), + (1073741824, 1073741824), + ) + for case in test_cases: + assert case[1] == check_type_bits(case[0]) + + +def test_check_type_bits_fail(): + test_cases = ( + 'foo', + '2KB', + '1MB', + '1GB', + ) + for case in test_cases: + with pytest.raises(TypeError) as e: + check_type_bits(case) + assert 'cannot be converted to a Bit value' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_check_type_bool.py b/test/units/module_utils/common/validation/test_check_type_bool.py new file mode 100644 index 0000000..bd867dc --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_bool.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_bool + + +def test_check_type_bool(): + test_cases = ( + (True, True), + (False, False), + ('1', True), + ('on', True), + (1, True), + ('0', False), + (0, False), + ('n', False), + ('f', False), + ('false', False), + ('true', True), + ('y', True), + ('t', True), + ('yes', True), + ('no', False), + ('off', False), + ) + for case in test_cases: + assert case[1] == check_type_bool(case[0]) + + +def test_check_type_bool_fail(): + default_test_msg = 'cannot be converted to a bool' + test_cases = ( + ({'k1': 'v1'}, 'is not a valid bool'), + (3.14159, default_test_msg), + (-1, default_test_msg), + (-90810398401982340981023948192349081, default_test_msg), + (90810398401982340981023948192349081, default_test_msg), + ) + for case in test_cases: + with pytest.raises(TypeError) as e: + check_type_bool(case) + assert 'cannot be converted to a bool' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_check_type_bytes.py b/test/units/module_utils/common/validation/test_check_type_bytes.py new file mode 100644 index 0000000..6ff62dc --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_bytes.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_bytes + + +def test_check_type_bytes(): + test_cases = ( + ('1', 1), + (99, 99), + (1.5, 2), + ('1.5', 2), + ('2b', 2), + ('2B', 2), + ('2k', 2048), + ('2K', 2048), + ('2KB', 2048), + ('1m', 1048576), + ('1M', 1048576), + ('1MB', 1048576), + ('1g', 1073741824), + ('1G', 1073741824), + ('1GB', 1073741824), + (1073741824, 1073741824), + ) + for case in test_cases: + assert case[1] == check_type_bytes(case[0]) + + +def test_check_type_bytes_fail(): + test_cases = ( + 'foo', + '2kb', + '2Kb', + '1mb', + '1Mb', + '1gb', + '1Gb', + ) + for case in test_cases: + with pytest.raises(TypeError) as e: + check_type_bytes(case) + assert 'cannot be converted to a Byte value' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_check_type_dict.py b/test/units/module_utils/common/validation/test_check_type_dict.py new file mode 100644 index 0000000..75638c5 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_dict.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.validation import check_type_dict + + +def test_check_type_dict(): + test_cases = ( + ({'k1': 'v1'}, {'k1': 'v1'}), + ('k1=v1,k2=v2', {'k1': 'v1', 'k2': 'v2'}), + ('k1=v1, k2=v2', {'k1': 'v1', 'k2': 'v2'}), + ('k1=v1, k2=v2, k3=v3', {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}), + ('{"key": "value", "list": ["one", "two"]}', {'key': 'value', 'list': ['one', 'two']}) + ) + for case in test_cases: + assert case[1] == check_type_dict(case[0]) + + +def test_check_type_dict_fail(): + test_cases = ( + 1, + 3.14159, + [1, 2], + 'a', + ) + for case in test_cases: + with pytest.raises(TypeError): + check_type_dict(case) diff --git a/test/units/module_utils/common/validation/test_check_type_float.py b/test/units/module_utils/common/validation/test_check_type_float.py new file mode 100644 index 0000000..57837fa --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_float.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_float + + +def test_check_type_float(): + test_cases = ( + ('1.5', 1.5), + ('''1.5''', 1.5), + (u'1.5', 1.5), + (1002, 1002.0), + (1.0, 1.0), + (3.141592653589793, 3.141592653589793), + ('3.141592653589793', 3.141592653589793), + (b'3.141592653589793', 3.141592653589793), + ) + for case in test_cases: + assert case[1] == check_type_float(case[0]) + + +def test_check_type_float_fail(): + test_cases = ( + {'k1': 'v1'}, + ['a', 'b'], + 'b', + ) + for case in test_cases: + with pytest.raises(TypeError) as e: + check_type_float(case) + assert 'cannot be converted to a float' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_check_type_int.py b/test/units/module_utils/common/validation/test_check_type_int.py new file mode 100644 index 0000000..22cedf6 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_int.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_int + + +def test_check_type_int(): + test_cases = ( + ('1', 1), + (u'1', 1), + (1002, 1002), + ) + for case in test_cases: + assert case[1] == check_type_int(case[0]) + + +def test_check_type_int_fail(): + test_cases = ( + {'k1': 'v1'}, + (b'1', 1), + (3.14159, 3), + 'b', + ) + for case in test_cases: + with pytest.raises(TypeError) as e: + check_type_int(case) + assert 'cannot be converted to an int' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_check_type_jsonarg.py b/test/units/module_utils/common/validation/test_check_type_jsonarg.py new file mode 100644 index 0000000..e78e54b --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_jsonarg.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_jsonarg + + +def test_check_type_jsonarg(): + test_cases = ( + ('a', 'a'), + ('a ', 'a'), + (b'99', b'99'), + (b'99 ', b'99'), + ({'k1': 'v1'}, '{"k1": "v1"}'), + ([1, 'a'], '[1, "a"]'), + ((1, 2, 'three'), '[1, 2, "three"]'), + ) + for case in test_cases: + assert case[1] == check_type_jsonarg(case[0]) + + +def test_check_type_jsonarg_fail(): + test_cases = ( + 1.5, + 910313498012384012341982374109384098, + ) + for case in test_cases: + with pytest.raises(TypeError) as e: + check_type_jsonarg(case) + assert 'cannot be converted to a json string' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_check_type_list.py b/test/units/module_utils/common/validation/test_check_type_list.py new file mode 100644 index 0000000..3f7a9ee --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_list.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.validation import check_type_list + + +def test_check_type_list(): + test_cases = ( + ([1, 2], [1, 2]), + (1, ['1']), + (['a', 'b'], ['a', 'b']), + ('a', ['a']), + (3.14159, ['3.14159']), + ('a,b,1,2', ['a', 'b', '1', '2']) + ) + for case in test_cases: + assert case[1] == check_type_list(case[0]) + + +def test_check_type_list_failure(): + test_cases = ( + {'k1': 'v1'}, + ) + for case in test_cases: + with pytest.raises(TypeError): + check_type_list(case) diff --git a/test/units/module_utils/common/validation/test_check_type_path.py b/test/units/module_utils/common/validation/test_check_type_path.py new file mode 100644 index 0000000..d6ff433 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_path.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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 re + +import os +from ansible.module_utils.common.validation import check_type_path + + +def mock_expand(value): + return re.sub(r'~|\$HOME', '/home/testuser', value) + + +def test_check_type_path(monkeypatch): + monkeypatch.setattr(os.path, 'expandvars', mock_expand) + monkeypatch.setattr(os.path, 'expanduser', mock_expand) + test_cases = ( + ('~/foo', '/home/testuser/foo'), + ('$HOME/foo', '/home/testuser/foo'), + ('/home/jane', '/home/jane'), + (u'/home/jané', u'/home/jané'), + ) + for case in test_cases: + assert case[1] == check_type_path(case[0]) diff --git a/test/units/module_utils/common/validation/test_check_type_raw.py b/test/units/module_utils/common/validation/test_check_type_raw.py new file mode 100644 index 0000000..988e554 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_raw.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.validation import check_type_raw + + +def test_check_type_raw(): + test_cases = ( + (1, 1), + ('1', '1'), + ('a', 'a'), + ({'k1': 'v1'}, {'k1': 'v1'}), + ([1, 2], [1, 2]), + (b'42', b'42'), + (u'42', u'42'), + ) + for case in test_cases: + assert case[1] == check_type_raw(case[0]) diff --git a/test/units/module_utils/common/validation/test_check_type_str.py b/test/units/module_utils/common/validation/test_check_type_str.py new file mode 100644 index 0000000..f10dad2 --- /dev/null +++ b/test/units/module_utils/common/validation/test_check_type_str.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils._text import to_native +from ansible.module_utils.common.validation import check_type_str + + +TEST_CASES = ( + ('string', 'string'), + (100, '100'), + (1.5, '1.5'), + ({'k1': 'v1'}, "{'k1': 'v1'}"), + ([1, 2, 'three'], "[1, 2, 'three']"), + ((1, 2,), '(1, 2)'), +) + + +@pytest.mark.parametrize('value, expected', TEST_CASES) +def test_check_type_str(value, expected): + assert expected == check_type_str(value) + + +@pytest.mark.parametrize('value, expected', TEST_CASES[1:]) +def test_check_type_str_no_conversion(value, expected): + with pytest.raises(TypeError) as e: + check_type_str(value, allow_conversion=False) + assert 'is not a string and conversion is not allowed' in to_native(e.value) diff --git a/test/units/module_utils/common/validation/test_count_terms.py b/test/units/module_utils/common/validation/test_count_terms.py new file mode 100644 index 0000000..f41dc40 --- /dev/null +++ b/test/units/module_utils/common/validation/test_count_terms.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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.module_utils.common.validation import count_terms + + +@pytest.fixture +def params(): + return { + 'name': 'bob', + 'dest': '/etc/hosts', + 'state': 'present', + 'value': 5, + } + + +def test_count_terms(params): + check = set(('name', 'dest')) + assert count_terms(check, params) == 2 + + +def test_count_terms_str_input(params): + check = 'name' + assert count_terms(check, params) == 1 + + +def test_count_terms_tuple_input(params): + check = ('name', 'dest') + assert count_terms(check, params) == 2 + + +def test_count_terms_list_input(params): + check = ['name', 'dest'] + assert count_terms(check, params) == 2 diff --git a/test/units/module_utils/common/warnings/test_deprecate.py b/test/units/module_utils/common/warnings/test_deprecate.py new file mode 100644 index 0000000..08c1b35 --- /dev/null +++ b/test/units/module_utils/common/warnings/test_deprecate.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# (c) 2019 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.module_utils.common import warnings + +from ansible.module_utils.common.warnings import deprecate, get_deprecation_messages +from ansible.module_utils.six import PY3 + + +@pytest.fixture +def deprecation_messages(): + return [ + {'msg': 'First deprecation', 'version': None, 'collection_name': None}, + {'msg': 'Second deprecation', 'version': None, 'collection_name': 'ansible.builtin'}, + {'msg': 'Third deprecation', 'version': '2.14', 'collection_name': None}, + {'msg': 'Fourth deprecation', 'version': '2.9', 'collection_name': None}, + {'msg': 'Fifth deprecation', 'version': '2.9', 'collection_name': 'ansible.builtin'}, + {'msg': 'Sixth deprecation', 'date': '2199-12-31', 'collection_name': None}, + {'msg': 'Seventh deprecation', 'date': '2199-12-31', 'collection_name': 'ansible.builtin'}, + ] + + +@pytest.fixture +def reset(monkeypatch): + monkeypatch.setattr(warnings, '_global_deprecations', []) + + +def test_deprecate_message_only(reset): + deprecate('Deprecation message') + assert warnings._global_deprecations == [ + {'msg': 'Deprecation message', 'version': None, 'collection_name': None}] + + +def test_deprecate_with_collection(reset): + deprecate(msg='Deprecation message', collection_name='ansible.builtin') + assert warnings._global_deprecations == [ + {'msg': 'Deprecation message', 'version': None, 'collection_name': 'ansible.builtin'}] + + +def test_deprecate_with_version(reset): + deprecate(msg='Deprecation message', version='2.14') + assert warnings._global_deprecations == [ + {'msg': 'Deprecation message', 'version': '2.14', 'collection_name': None}] + + +def test_deprecate_with_version_and_collection(reset): + deprecate(msg='Deprecation message', version='2.14', collection_name='ansible.builtin') + assert warnings._global_deprecations == [ + {'msg': 'Deprecation message', 'version': '2.14', 'collection_name': 'ansible.builtin'}] + + +def test_deprecate_with_date(reset): + deprecate(msg='Deprecation message', date='2199-12-31') + assert warnings._global_deprecations == [ + {'msg': 'Deprecation message', 'date': '2199-12-31', 'collection_name': None}] + + +def test_deprecate_with_date_and_collection(reset): + deprecate(msg='Deprecation message', date='2199-12-31', collection_name='ansible.builtin') + assert warnings._global_deprecations == [ + {'msg': 'Deprecation message', 'date': '2199-12-31', 'collection_name': 'ansible.builtin'}] + + +def test_multiple_deprecations(deprecation_messages, reset): + for d in deprecation_messages: + deprecate(**d) + + assert deprecation_messages == warnings._global_deprecations + + +def test_get_deprecation_messages(deprecation_messages, reset): + for d in deprecation_messages: + deprecate(**d) + + accessor_deprecations = get_deprecation_messages() + assert isinstance(accessor_deprecations, tuple) + assert len(accessor_deprecations) == 7 + + +@pytest.mark.parametrize( + 'test_case', + ( + 1, + True, + [1], + {'k1': 'v1'}, + (1, 2), + 6.62607004, + b'bytestr' if PY3 else None, + None, + ) +) +def test_deprecate_failure(test_case): + with pytest.raises(TypeError, match='deprecate requires a string not a %s' % type(test_case)): + deprecate(test_case) diff --git a/test/units/module_utils/common/warnings/test_warn.py b/test/units/module_utils/common/warnings/test_warn.py new file mode 100644 index 0000000..41e1a7b --- /dev/null +++ b/test/units/module_utils/common/warnings/test_warn.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# (c) 2019 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.module_utils.common import warnings + +from ansible.module_utils.common.warnings import warn, get_warning_messages +from ansible.module_utils.six import PY3 + + +@pytest.fixture +def warning_messages(): + return [ + 'First warning', + 'Second warning', + 'Third warning', + ] + + +def test_warn(): + warn('Warning message') + assert warnings._global_warnings == ['Warning message'] + + +def test_multiple_warningss(warning_messages): + for w in warning_messages: + warn(w) + + assert warning_messages == warnings._global_warnings + + +def test_get_warning_messages(warning_messages): + for w in warning_messages: + warn(w) + + accessor_warnings = get_warning_messages() + assert isinstance(accessor_warnings, tuple) + assert len(accessor_warnings) == 3 + + +@pytest.mark.parametrize( + 'test_case', + ( + 1, + True, + [1], + {'k1': 'v1'}, + (1, 2), + 6.62607004, + b'bytestr' if PY3 else None, + None, + ) +) +def test_warn_failure(test_case): + with pytest.raises(TypeError, match='warn requires a string not a %s' % type(test_case)): + warn(test_case) |