1# -*- coding: utf-8 -*-
2# Copyright (c) 2018–2019, Sviatoslav Sydorenko <webknjaz@redhat.com>
3# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
4"""Test low-level utility functions from ``module_utils.common.collections``."""
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9import pytest
10
11from ansible.module_utils.six import Iterator
12from ansible.module_utils.common._collections_compat import Sequence
13from ansible.module_utils.common.collections import ImmutableDict, is_iterable, is_sequence
14
15
16class SeqStub:
17    """Stub emulating a sequence type.
18
19    >>> from collections.abc import Sequence
20    >>> assert issubclass(SeqStub, Sequence)
21    >>> assert isinstance(SeqStub(), Sequence)
22    """
23
24
25Sequence.register(SeqStub)
26
27
28class IteratorStub(Iterator):
29    def __next__(self):
30        raise StopIteration
31
32
33class IterableStub:
34    def __iter__(self):
35        return IteratorStub()
36
37
38TEST_STRINGS = u'he', u'Україна', u'Česká republika'
39TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS)
40
41TEST_ITEMS_NON_SEQUENCES = (
42    {}, object(), frozenset(),
43    4, 0.,
44) + TEST_STRINGS
45
46TEST_ITEMS_SEQUENCES = (
47    [], (),
48    SeqStub(),
49)
50TEST_ITEMS_SEQUENCES = TEST_ITEMS_SEQUENCES + (
51    # Iterable effectively containing nested random data:
52    TEST_ITEMS_NON_SEQUENCES,
53)
54
55
56@pytest.mark.parametrize('sequence_input', TEST_ITEMS_SEQUENCES)
57def test_sequence_positive(sequence_input):
58    """Test that non-string item sequences are identified correctly."""
59    assert is_sequence(sequence_input)
60    assert is_sequence(sequence_input, include_strings=False)
61
62
63@pytest.mark.parametrize('non_sequence_input', TEST_ITEMS_NON_SEQUENCES)
64def test_sequence_negative(non_sequence_input):
65    """Test that non-sequences are identified correctly."""
66    assert not is_sequence(non_sequence_input)
67
68
69@pytest.mark.parametrize('string_input', TEST_STRINGS)
70def test_sequence_string_types_with_strings(string_input):
71    """Test that ``is_sequence`` can separate string and non-string."""
72    assert is_sequence(string_input, include_strings=True)
73
74
75@pytest.mark.parametrize('string_input', TEST_STRINGS)
76def test_sequence_string_types_without_strings(string_input):
77    """Test that ``is_sequence`` can separate string and non-string."""
78    assert not is_sequence(string_input, include_strings=False)
79
80
81@pytest.mark.parametrize(
82    'seq',
83    ([], (), {}, set(), frozenset(), IterableStub()),
84)
85def test_iterable_positive(seq):
86    assert is_iterable(seq)
87
88
89@pytest.mark.parametrize(
90    'seq', (IteratorStub(), object(), 5, 9.)
91)
92def test_iterable_negative(seq):
93    assert not is_iterable(seq)
94
95
96@pytest.mark.parametrize('string_input', TEST_STRINGS)
97def test_iterable_including_strings(string_input):
98    assert is_iterable(string_input, include_strings=True)
99
100
101@pytest.mark.parametrize('string_input', TEST_STRINGS)
102def test_iterable_excluding_strings(string_input):
103    assert not is_iterable(string_input, include_strings=False)
104
105
106class TestImmutableDict:
107    def test_scalar(self):
108        imdict = ImmutableDict({1: 2})
109        assert imdict[1] == 2
110
111    def test_string(self):
112        imdict = ImmutableDict({u'café': u'くらとみ'})
113        assert imdict[u'café'] == u'くらとみ'
114
115    def test_container(self):
116        imdict = ImmutableDict({(1, 2): ['1', '2']})
117        assert imdict[(1, 2)] == ['1', '2']
118
119    def test_from_tuples(self):
120        imdict = ImmutableDict((('a', 1), ('b', 2)))
121        assert frozenset(imdict.items()) == frozenset((('a', 1), ('b', 2)))
122
123    def test_from_kwargs(self):
124        imdict = ImmutableDict(a=1, b=2)
125        assert frozenset(imdict.items()) == frozenset((('a', 1), ('b', 2)))
126
127    def test_immutable(self):
128        imdict = ImmutableDict({1: 2})
129
130        expected_reason = r"^'ImmutableDict' object does not support item assignment$"
131
132        with pytest.raises(TypeError, match=expected_reason):
133            imdict[1] = 3
134
135        with pytest.raises(TypeError, match=expected_reason):
136            imdict[5] = 3
137
138    def test_hashable(self):
139        # ImmutableDict is hashable when all of its values are hashable
140        imdict = ImmutableDict({u'café': u'くらとみ'})
141        assert hash(imdict)
142
143    def test_nonhashable(self):
144        # ImmutableDict is unhashable when one of its values is unhashable
145        imdict = ImmutableDict({u'café': u'くらとみ', 1: [1, 2]})
146
147        expected_reason = r"^unhashable type: 'list'$"
148
149        with pytest.raises(TypeError, match=expected_reason):
150            hash(imdict)
151
152    def test_len(self):
153        imdict = ImmutableDict({1: 2, 'a': 'b'})
154        assert len(imdict) == 2
155
156    def test_repr(self):
157        initial_data = {1: 2, 'a': 'b'}
158        initial_data_repr = repr(initial_data)
159        imdict = ImmutableDict(initial_data)
160        actual_repr = repr(imdict)
161        expected_repr = "ImmutableDict({0})".format(initial_data_repr)
162        assert actual_repr == expected_repr
163