1import random
2import sys
3
4from nose.tools import eq_, ok_, assert_true, assert_false, assert_equal
5import fudge
6from fudge import Fake, with_fakes, patched_context
7
8from fabric import decorators, tasks
9from fabric.state import env
10import fabric # for patching fabric.state.xxx
11from fabric.tasks import _parallel_tasks, requires_parallel, execute
12from fabric.context_managers import lcd, settings, hide
13
14from mock_streams import mock_streams
15
16
17#
18# Support
19#
20
21def fake_function(*args, **kwargs):
22    """
23    Returns a ``fudge.Fake`` exhibiting function-like attributes.
24
25    Passes in all args/kwargs to the ``fudge.Fake`` constructor. However, if
26    ``callable`` or ``expect_call`` kwargs are not given, ``callable`` will be
27    set to True by default.
28    """
29    # Must define __name__ to be compatible with function wrapping mechanisms
30    # like @wraps().
31    if 'callable' not in kwargs and 'expect_call' not in kwargs:
32        kwargs['callable'] = True
33    return Fake(*args, **kwargs).has_attr(__name__='fake')
34
35
36
37#
38# @task
39#
40
41def test_task_returns_an_instance_of_wrappedfunctask_object():
42    def foo():
43        pass
44    task = decorators.task(foo)
45    ok_(isinstance(task, tasks.WrappedCallableTask))
46
47
48def test_task_will_invoke_provided_class():
49    def foo(): pass
50    fake = Fake()
51    fake.expects("__init__").with_args(foo)
52    fudge.clear_calls()
53    fudge.clear_expectations()
54
55    foo = decorators.task(foo, task_class=fake)
56
57    fudge.verify()
58
59
60def test_task_passes_args_to_the_task_class():
61    random_vars = ("some text", random.randint(100, 200))
62    def foo(): pass
63
64    fake = Fake()
65    fake.expects("__init__").with_args(foo, *random_vars)
66    fudge.clear_calls()
67    fudge.clear_expectations()
68
69    foo = decorators.task(foo, task_class=fake, *random_vars)
70    fudge.verify()
71
72
73def test_passes_kwargs_to_the_task_class():
74    random_vars = {
75        "msg": "some text",
76        "number": random.randint(100, 200),
77    }
78    def foo(): pass
79
80    fake = Fake()
81    fake.expects("__init__").with_args(foo, **random_vars)
82    fudge.clear_calls()
83    fudge.clear_expectations()
84
85    foo = decorators.task(foo, task_class=fake, **random_vars)
86    fudge.verify()
87
88
89def test_integration_tests_for_invoked_decorator_with_no_args():
90    r = random.randint(100, 200)
91    @decorators.task()
92    def foo():
93        return r
94
95    eq_(r, foo())
96
97
98def test_integration_tests_for_decorator():
99    r = random.randint(100, 200)
100    @decorators.task(task_class=tasks.WrappedCallableTask)
101    def foo():
102        return r
103
104    eq_(r, foo())
105
106
107def test_original_non_invoked_style_task():
108    r = random.randint(100, 200)
109    @decorators.task
110    def foo():
111        return r
112
113    eq_(r, foo())
114
115
116
117#
118# @runs_once
119#
120
121@with_fakes
122def test_runs_once_runs_only_once():
123    """
124    @runs_once prevents decorated func from running >1 time
125    """
126    func = fake_function(expect_call=True).times_called(1)
127    task = decorators.runs_once(func)
128    for i in range(2):
129        task()
130
131
132def test_runs_once_returns_same_value_each_run():
133    """
134    @runs_once memoizes return value of decorated func
135    """
136    return_value = "foo"
137    task = decorators.runs_once(fake_function().returns(return_value))
138    for i in range(2):
139        eq_(task(), return_value)
140
141
142@decorators.runs_once
143def single_run():
144    pass
145
146def test_runs_once():
147    assert_false(hasattr(single_run, 'return_value'))
148    single_run()
149    assert_true(hasattr(single_run, 'return_value'))
150    assert_equal(None, single_run())
151
152
153
154#
155# @serial / @parallel
156#
157
158
159@decorators.serial
160def serial():
161    pass
162
163@decorators.serial
164@decorators.parallel
165def serial2():
166    pass
167
168@decorators.parallel
169@decorators.serial
170def serial3():
171    pass
172
173@decorators.parallel
174def parallel():
175    pass
176
177@decorators.parallel(pool_size=20)
178def parallel2():
179    pass
180
181fake_tasks = {
182    'serial': serial,
183    'serial2': serial2,
184    'serial3': serial3,
185    'parallel': parallel,
186    'parallel2': parallel2,
187}
188
189def parallel_task_helper(actual_tasks, expected):
190    commands_to_run = map(lambda x: [x], actual_tasks)
191    with patched_context(fabric.state, 'commands', fake_tasks):
192        eq_(_parallel_tasks(commands_to_run), expected)
193
194def test_parallel_tasks():
195    for desc, task_names, expected in (
196        ("One @serial-decorated task == no parallelism",
197            ['serial'], False),
198        ("One @parallel-decorated task == parallelism",
199            ['parallel'], True),
200        ("One @parallel-decorated and one @serial-decorated task == paralellism",
201            ['parallel', 'serial'], True),
202        ("Tasks decorated with both @serial and @parallel count as @parallel",
203            ['serial2', 'serial3'], True)
204    ):
205        parallel_task_helper.description = desc
206        yield parallel_task_helper, task_names, expected
207        del parallel_task_helper.description
208
209def test_parallel_wins_vs_serial():
210    """
211    @parallel takes precedence over @serial when both are used on one task
212    """
213    ok_(requires_parallel(serial2))
214    ok_(requires_parallel(serial3))
215
216@mock_streams('stdout')
217def test_global_parallel_honors_runs_once():
218    """
219    fab -P (or env.parallel) should honor @runs_once
220    """
221    @decorators.runs_once
222    def mytask():
223        print("yolo") # 'Carpe diem' for stupid people!
224    with settings(hide('everything'), parallel=True):
225        execute(mytask, hosts=['localhost', '127.0.0.1'])
226    result = sys.stdout.getvalue()
227    eq_(result, "yolo\n")
228    assert result != "yolo\nyolo\n"
229
230
231#
232# @roles
233#
234
235@decorators.roles('test')
236def use_roles():
237    pass
238
239def test_roles():
240    assert_true(hasattr(use_roles, 'roles'))
241    assert_equal(use_roles.roles, ['test'])
242
243
244
245#
246# @hosts
247#
248
249@decorators.hosts('test')
250def use_hosts():
251    pass
252
253def test_hosts():
254    assert_true(hasattr(use_hosts, 'hosts'))
255    assert_equal(use_hosts.hosts, ['test'])
256
257
258
259#
260# @with_settings
261#
262
263def test_with_settings_passes_env_vars_into_decorated_function():
264    env.value = True
265    random_return = random.randint(1000, 2000)
266    def some_task():
267        return env.value
268    decorated_task = decorators.with_settings(value=random_return)(some_task)
269    ok_(some_task(), msg="sanity check")
270    eq_(random_return, decorated_task())
271
272def test_with_settings_with_other_context_managers():
273    """
274    with_settings() should take other context managers, and use them with other
275    overrided key/value pairs.
276    """
277    env.testval1 = "outer 1"
278    prev_lcwd = env.lcwd
279
280    def some_task():
281        eq_(env.testval1, "inner 1")
282        ok_(env.lcwd.endswith("here")) # Should be the side-effect of adding cd to settings
283
284    decorated_task = decorators.with_settings(
285        lcd("here"),
286        testval1="inner 1"
287    )(some_task)
288    decorated_task()
289
290    ok_(env.testval1, "outer 1")
291    eq_(env.lcwd, prev_lcwd)
292