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
22
23from units.compat import unittest
24from units.compat.builtins import BUILTINS
25from units.compat.mock import mock_open, patch
26from ansible.errors import AnsibleError
27from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
28
29
30class TestErrors(unittest.TestCase):
31
32    def setUp(self):
33        self.message = 'This is the error message'
34        self.unicode_message = 'This is an error with \xf0\x9f\x98\xa8 in it'
35
36        self.obj = AnsibleBaseYAMLObject()
37
38    def test_basic_error(self):
39        e = AnsibleError(self.message)
40        self.assertEqual(e.message, self.message)
41        self.assertEqual(e.__repr__(), self.message)
42
43    def test_basic_unicode_error(self):
44        e = AnsibleError(self.unicode_message)
45        self.assertEqual(e.message, self.unicode_message)
46        self.assertEqual(e.__repr__(), self.unicode_message)
47
48    @patch.object(AnsibleError, '_get_error_lines_from_file')
49    def test_error_with_kv(self, mock_method):
50        ''' This tests a task with both YAML and k=v syntax
51
52        - lineinfile: line=foo path=bar
53            line: foo
54
55        An accurate error message and position indicator are expected.
56
57        _get_error_lines_from_file() returns (target_line, prev_line)
58        '''
59
60        self.obj.ansible_pos = ('foo.yml', 2, 1)
61
62        mock_method.return_value = ['    line: foo\n', '- lineinfile: line=foo path=bar\n']
63
64        e = AnsibleError(self.message, self.obj)
65        self.assertEqual(
66            e.message,
67            ("This is the error message\n\nThe error appears to be in 'foo.yml': line 1, column 19, but may\nbe elsewhere in the "
68             "file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n- lineinfile: line=foo path=bar\n"
69             "                  ^ here\n\n"
70             "There appears to be both 'k=v' shorthand syntax and YAML in this task. Only one syntax may be used.\n")
71        )
72
73    @patch.object(AnsibleError, '_get_error_lines_from_file')
74    def test_error_with_object(self, mock_method):
75        self.obj.ansible_pos = ('foo.yml', 1, 1)
76
77        mock_method.return_value = ('this is line 1\n', '')
78        e = AnsibleError(self.message, self.obj)
79
80        self.assertEqual(
81            e.message,
82            ("This is the error message\n\nThe error appears to be in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the file depending on the "
83             "exact syntax problem.\n\nThe offending line appears to be:\n\n\nthis is line 1\n^ here\n")
84        )
85
86    def test_get_error_lines_from_file(self):
87        m = mock_open()
88        m.return_value.readlines.return_value = ['this is line 1\n']
89
90        with patch('{0}.open'.format(BUILTINS), m):
91            # this line will be found in the file
92            self.obj.ansible_pos = ('foo.yml', 1, 1)
93            e = AnsibleError(self.message, self.obj)
94            self.assertEqual(
95                e.message,
96                ("This is the error message\n\nThe error appears to be in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the file depending on "
97                 "the exact syntax problem.\n\nThe offending line appears to be:\n\n\nthis is line 1\n^ here\n")
98            )
99
100            with patch('ansible.errors.to_text', side_effect=IndexError('Raised intentionally')):
101                # raise an IndexError
102                self.obj.ansible_pos = ('foo.yml', 2, 1)
103                e = AnsibleError(self.message, self.obj)
104                self.assertEqual(
105                    e.message,
106                    ("This is the error message\n\nThe error appears to be in 'foo.yml': line 2, column 1, but may\nbe elsewhere in the file depending on "
107                     "the exact syntax problem.\n\n(specified line no longer in file, maybe it changed?)")
108                )
109
110        m = mock_open()
111        m.return_value.readlines.return_value = ['this line has unicode \xf0\x9f\x98\xa8 in it!\n']
112
113        with patch('{0}.open'.format(BUILTINS), m):
114            # this line will be found in the file
115            self.obj.ansible_pos = ('foo.yml', 1, 1)
116            e = AnsibleError(self.unicode_message, self.obj)
117            self.assertEqual(
118                e.message,
119                ("This is an error with \xf0\x9f\x98\xa8 in it\n\nThe error appears to be in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the "
120                 "file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\nthis line has unicode \xf0\x9f\x98\xa8 in it!\n^ "
121                 "here\n")
122            )
123
124    def test_get_error_lines_error_in_last_line(self):
125        m = mock_open()
126        m.return_value.readlines.return_value = ['this is line 1\n', 'this is line 2\n', 'this is line 3\n']
127
128        with patch('{0}.open'.format(BUILTINS), m):
129            # If the error occurs in the last line of the file, use the correct index to get the line
130            # and avoid the IndexError
131            self.obj.ansible_pos = ('foo.yml', 4, 1)
132            e = AnsibleError(self.message, self.obj)
133            self.assertEqual(
134                e.message,
135                ("This is the error message\n\nThe error appears to be in 'foo.yml': line 4, column 1, but may\nbe elsewhere in the file depending on "
136                 "the exact syntax problem.\n\nThe offending line appears to be:\n\nthis is line 2\nthis is line 3\n^ here\n")
137            )
138
139    def test_get_error_lines_error_empty_lines_around_error(self):
140        """Test that trailing whitespace after the error is removed"""
141        m = mock_open()
142        m.return_value.readlines.return_value = ['this is line 1\n', 'this is line 2\n', 'this is line 3\n', '  \n', '   \n', ' ']
143
144        with patch('{0}.open'.format(BUILTINS), m):
145            self.obj.ansible_pos = ('foo.yml', 5, 1)
146            e = AnsibleError(self.message, self.obj)
147            self.assertEqual(
148                e.message,
149                ("This is the error message\n\nThe error appears to be in 'foo.yml': line 5, column 1, but may\nbe elsewhere in the file depending on "
150                 "the exact syntax problem.\n\nThe offending line appears to be:\n\nthis is line 2\nthis is line 3\n^ here\n")
151            )
152