You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/test/units/parsing/yaml/test_errors.py

134 lines
6.2 KiB
Python

from __future__ import annotations
import pathlib
import tempfile
from unittest.mock import call
import typing as t
import pytest
import pytest_mock
from ansible import constants as C
from ansible._internal._errors._utils import format_exception_message
from ansible._internal._datatag._tags import Origin
from ansible.parsing.utils.yaml import from_yaml
from ansible._internal._yaml._errors import AnsibleYAMLParserError
from ansible.utils.display import Display
@pytest.mark.parametrize("content, expected_message, expect_help_text, line, col", (
# cases with multiple permutations due to handling of multiple levels of list and up to one dict level
('{{ bar }}', 'This may be an issue with missing quotes around a template block.', True, 1, 1),
('foo: {{ bar }}', 'This may be an issue with missing quotes around a template block.', True, 1, 6),
('- {{ bar }}', 'This may be an issue with missing quotes around a template block.', True, 1, 3),
('- foo: {{ bar }}', 'This may be an issue with missing quotes around a template block.', True, 1, 8),
(' - - {{ bar }}', 'This may be an issue with missing quotes around a template block.', True, 1, 7),
(' - - foo: {{ bar }}', 'This may be an issue with missing quotes around a template block.', True, 1, 12),
('"foo" foo', 'Values starting with a quote must end with the same quote.', True, 1, 1),
('foo: "foo" foo', 'Values starting with a quote must end with the same quote.', True, 1, 6),
('- "foo" foo', 'Values starting with a quote must end with the same quote.', True, 1, 3),
('- foo: "foo" foo', 'Values starting with a quote must end with the same quote.', True, 1, 8),
(' - - "foo" foo', 'Values starting with a quote must end with the same quote.', True, 1, 7),
(' - - foo: "foo" foo', 'Values starting with a quote must end with the same quote.', True, 1, 12),
('"foo" "foo"', 'Values starting with a quote must end with the same quote, and not contain that quote.', True, 1, 1),
('foo: "foo" "foo"', 'Values starting with a quote must end with the same quote, and not contain that quote.', True, 1, 6),
('- "foo" "foo"', 'Values starting with a quote must end with the same quote, and not contain that quote.', True, 1, 3),
('- foo: "foo" "foo"', 'Values starting with a quote must end with the same quote, and not contain that quote.', True, 1, 8),
(' - - "foo" "foo"', 'Values starting with a quote must end with the same quote, and not contain that quote.', True, 1, 7),
(' - - foo: "foo" "foo"', 'Values starting with a quote must end with the same quote, and not contain that quote.', True, 1, 12),
# cases without list/dict handling
('aaa: bbb:', 'Colons in unquoted values must be followed by a non-space character.', True, 1, 9),
('aaa: bbb: ccc', 'Colons in unquoted values must be followed by a non-space character.', True, 1, 9),
('''- value == "x: 'x' x"''', 'Colons in unquoted values must be followed by a non-space character.', True, 1, 14),
('!!map 1\t2\t3', 'Tabs are usually invalid in YAML.', False, 1, 8),
('{a: 1, a: 1}', "Found duplicate mapping key 'a'.", False, 1, 8),
(1, 'a string or stream input is required', False, None, None), # wrong type (misuse of API)
# cases where the underling MarkedYAMLError is used
('k1: v1\n k2: v2', 'Mapping values are not allowed in this context.', False, 2, 5),
('k1: v1\n k2: ":"', 'Mapping values are not allowed in this context.', False, 2, 5),
(':', 'While parsing a block mapping did not find expected key.', False, 1, 1),
('!!map 1', 'Expected a mapping node, but found scalar.', False, 1, 1),
('[]: bad', 'While constructing a mapping found unhashable key.', False, 1, 1),
('{}: bad', 'While constructing a mapping found unhashable key.', False, 1, 1),
('"\\"', 'While scanning a quoted scalar found unexpected end of stream.', False, 1, 4),
# DTFIX-FUTURE: add tests that use comments
))
def test_yaml_parser_error(
content: t.Any,
expected_message: str,
expect_help_text: bool,
line: int | None,
col: int | None,
mocker: pytest_mock.MockerFixture,
) -> None:
set_duplicate_yaml_dict_key_config(mocker, 'error')
expected_message = f'YAML parsing failed: {expected_message}'
with tempfile.TemporaryDirectory() as tempdir:
source_path = pathlib.Path(tempdir) / 'source.yml'
source_path.write_text(str(content))
with pytest.raises(AnsibleYAMLParserError) as error:
from_yaml(content, file_name=str(source_path))
assert error.value.message == expected_message
assert error.value._original_message == expected_message
assert format_exception_message(error.value) == expected_message
assert str(error.value) == expected_message
assert error.value.obj == Origin(path=str(source_path), line_num=line, col_num=col)
if expect_help_text:
assert error.value._help_text is not None # DTFIX-FUTURE: check the content later once it's less volatile
else:
assert error.value._help_text is None
def test_yaml_duplicate_key_warning(mocker: pytest_mock.MockerFixture) -> None:
set_duplicate_yaml_dict_key_config(mocker, 'warn')
patched_warning = mocker.patch.object(Display(), 'warning')
assert from_yaml('{a: 1, b: 2, c: 3, b: 4, d: 5, b: 6}', file_name='/hello.yml')
patched_warning.assert_has_calls((
call(msg="Found duplicate mapping key 'b'.", obj="b", help_text="Using last defined value only."),
call(msg="Found duplicate mapping key 'b'.", obj="b", help_text="Using last defined value only."),
))
def test_yaml_duplicate_key_ignore(mocker: pytest_mock.MockerFixture) -> None:
set_duplicate_yaml_dict_key_config(mocker, 'ignore')
warning_spy = mocker.spy(Display(), 'warning')
assert from_yaml('{a: 1, a: 2}', file_name='/hello.yml')
warning_spy.assert_not_called()
def set_duplicate_yaml_dict_key_config(mocker: pytest_mock.MockerFixture, value: str):
original_get_config_value = C.config.get_config_value
def mocked_get_config_value(config, *args, **kwargs):
if config == 'DUPLICATE_YAML_DICT_KEY':
return value
return original_get_config_value(config, *args, **kwargs)
mocker.patch.object(C.config, 'get_config_value', mocked_get_config_value)