summaryrefslogtreecommitdiffstats
path: root/test/units/module_utils/facts/test_timeout.py
blob: cc64f9f0492a3538fa5b03563349c8c3b362e745 (plain)
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
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Toshio Kuratomi <tkuratomi@ansible.com>
# Copyright: Contributors to the Ansible project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import annotations

import sys
import time

import pytest

from ansible.module_utils.facts import timeout


@pytest.fixture
def set_gather_timeout_higher():
    default_timeout = timeout.GATHER_TIMEOUT
    timeout.GATHER_TIMEOUT = 5
    yield
    timeout.GATHER_TIMEOUT = default_timeout


@pytest.fixture
def set_gather_timeout_lower():
    default_timeout = timeout.GATHER_TIMEOUT
    timeout.GATHER_TIMEOUT = 2
    yield
    timeout.GATHER_TIMEOUT = default_timeout


@timeout.timeout
def sleep_amount_implicit(amount):
    # implicit refers to the lack of argument to the decorator
    time.sleep(amount)
    return 'Succeeded after {0} sec'.format(amount)


@timeout.timeout(timeout.DEFAULT_GATHER_TIMEOUT + 5)
def sleep_amount_explicit_higher(amount):
    # explicit refers to the argument to the decorator
    time.sleep(amount)
    return 'Succeeded after {0} sec'.format(amount)


@timeout.timeout(2)
def sleep_amount_explicit_lower(amount):
    # explicit refers to the argument to the decorator
    time.sleep(amount)
    return 'Succeeded after {0} sec'.format(amount)


#
# Tests for how the timeout decorator is specified
#

def test_defaults_still_within_bounds():
    # If the default changes outside of these bounds, some of the tests will
    # no longer test the right thing.  Need to review and update the timeouts
    # in the other tests if this fails
    assert timeout.DEFAULT_GATHER_TIMEOUT >= 4


def test_implicit_file_default_succeeds():
    # amount checked must be less than DEFAULT_GATHER_TIMEOUT
    assert sleep_amount_implicit(1) == 'Succeeded after 1 sec'


def test_implicit_file_default_timesout(monkeypatch):
    monkeypatch.setattr(timeout, 'DEFAULT_GATHER_TIMEOUT', 1)
    # sleep_time is greater than the default
    sleep_time = timeout.DEFAULT_GATHER_TIMEOUT + 1
    with pytest.raises(timeout.TimeoutError, match=r"^Timer expired after"):
        sleep_amount_implicit(sleep_time)


def test_implicit_file_overridden_succeeds(set_gather_timeout_higher):
    # Set sleep_time greater than the default timeout and less than our new timeout
    sleep_time = 3
    assert sleep_amount_implicit(sleep_time) == 'Succeeded after {0} sec'.format(sleep_time)


def test_implicit_file_overridden_timesout(set_gather_timeout_lower):
    # Set sleep_time greater than our new timeout but less than the default
    sleep_time = 3
    with pytest.raises(timeout.TimeoutError, match=r"^Timer expired after"):
        sleep_amount_implicit(sleep_time)


def test_explicit_succeeds(monkeypatch):
    monkeypatch.setattr(timeout, 'DEFAULT_GATHER_TIMEOUT', 1)
    # Set sleep_time greater than the default timeout and less than our new timeout
    sleep_time = 2
    assert sleep_amount_explicit_higher(sleep_time) == 'Succeeded after {0} sec'.format(sleep_time)


def test_explicit_timeout():
    # Set sleep_time greater than our new timeout but less than the default
    sleep_time = 3
    with pytest.raises(timeout.TimeoutError, match=r"^Timer expired after"):
        sleep_amount_explicit_lower(sleep_time)


#
# Test that exception handling works
#

@timeout.timeout(1)
def function_times_out():
    time.sleep(2)


# This is just about the same test as function_times_out but uses a separate process which is where
# we normally have our timeouts.  It's more of an integration test than a unit test.
@timeout.timeout(1)
def function_times_out_in_run_command(am):
    am.run_command([sys.executable, '-c', 'import time ; time.sleep(2)'])


@timeout.timeout(1)
def function_other_timeout():
    raise TimeoutError('Vanilla Timeout')


@timeout.timeout(1)
def function_raises():
    return 1 / 0


@timeout.timeout(1)
def function_catches_all_exceptions():
    try:
        time.sleep(10)
    except BaseException:
        raise RuntimeError('We should not have gotten here')


def test_timeout_raises_timeout():
    with pytest.raises(timeout.TimeoutError, match=r"^Timer expired after"):
        function_times_out()


@pytest.mark.parametrize('stdin', ({},), indirect=['stdin'])
def test_timeout_raises_timeout_integration_test(am):
    with pytest.raises(timeout.TimeoutError, match=r"^Timer expired after"):
        function_times_out_in_run_command(am)


def test_timeout_raises_other_exception():
    with pytest.raises(ZeroDivisionError, match=r"^division by"):
        function_raises()


def test_exception_not_caught_by_called_code():
    with pytest.raises(timeout.TimeoutError, match=r"^Timer expired after"):
        function_catches_all_exceptions()