1# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17
18# Make coding more python3-ish
19from __future__ import (absolute_import, division, print_function)
20__metaclass__ = type
21
22from units.compat import unittest
23from units.compat.mock import patch, MagicMock
24
25from ansible.errors import AnsibleError, AnsibleParserError
26from ansible.module_utils.common._collections_compat import Container
27from ansible.playbook.block import Block
28
29from units.mock.loader import DictDataLoader
30from units.mock.path import mock_unfrackpath_noop
31
32from ansible.playbook.role import Role
33from ansible.playbook.role.include import RoleInclude
34from ansible.playbook.role import hash_params
35
36
37class TestHashParams(unittest.TestCase):
38    def test(self):
39        params = {'foo': 'bar'}
40        res = hash_params(params)
41        self._assert_set(res)
42        self._assert_hashable(res)
43
44    def _assert_hashable(self, res):
45        a_dict = {}
46        try:
47            a_dict[res] = res
48        except TypeError as e:
49            self.fail('%s is not hashable: %s' % (res, e))
50
51    def _assert_set(self, res):
52        self.assertIsInstance(res, frozenset)
53
54    def test_dict_tuple(self):
55        params = {'foo': (1, 'bar',)}
56        res = hash_params(params)
57        self._assert_set(res)
58
59    def test_tuple(self):
60        params = (1, None, 'foo')
61        res = hash_params(params)
62        self._assert_hashable(res)
63
64    def test_tuple_dict(self):
65        params = ({'foo': 'bar'}, 37)
66        res = hash_params(params)
67        self._assert_hashable(res)
68
69    def test_list(self):
70        params = ['foo', 'bar', 1, 37, None]
71        res = hash_params(params)
72        self._assert_set(res)
73        self._assert_hashable(res)
74
75    def test_dict_with_list_value(self):
76        params = {'foo': [1, 4, 'bar']}
77        res = hash_params(params)
78        self._assert_set(res)
79        self._assert_hashable(res)
80
81    def test_empty_set(self):
82        params = set([])
83        res = hash_params(params)
84        self._assert_hashable(res)
85        self._assert_set(res)
86
87    def test_generator(self):
88        def my_generator():
89            for i in ['a', 1, None, {}]:
90                yield i
91
92        params = my_generator()
93        res = hash_params(params)
94        self._assert_hashable(res)
95
96    def test_container_but_not_iterable(self):
97        # This is a Container that is not iterable, which is unlikely but...
98        class MyContainer(Container):
99            def __init__(self, some_thing):
100                self.data = []
101                self.data.append(some_thing)
102
103            def __contains__(self, item):
104                return item in self.data
105
106            def __hash__(self):
107                return hash(self.data)
108
109            def __len__(self):
110                return len(self.data)
111
112            def __call__(self):
113                return False
114
115        foo = MyContainer('foo bar')
116        params = foo
117
118        self.assertRaises(TypeError, hash_params, params)
119
120    def test_param_dict_dupe_values(self):
121        params1 = {'foo': False}
122        params2 = {'bar': False}
123
124        res1 = hash_params(params1)
125        res2 = hash_params(params2)
126
127        hash1 = hash(res1)
128        hash2 = hash(res2)
129        self.assertNotEqual(res1, res2)
130        self.assertNotEqual(hash1, hash2)
131
132    def test_param_dupe(self):
133        params1 = {
134            # 'from_files': {},
135            'tags': [],
136            u'testvalue': False,
137            u'testvalue2': True,
138            # 'when': []
139        }
140        params2 = {
141            # 'from_files': {},
142            'tags': [],
143            u'testvalue': True,
144            u'testvalue2': False,
145            # 'when': []
146        }
147        res1 = hash_params(params1)
148        res2 = hash_params(params2)
149
150        self.assertNotEqual(hash(res1), hash(res2))
151        self.assertNotEqual(res1, res2)
152
153        foo = {}
154        foo[res1] = 'params1'
155        foo[res2] = 'params2'
156
157        self.assertEqual(len(foo), 2)
158
159        del foo[res2]
160        self.assertEqual(len(foo), 1)
161
162        for key in foo:
163            self.assertTrue(key in foo)
164            self.assertIn(key, foo)
165
166
167class TestRole(unittest.TestCase):
168
169    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
170    def test_load_role_with_tasks(self):
171
172        fake_loader = DictDataLoader({
173            "/usr/local/etc/ansible/roles/foo_tasks/tasks/main.yml": """
174            - shell: echo 'hello world'
175            """,
176        })
177
178        mock_play = MagicMock()
179        mock_play.ROLE_CACHE = {}
180
181        i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader)
182        r = Role.load(i, play=mock_play)
183
184        self.assertEqual(str(r), 'foo_tasks')
185        self.assertEqual(len(r._task_blocks), 1)
186        assert isinstance(r._task_blocks[0], Block)
187
188    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
189    def test_load_role_with_tasks_dir_vs_file(self):
190
191        fake_loader = DictDataLoader({
192            "/usr/local/etc/ansible/roles/foo_tasks/tasks/custom_main/foo.yml": """
193            - command: bar
194            """,
195            "/usr/local/etc/ansible/roles/foo_tasks/tasks/custom_main.yml": """
196            - command: baz
197            """,
198        })
199
200        mock_play = MagicMock()
201        mock_play.ROLE_CACHE = {}
202
203        i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader)
204        r = Role.load(i, play=mock_play, from_files=dict(tasks='custom_main'))
205
206        self.assertEqual(r._task_blocks[0]._ds[0]['command'], 'baz')
207
208    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
209    def test_load_role_with_handlers(self):
210
211        fake_loader = DictDataLoader({
212            "/usr/local/etc/ansible/roles/foo_handlers/handlers/main.yml": """
213            - name: test handler
214              shell: echo 'hello world'
215            """,
216        })
217
218        mock_play = MagicMock()
219        mock_play.ROLE_CACHE = {}
220
221        i = RoleInclude.load('foo_handlers', play=mock_play, loader=fake_loader)
222        r = Role.load(i, play=mock_play)
223
224        self.assertEqual(len(r._handler_blocks), 1)
225        assert isinstance(r._handler_blocks[0], Block)
226
227    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
228    def test_load_role_with_vars(self):
229
230        fake_loader = DictDataLoader({
231            "/usr/local/etc/ansible/roles/foo_vars/defaults/main.yml": """
232            foo: bar
233            """,
234            "/usr/local/etc/ansible/roles/foo_vars/vars/main.yml": """
235            foo: bam
236            """,
237        })
238
239        mock_play = MagicMock()
240        mock_play.ROLE_CACHE = {}
241
242        i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
243        r = Role.load(i, play=mock_play)
244
245        self.assertEqual(r._default_vars, dict(foo='bar'))
246        self.assertEqual(r._role_vars, dict(foo='bam'))
247
248    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
249    def test_load_role_with_vars_dirs(self):
250
251        fake_loader = DictDataLoader({
252            "/usr/local/etc/ansible/roles/foo_vars/defaults/main/foo.yml": """
253            foo: bar
254            """,
255            "/usr/local/etc/ansible/roles/foo_vars/vars/main/bar.yml": """
256            foo: bam
257            """,
258        })
259
260        mock_play = MagicMock()
261        mock_play.ROLE_CACHE = {}
262
263        i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
264        r = Role.load(i, play=mock_play)
265
266        self.assertEqual(r._default_vars, dict(foo='bar'))
267        self.assertEqual(r._role_vars, dict(foo='bam'))
268
269    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
270    def test_load_role_with_vars_nested_dirs(self):
271
272        fake_loader = DictDataLoader({
273            "/usr/local/etc/ansible/roles/foo_vars/defaults/main/foo/bar.yml": """
274            foo: bar
275            """,
276            "/usr/local/etc/ansible/roles/foo_vars/vars/main/bar/foo.yml": """
277            foo: bam
278            """,
279        })
280
281        mock_play = MagicMock()
282        mock_play.ROLE_CACHE = {}
283
284        i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
285        r = Role.load(i, play=mock_play)
286
287        self.assertEqual(r._default_vars, dict(foo='bar'))
288        self.assertEqual(r._role_vars, dict(foo='bam'))
289
290    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
291    def test_load_role_with_vars_nested_dirs_combined(self):
292
293        fake_loader = DictDataLoader({
294            "/usr/local/etc/ansible/roles/foo_vars/defaults/main/foo/bar.yml": """
295            foo: bar
296            a: 1
297            """,
298            "/usr/local/etc/ansible/roles/foo_vars/defaults/main/bar/foo.yml": """
299            foo: bam
300            b: 2
301            """,
302        })
303
304        mock_play = MagicMock()
305        mock_play.ROLE_CACHE = {}
306
307        i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
308        r = Role.load(i, play=mock_play)
309
310        self.assertEqual(r._default_vars, dict(foo='bar', a=1, b=2))
311
312    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
313    def test_load_role_with_vars_dir_vs_file(self):
314
315        fake_loader = DictDataLoader({
316            "/usr/local/etc/ansible/roles/foo_vars/vars/main/foo.yml": """
317            foo: bar
318            """,
319            "/usr/local/etc/ansible/roles/foo_vars/vars/main.yml": """
320            foo: bam
321            """,
322        })
323
324        mock_play = MagicMock()
325        mock_play.ROLE_CACHE = {}
326
327        i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
328        r = Role.load(i, play=mock_play)
329
330        self.assertEqual(r._role_vars, dict(foo='bam'))
331
332    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
333    def test_load_role_with_metadata(self):
334
335        fake_loader = DictDataLoader({
336            '/usr/local/etc/ansible/roles/foo_metadata/meta/main.yml': """
337                allow_duplicates: true
338                dependencies:
339                  - bar_metadata
340                galaxy_info:
341                  a: 1
342                  b: 2
343                  c: 3
344            """,
345            '/usr/local/etc/ansible/roles/bar_metadata/meta/main.yml': """
346                dependencies:
347                  - baz_metadata
348            """,
349            '/usr/local/etc/ansible/roles/baz_metadata/meta/main.yml': """
350                dependencies:
351                  - bam_metadata
352            """,
353            '/usr/local/etc/ansible/roles/bam_metadata/meta/main.yml': """
354                dependencies: []
355            """,
356            '/usr/local/etc/ansible/roles/bad1_metadata/meta/main.yml': """
357                1
358            """,
359            '/usr/local/etc/ansible/roles/bad2_metadata/meta/main.yml': """
360                foo: bar
361            """,
362            '/usr/local/etc/ansible/roles/recursive1_metadata/meta/main.yml': """
363                dependencies: ['recursive2_metadata']
364            """,
365            '/usr/local/etc/ansible/roles/recursive2_metadata/meta/main.yml': """
366                dependencies: ['recursive1_metadata']
367            """,
368        })
369
370        mock_play = MagicMock()
371        mock_play.collections = None
372        mock_play.ROLE_CACHE = {}
373
374        i = RoleInclude.load('foo_metadata', play=mock_play, loader=fake_loader)
375        r = Role.load(i, play=mock_play)
376
377        role_deps = r.get_direct_dependencies()
378
379        self.assertEqual(len(role_deps), 1)
380        self.assertEqual(type(role_deps[0]), Role)
381        self.assertEqual(len(role_deps[0].get_parents()), 1)
382        self.assertEqual(role_deps[0].get_parents()[0], r)
383        self.assertEqual(r._metadata.allow_duplicates, True)
384        self.assertEqual(r._metadata.galaxy_info, dict(a=1, b=2, c=3))
385
386        all_deps = r.get_all_dependencies()
387        self.assertEqual(len(all_deps), 3)
388        self.assertEqual(all_deps[0].get_name(), 'bam_metadata')
389        self.assertEqual(all_deps[1].get_name(), 'baz_metadata')
390        self.assertEqual(all_deps[2].get_name(), 'bar_metadata')
391
392        i = RoleInclude.load('bad1_metadata', play=mock_play, loader=fake_loader)
393        self.assertRaises(AnsibleParserError, Role.load, i, play=mock_play)
394
395        i = RoleInclude.load('bad2_metadata', play=mock_play, loader=fake_loader)
396        self.assertRaises(AnsibleParserError, Role.load, i, play=mock_play)
397
398        # TODO: re-enable this test once Ansible has proper role dep cycle detection
399        # that doesn't rely on stack overflows being recoverable (as they aren't in Py3.7+)
400        # see https://github.com/ansible/ansible/issues/61527
401        # i = RoleInclude.load('recursive1_metadata', play=mock_play, loader=fake_loader)
402        # self.assertRaises(AnsibleError, Role.load, i, play=mock_play)
403
404    @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
405    def test_load_role_complex(self):
406
407        # FIXME: add tests for the more complex uses of
408        #        params and tags/when statements
409
410        fake_loader = DictDataLoader({
411            "/usr/local/etc/ansible/roles/foo_complex/tasks/main.yml": """
412            - shell: echo 'hello world'
413            """,
414        })
415
416        mock_play = MagicMock()
417        mock_play.ROLE_CACHE = {}
418
419        i = RoleInclude.load(dict(role='foo_complex'), play=mock_play, loader=fake_loader)
420        r = Role.load(i, play=mock_play)
421
422        self.assertEqual(r.get_name(), "foo_complex")
423