File: test_command.py

package info (click to toggle)
ros2-colcon-core 0.20.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,156 kB
  • sloc: python: 10,333; makefile: 7
file content (263 lines) | stat: -rw-r--r-- 8,926 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# Copyright 2016-2018 Dirk Thomas
# Licensed under the Apache License, Version 2.0

import os
import shutil
import signal
import sys
from tempfile import mkdtemp
from tempfile import TemporaryDirectory
from unittest.mock import Mock
from unittest.mock import patch

from colcon_core.command import CommandContext
from colcon_core.command import create_parser
from colcon_core.command import get_prog_name
from colcon_core.command import main
from colcon_core.command import verb_main
from colcon_core.environment_variable import EnvironmentVariable
from colcon_core.verb import VerbExtensionPoint
import pytest

from .extension_point_context import ExtensionPointContext


class Extension1(VerbExtensionPoint):
    pass


class Extension2:
    """Very long line so that the help text needs to be wrapped."""

    def main(self, *, context):
        pass  # pragma: no cover


class Extension3(VerbExtensionPoint):

    def add_arguments(self, *, parser):
        raise RuntimeError('custom exception')


@patch('colcon_core.output_style.get_output_style_extensions', dict)
def test_main():
    with ExtensionPointContext(
        extension1=Extension1, extension2=Extension2, extension3=Extension3
    ):
        with patch(
            'colcon_core.argument_parser.get_argument_parser_extensions',
            return_value={}
        ):
            with pytest.raises(SystemExit) as e:
                main(argv=['--help'])
            assert e.value.code == 0

            with pytest.raises(SystemExit) as e:
                main(argv=['--log-level', 'invalid'])
            assert e.value.code == 2

            # avoid creating log directory in the package directory
            log_base = mkdtemp(prefix='test_colcon_')
            argv = ['--log-base', log_base]
            try:
                main(argv=argv + ['--log-level', 'info'])

                with patch(
                    'colcon_core.command.load_extension_points',
                    return_value={
                        'key1': EnvironmentVariable('name', 'description'),
                        'key2': EnvironmentVariable(
                            'extra_long_name_to_wrap help',
                            'extra long description text to require a wrap of '
                            'the help text not_only_on_spaces_but_also_forced_'
                            'within_a_very_long_consecutive_word'),
                    }
                ):
                    main(argv=argv + ['extension1'])
            finally:
                # the logging subsystem might still have file handles pending
                # therefore only try to delete the temporary directory
                shutil.rmtree(log_base, ignore_errors=True)

        # catch KeyboardInterrupt and return SIGINT error code
        with patch('colcon_core.command._main', return_value=0) as _main:
            _main.side_effect = KeyboardInterrupt()
            rc = main()
            assert rc == signal.SIGINT


def test_main_no_verbs_or_env():
    with ExtensionPointContext():
        with patch(
            'colcon_core.command.load_extension_points',
            return_value={},
        ):
            with pytest.raises(SystemExit) as e:
                main(argv=['--help'])
            assert e.value.code == 0


def test_main_default_verb():
    with ExtensionPointContext():
        with patch(
            'colcon_core.argument_parser.get_argument_parser_extensions',
            return_value={}
        ):
            with pytest.raises(SystemExit) as e:
                main(argv=['--help'], default_verb=Extension1)
            assert e.value.code == 0

            with pytest.raises(SystemExit) as e:
                main(
                    argv=['--log-level', 'invalid'],
                    default_verb=Extension1)
            assert e.value.code == 2

            with patch.object(Extension1, 'main', return_value=0) as mock_main:
                assert not main(
                    argv=['--log-base', '/dev/null'],
                    default_verb=Extension1)
                mock_main.assert_called_once()


def test_create_parser():
    with ExtensionPointContext():
        parser = create_parser('colcon_core.environment_variable')

    parser.add_argument('--foo', nargs='*', type=str.lstrip)
    args = parser.parse_args(['--foo', '--bar', '--baz'])
    assert args.foo == ['--bar', '--baz']

    parser.add_argument('--baz', action='store_true')
    args = parser.parse_args(['--foo', '--bar', '--baz'])
    assert args.foo == ['--bar']
    assert args.baz is True

    args = parser.parse_args(['--foo', '--bar', ' --baz'])
    assert args.foo == ['--bar', '--baz']

    argv = sys.argv
    sys.argv = ['/some/path/prog_name/__main__.py'] + sys.argv[1:]
    with ExtensionPointContext():
        parser = create_parser('colcon_core.environment_variable')
    sys.argv = argv
    assert parser.prog == 'prog_name'


class Object(object):
    pass


def test_verb_main():
    args = Object()
    args.verb_name = 'verb_name'
    logger = Object()
    logger.error = Mock()

    # pass through return code
    args.main = Mock(return_value=42)
    context = CommandContext(command_name='command_name', args=args)
    rc = verb_main(context, logger)
    assert rc == args.main.return_value
    logger.error.assert_not_called()

    # catch RuntimeError and output error message
    args.main.side_effect = RuntimeError('known error condition')
    rc = verb_main(context, logger)
    assert rc
    logger.error.assert_called_once_with(
        'command_name verb_name: known error condition')
    logger.error.reset_mock()

    # catch Exception and output error message including traceback
    args.main.side_effect = Exception('custom error message')
    rc = verb_main(context, logger)
    assert rc
    assert logger.error.call_count == 1
    assert len(logger.error.call_args[0]) == 1
    assert logger.error.call_args[0][0].startswith(
        'command_name verb_name: custom error message\n')
    assert 'Exception: custom error message' in logger.error.call_args[0][0]


def test_prog_name_module():
    argv = [os.path.join('foo', 'bar', '__main__.py')]
    with patch('colcon_core.command.sys.argv', argv):
        # prog should be the module containing __main__.py
        assert get_prog_name() == 'bar'


def test_prog_name_on_path():
    # use __file__ since we know it exists
    argv = [__file__]
    with patch('colcon_core.command.sys.argv', argv):
        with patch(
            'colcon_core.command.shutil.which',
            return_value=__file__
        ):
            # prog should be shortened to the basename
            assert get_prog_name() == 'test_command.py'


def test_prog_name_not_on_path():
    # use __file__ since we know it exists
    argv = [__file__]
    with patch('colcon_core.command.sys.argv', argv):
        with patch('colcon_core.command.shutil.which', return_value=None):
            # prog should remain unchanged
            assert get_prog_name() == __file__


def test_prog_name_different_on_path():
    # use __file__ since we know it exists
    argv = [__file__]
    with patch('colcon_core.command.sys.argv', argv):
        with patch(
            'colcon_core.command.shutil.which',
            return_value=sys.executable
        ):
            # prog should remain unchanged
            assert get_prog_name() == __file__


def test_prog_name_not_a_file():
    # pick some file that doesn't actually exist on disk
    no_such_file = os.path.join(__file__, 'foobar')
    argv = [no_such_file]
    with patch('colcon_core.command.sys.argv', argv):
        with patch(
            'colcon_core.command.shutil.which',
            return_value=no_such_file
        ):
            # prog should remain unchanged
            assert get_prog_name() == no_such_file


@pytest.mark.skipif(sys.platform == 'win32', reason='Symlinks not supported.')
def test_prog_name_symlink():
    # use __file__ since we know it exists
    with TemporaryDirectory(prefix='test_colcon_') as temp_dir:
        linked_file = os.path.join(temp_dir, 'test_command.py')
        os.symlink(__file__, linked_file)

        argv = [linked_file]
        with patch('colcon_core.command.sys.argv', argv):
            with patch(
                'colcon_core.command.shutil.which',
                return_value=__file__
            ):
                # prog should be shortened to the basename
                assert get_prog_name() == 'test_command.py'


@pytest.mark.skipif(sys.platform != 'win32', reason='Only valid on Windows.')
def test_prog_name_easy_install():
    # use __file__ since we know it exists
    argv = [__file__[:-3]]
    with patch('colcon_core.command.sys.argv', argv):
        with patch(
            'colcon_core.command.shutil.which',
            return_value=__file__
        ):
            # prog should be shortened to the basename
            assert get_prog_name() == 'test_command'