##// END OF EJS Templates
hooks: added debug logs.
marcink -
r2135:d1c488ba default
parent child Browse files
Show More
@@ -1,239 +1,241 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22 import logging
22 import logging
23 import traceback
23 import traceback
24 import threading
24 import threading
25 from BaseHTTPServer import BaseHTTPRequestHandler
25 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
26 from SocketServer import TCPServer
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode.model import meta
29 from rhodecode.model import meta
30 from rhodecode.lib.base import bootstrap_request
30 from rhodecode.lib.base import bootstrap_request
31 from rhodecode.lib import hooks_base
31 from rhodecode.lib import hooks_base
32 from rhodecode.lib.utils2 import AttributeDict
32 from rhodecode.lib.utils2 import AttributeDict
33
33
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class HooksHttpHandler(BaseHTTPRequestHandler):
38 class HooksHttpHandler(BaseHTTPRequestHandler):
39 def do_POST(self):
39 def do_POST(self):
40 method, extras = self._read_request()
40 method, extras = self._read_request()
41 try:
41 try:
42 result = self._call_hook(method, extras)
42 result = self._call_hook(method, extras)
43 except Exception as e:
43 except Exception as e:
44 exc_tb = traceback.format_exc()
44 exc_tb = traceback.format_exc()
45 result = {
45 result = {
46 'exception': e.__class__.__name__,
46 'exception': e.__class__.__name__,
47 'exception_traceback': exc_tb,
47 'exception_traceback': exc_tb,
48 'exception_args': e.args
48 'exception_args': e.args
49 }
49 }
50 self._write_response(result)
50 self._write_response(result)
51
51
52 def _read_request(self):
52 def _read_request(self):
53 length = int(self.headers['Content-Length'])
53 length = int(self.headers['Content-Length'])
54 body = self.rfile.read(length).decode('utf-8')
54 body = self.rfile.read(length).decode('utf-8')
55 data = json.loads(body)
55 data = json.loads(body)
56 return data['method'], data['extras']
56 return data['method'], data['extras']
57
57
58 def _write_response(self, result):
58 def _write_response(self, result):
59 self.send_response(200)
59 self.send_response(200)
60 self.send_header("Content-type", "text/json")
60 self.send_header("Content-type", "text/json")
61 self.end_headers()
61 self.end_headers()
62 self.wfile.write(json.dumps(result))
62 self.wfile.write(json.dumps(result))
63
63
64 def _call_hook(self, method, extras):
64 def _call_hook(self, method, extras):
65 hooks = Hooks()
65 hooks = Hooks()
66 try:
66 try:
67 result = getattr(hooks, method)(extras)
67 result = getattr(hooks, method)(extras)
68 finally:
68 finally:
69 meta.Session.remove()
69 meta.Session.remove()
70 return result
70 return result
71
71
72 def log_message(self, format, *args):
72 def log_message(self, format, *args):
73 """
73 """
74 This is an overridden method of BaseHTTPRequestHandler which logs using
74 This is an overridden method of BaseHTTPRequestHandler which logs using
75 logging library instead of writing directly to stderr.
75 logging library instead of writing directly to stderr.
76 """
76 """
77
77
78 message = format % args
78 message = format % args
79
79
80 # TODO: mikhail: add different log levels support
80 # TODO: mikhail: add different log levels support
81 log.debug(
81 log.debug(
82 "%s - - [%s] %s", self.client_address[0],
82 "%s - - [%s] %s", self.client_address[0],
83 self.log_date_time_string(), message)
83 self.log_date_time_string(), message)
84
84
85
85
86 class DummyHooksCallbackDaemon(object):
86 class DummyHooksCallbackDaemon(object):
87 def __init__(self):
87 def __init__(self):
88 self.hooks_module = Hooks.__module__
88 self.hooks_module = Hooks.__module__
89
89
90 def __enter__(self):
90 def __enter__(self):
91 log.debug('Running dummy hooks callback daemon')
91 log.debug('Running dummy hooks callback daemon')
92 return self
92 return self
93
93
94 def __exit__(self, exc_type, exc_val, exc_tb):
94 def __exit__(self, exc_type, exc_val, exc_tb):
95 log.debug('Exiting dummy hooks callback daemon')
95 log.debug('Exiting dummy hooks callback daemon')
96
96
97
97
98 class ThreadedHookCallbackDaemon(object):
98 class ThreadedHookCallbackDaemon(object):
99
99
100 _callback_thread = None
100 _callback_thread = None
101 _daemon = None
101 _daemon = None
102 _done = False
102 _done = False
103
103
104 def __init__(self):
104 def __init__(self):
105 self._prepare()
105 self._prepare()
106
106
107 def __enter__(self):
107 def __enter__(self):
108 self._run()
108 self._run()
109 return self
109 return self
110
110
111 def __exit__(self, exc_type, exc_val, exc_tb):
111 def __exit__(self, exc_type, exc_val, exc_tb):
112 log.debug('Callback daemon exiting now...')
112 self._stop()
113 self._stop()
113
114
114 def _prepare(self):
115 def _prepare(self):
115 raise NotImplementedError()
116 raise NotImplementedError()
116
117
117 def _run(self):
118 def _run(self):
118 raise NotImplementedError()
119 raise NotImplementedError()
119
120
120 def _stop(self):
121 def _stop(self):
121 raise NotImplementedError()
122 raise NotImplementedError()
122
123
123
124
124 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
125 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
125 """
126 """
126 Context manager which will run a callback daemon in a background thread.
127 Context manager which will run a callback daemon in a background thread.
127 """
128 """
128
129
129 hooks_uri = None
130 hooks_uri = None
130
131
131 IP_ADDRESS = '127.0.0.1'
132 IP_ADDRESS = '127.0.0.1'
132
133
133 # From Python docs: Polling reduces our responsiveness to a shutdown
134 # From Python docs: Polling reduces our responsiveness to a shutdown
134 # request and wastes cpu at all other times.
135 # request and wastes cpu at all other times.
135 POLL_INTERVAL = 0.1
136 POLL_INTERVAL = 0.1
136
137
137 def _prepare(self):
138 def _prepare(self):
138 log.debug("Preparing callback daemon and registering hook object")
139 log.debug("Preparing HTTP callback daemon and registering hook object")
139
140
140 self._done = False
141 self._done = False
141 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
142 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
142 _, port = self._daemon.server_address
143 _, port = self._daemon.server_address
143 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
144 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
144
145
145 log.debug("Hooks uri is: %s", self.hooks_uri)
146 log.debug("Hooks uri is: %s", self.hooks_uri)
146
147
147 def _run(self):
148 def _run(self):
148 log.debug("Running event loop of callback daemon in background thread")
149 log.debug("Running event loop of callback daemon in background thread")
149 callback_thread = threading.Thread(
150 callback_thread = threading.Thread(
150 target=self._daemon.serve_forever,
151 target=self._daemon.serve_forever,
151 kwargs={'poll_interval': self.POLL_INTERVAL})
152 kwargs={'poll_interval': self.POLL_INTERVAL})
152 callback_thread.daemon = True
153 callback_thread.daemon = True
153 callback_thread.start()
154 callback_thread.start()
154 self._callback_thread = callback_thread
155 self._callback_thread = callback_thread
155
156
156 def _stop(self):
157 def _stop(self):
157 log.debug("Waiting for background thread to finish.")
158 log.debug("Waiting for background thread to finish.")
158 self._daemon.shutdown()
159 self._daemon.shutdown()
159 self._callback_thread.join()
160 self._callback_thread.join()
160 self._daemon = None
161 self._daemon = None
161 self._callback_thread = None
162 self._callback_thread = None
162
163
163
164
164 def prepare_callback_daemon(extras, protocol, use_direct_calls):
165 def prepare_callback_daemon(extras, protocol, use_direct_calls):
165 callback_daemon = None
166 callback_daemon = None
166
167
167 if use_direct_calls:
168 if use_direct_calls:
168 callback_daemon = DummyHooksCallbackDaemon()
169 callback_daemon = DummyHooksCallbackDaemon()
169 extras['hooks_module'] = callback_daemon.hooks_module
170 extras['hooks_module'] = callback_daemon.hooks_module
170 else:
171 else:
171 if protocol == 'http':
172 if protocol == 'http':
172 callback_daemon = HttpHooksCallbackDaemon()
173 callback_daemon = HttpHooksCallbackDaemon()
173 else:
174 else:
174 log.error('Unsupported callback daemon protocol "%s"', protocol)
175 log.error('Unsupported callback daemon protocol "%s"', protocol)
175 raise Exception('Unsupported callback daemon protocol.')
176 raise Exception('Unsupported callback daemon protocol.')
176
177
177 extras['hooks_uri'] = callback_daemon.hooks_uri
178 extras['hooks_uri'] = callback_daemon.hooks_uri
178 extras['hooks_protocol'] = protocol
179 extras['hooks_protocol'] = protocol
179
180
181 log.debug('Prepared a callback daemon: %s', callback_daemon)
180 return callback_daemon, extras
182 return callback_daemon, extras
181
183
182
184
183 class Hooks(object):
185 class Hooks(object):
184 """
186 """
185 Exposes the hooks for remote call backs
187 Exposes the hooks for remote call backs
186 """
188 """
187
189
188 def repo_size(self, extras):
190 def repo_size(self, extras):
189 log.debug("Called repo_size of %s object", self)
191 log.debug("Called repo_size of %s object", self)
190 return self._call_hook(hooks_base.repo_size, extras)
192 return self._call_hook(hooks_base.repo_size, extras)
191
193
192 def pre_pull(self, extras):
194 def pre_pull(self, extras):
193 log.debug("Called pre_pull of %s object", self)
195 log.debug("Called pre_pull of %s object", self)
194 return self._call_hook(hooks_base.pre_pull, extras)
196 return self._call_hook(hooks_base.pre_pull, extras)
195
197
196 def post_pull(self, extras):
198 def post_pull(self, extras):
197 log.debug("Called post_pull of %s object", self)
199 log.debug("Called post_pull of %s object", self)
198 return self._call_hook(hooks_base.post_pull, extras)
200 return self._call_hook(hooks_base.post_pull, extras)
199
201
200 def pre_push(self, extras):
202 def pre_push(self, extras):
201 log.debug("Called pre_push of %s object", self)
203 log.debug("Called pre_push of %s object", self)
202 return self._call_hook(hooks_base.pre_push, extras)
204 return self._call_hook(hooks_base.pre_push, extras)
203
205
204 def post_push(self, extras):
206 def post_push(self, extras):
205 log.debug("Called post_push of %s object", self)
207 log.debug("Called post_push of %s object", self)
206 return self._call_hook(hooks_base.post_push, extras)
208 return self._call_hook(hooks_base.post_push, extras)
207
209
208 def _call_hook(self, hook, extras):
210 def _call_hook(self, hook, extras):
209 extras = AttributeDict(extras)
211 extras = AttributeDict(extras)
210
212
211 extras.request = bootstrap_request(
213 extras.request = bootstrap_request(
212 application_url=extras['server_url'])
214 application_url=extras['server_url'])
213
215
214 try:
216 try:
215 result = hook(extras)
217 result = hook(extras)
216 except Exception as error:
218 except Exception as error:
217 exc_tb = traceback.format_exc()
219 exc_tb = traceback.format_exc()
218 log.exception('Exception when handling hook %s', hook)
220 log.exception('Exception when handling hook %s', hook)
219 error_args = error.args
221 error_args = error.args
220 return {
222 return {
221 'status': 128,
223 'status': 128,
222 'output': '',
224 'output': '',
223 'exception': type(error).__name__,
225 'exception': type(error).__name__,
224 'exception_traceback': exc_tb,
226 'exception_traceback': exc_tb,
225 'exception_args': error_args,
227 'exception_args': error_args,
226 }
228 }
227 finally:
229 finally:
228 meta.Session.remove()
230 meta.Session.remove()
229
231
230 return {
232 return {
231 'status': result.status,
233 'status': result.status,
232 'output': result.output,
234 'output': result.output,
233 }
235 }
234
236
235 def __enter__(self):
237 def __enter__(self):
236 return self
238 return self
237
239
238 def __exit__(self, exc_type, exc_val, exc_tb):
240 def __exit__(self, exc_type, exc_val, exc_tb):
239 pass
241 pass
@@ -1,321 +1,321 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22 import logging
22 import logging
23 from StringIO import StringIO
23 from StringIO import StringIO
24
24
25 import mock
25 import mock
26 import pytest
26 import pytest
27
27
28 from rhodecode.lib import hooks_daemon
28 from rhodecode.lib import hooks_daemon
29 from rhodecode.tests.utils import assert_message_in_log
29 from rhodecode.tests.utils import assert_message_in_log
30
30
31
31
32 class TestDummyHooksCallbackDaemon(object):
32 class TestDummyHooksCallbackDaemon(object):
33 def test_hooks_module_path_set_properly(self):
33 def test_hooks_module_path_set_properly(self):
34 daemon = hooks_daemon.DummyHooksCallbackDaemon()
34 daemon = hooks_daemon.DummyHooksCallbackDaemon()
35 assert daemon.hooks_module == 'rhodecode.lib.hooks_daemon'
35 assert daemon.hooks_module == 'rhodecode.lib.hooks_daemon'
36
36
37 def test_logs_entering_the_hook(self):
37 def test_logs_entering_the_hook(self):
38 daemon = hooks_daemon.DummyHooksCallbackDaemon()
38 daemon = hooks_daemon.DummyHooksCallbackDaemon()
39 with mock.patch.object(hooks_daemon.log, 'debug') as log_mock:
39 with mock.patch.object(hooks_daemon.log, 'debug') as log_mock:
40 with daemon as return_value:
40 with daemon as return_value:
41 log_mock.assert_called_once_with(
41 log_mock.assert_called_once_with(
42 'Running dummy hooks callback daemon')
42 'Running dummy hooks callback daemon')
43 assert return_value == daemon
43 assert return_value == daemon
44
44
45 def test_logs_exiting_the_hook(self):
45 def test_logs_exiting_the_hook(self):
46 daemon = hooks_daemon.DummyHooksCallbackDaemon()
46 daemon = hooks_daemon.DummyHooksCallbackDaemon()
47 with mock.patch.object(hooks_daemon.log, 'debug') as log_mock:
47 with mock.patch.object(hooks_daemon.log, 'debug') as log_mock:
48 with daemon:
48 with daemon:
49 pass
49 pass
50 log_mock.assert_called_with('Exiting dummy hooks callback daemon')
50 log_mock.assert_called_with('Exiting dummy hooks callback daemon')
51
51
52
52
53 class TestHooks(object):
53 class TestHooks(object):
54 def test_hooks_can_be_used_as_a_context_processor(self):
54 def test_hooks_can_be_used_as_a_context_processor(self):
55 hooks = hooks_daemon.Hooks()
55 hooks = hooks_daemon.Hooks()
56 with hooks as return_value:
56 with hooks as return_value:
57 pass
57 pass
58 assert hooks == return_value
58 assert hooks == return_value
59
59
60
60
61 class TestHooksHttpHandler(object):
61 class TestHooksHttpHandler(object):
62 def test_read_request_parses_method_name_and_arguments(self):
62 def test_read_request_parses_method_name_and_arguments(self):
63 data = {
63 data = {
64 'method': 'test',
64 'method': 'test',
65 'extras': {
65 'extras': {
66 'param1': 1,
66 'param1': 1,
67 'param2': 'a'
67 'param2': 'a'
68 }
68 }
69 }
69 }
70 request = self._generate_post_request(data)
70 request = self._generate_post_request(data)
71 hooks_patcher = mock.patch.object(
71 hooks_patcher = mock.patch.object(
72 hooks_daemon.Hooks, data['method'], create=True, return_value=1)
72 hooks_daemon.Hooks, data['method'], create=True, return_value=1)
73
73
74 with hooks_patcher as hooks_mock:
74 with hooks_patcher as hooks_mock:
75 MockServer(hooks_daemon.HooksHttpHandler, request)
75 MockServer(hooks_daemon.HooksHttpHandler, request)
76
76
77 hooks_mock.assert_called_once_with(data['extras'])
77 hooks_mock.assert_called_once_with(data['extras'])
78
78
79 def test_hooks_serialized_result_is_returned(self):
79 def test_hooks_serialized_result_is_returned(self):
80 request = self._generate_post_request({})
80 request = self._generate_post_request({})
81 rpc_method = 'test'
81 rpc_method = 'test'
82 hook_result = {
82 hook_result = {
83 'first': 'one',
83 'first': 'one',
84 'second': 2
84 'second': 2
85 }
85 }
86 read_patcher = mock.patch.object(
86 read_patcher = mock.patch.object(
87 hooks_daemon.HooksHttpHandler, '_read_request',
87 hooks_daemon.HooksHttpHandler, '_read_request',
88 return_value=(rpc_method, {}))
88 return_value=(rpc_method, {}))
89 hooks_patcher = mock.patch.object(
89 hooks_patcher = mock.patch.object(
90 hooks_daemon.Hooks, rpc_method, create=True,
90 hooks_daemon.Hooks, rpc_method, create=True,
91 return_value=hook_result)
91 return_value=hook_result)
92
92
93 with read_patcher, hooks_patcher:
93 with read_patcher, hooks_patcher:
94 server = MockServer(hooks_daemon.HooksHttpHandler, request)
94 server = MockServer(hooks_daemon.HooksHttpHandler, request)
95
95
96 expected_result = json.dumps(hook_result)
96 expected_result = json.dumps(hook_result)
97 assert server.request.output_stream.buflist[-1] == expected_result
97 assert server.request.output_stream.buflist[-1] == expected_result
98
98
99 def test_exception_is_returned_in_response(self):
99 def test_exception_is_returned_in_response(self):
100 request = self._generate_post_request({})
100 request = self._generate_post_request({})
101 rpc_method = 'test'
101 rpc_method = 'test'
102 read_patcher = mock.patch.object(
102 read_patcher = mock.patch.object(
103 hooks_daemon.HooksHttpHandler, '_read_request',
103 hooks_daemon.HooksHttpHandler, '_read_request',
104 return_value=(rpc_method, {}))
104 return_value=(rpc_method, {}))
105 hooks_patcher = mock.patch.object(
105 hooks_patcher = mock.patch.object(
106 hooks_daemon.Hooks, rpc_method, create=True,
106 hooks_daemon.Hooks, rpc_method, create=True,
107 side_effect=Exception('Test exception'))
107 side_effect=Exception('Test exception'))
108
108
109 with read_patcher, hooks_patcher:
109 with read_patcher, hooks_patcher:
110 server = MockServer(hooks_daemon.HooksHttpHandler, request)
110 server = MockServer(hooks_daemon.HooksHttpHandler, request)
111
111
112 org_exc = json.loads(server.request.output_stream.buflist[-1])
112 org_exc = json.loads(server.request.output_stream.buflist[-1])
113 expected_result = {
113 expected_result = {
114 'exception': 'Exception',
114 'exception': 'Exception',
115 'exception_traceback': org_exc['exception_traceback'],
115 'exception_traceback': org_exc['exception_traceback'],
116 'exception_args': ['Test exception']
116 'exception_args': ['Test exception']
117 }
117 }
118 assert org_exc == expected_result
118 assert org_exc == expected_result
119
119
120 def test_log_message_writes_to_debug_log(self, caplog):
120 def test_log_message_writes_to_debug_log(self, caplog):
121 ip_port = ('0.0.0.0', 8888)
121 ip_port = ('0.0.0.0', 8888)
122 handler = hooks_daemon.HooksHttpHandler(
122 handler = hooks_daemon.HooksHttpHandler(
123 MockRequest('POST /'), ip_port, mock.Mock())
123 MockRequest('POST /'), ip_port, mock.Mock())
124 fake_date = '1/Nov/2015 00:00:00'
124 fake_date = '1/Nov/2015 00:00:00'
125 date_patcher = mock.patch.object(
125 date_patcher = mock.patch.object(
126 handler, 'log_date_time_string', return_value=fake_date)
126 handler, 'log_date_time_string', return_value=fake_date)
127 with date_patcher, caplog.at_level(logging.DEBUG):
127 with date_patcher, caplog.at_level(logging.DEBUG):
128 handler.log_message('Some message %d, %s', 123, 'string')
128 handler.log_message('Some message %d, %s', 123, 'string')
129
129
130 expected_message = '{} - - [{}] Some message 123, string'.format(
130 expected_message = '{} - - [{}] Some message 123, string'.format(
131 ip_port[0], fake_date)
131 ip_port[0], fake_date)
132 assert_message_in_log(
132 assert_message_in_log(
133 caplog.records, expected_message,
133 caplog.records, expected_message,
134 levelno=logging.DEBUG, module='hooks_daemon')
134 levelno=logging.DEBUG, module='hooks_daemon')
135
135
136 def _generate_post_request(self, data):
136 def _generate_post_request(self, data):
137 payload = json.dumps(data)
137 payload = json.dumps(data)
138 return 'POST / HTTP/1.0\nContent-Length: {}\n\n{}'.format(
138 return 'POST / HTTP/1.0\nContent-Length: {}\n\n{}'.format(
139 len(payload), payload)
139 len(payload), payload)
140
140
141
141
142 class ThreadedHookCallbackDaemon(object):
142 class ThreadedHookCallbackDaemon(object):
143 def test_constructor_calls_prepare(self):
143 def test_constructor_calls_prepare(self):
144 prepare_daemon_patcher = mock.patch.object(
144 prepare_daemon_patcher = mock.patch.object(
145 hooks_daemon.ThreadedHookCallbackDaemon, '_prepare')
145 hooks_daemon.ThreadedHookCallbackDaemon, '_prepare')
146 with prepare_daemon_patcher as prepare_daemon_mock:
146 with prepare_daemon_patcher as prepare_daemon_mock:
147 hooks_daemon.ThreadedHookCallbackDaemon()
147 hooks_daemon.ThreadedHookCallbackDaemon()
148 prepare_daemon_mock.assert_called_once_with()
148 prepare_daemon_mock.assert_called_once_with()
149
149
150 def test_run_is_called_on_context_start(self):
150 def test_run_is_called_on_context_start(self):
151 patchers = mock.patch.multiple(
151 patchers = mock.patch.multiple(
152 hooks_daemon.ThreadedHookCallbackDaemon,
152 hooks_daemon.ThreadedHookCallbackDaemon,
153 _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT)
153 _run=mock.DEFAULT, _prepare=mock.DEFAULT, __exit__=mock.DEFAULT)
154
154
155 with patchers as mocks:
155 with patchers as mocks:
156 daemon = hooks_daemon.ThreadedHookCallbackDaemon()
156 daemon = hooks_daemon.ThreadedHookCallbackDaemon()
157 with daemon as daemon_context:
157 with daemon as daemon_context:
158 pass
158 pass
159 mocks['_run'].assert_called_once_with()
159 mocks['_run'].assert_called_once_with()
160 assert daemon_context == daemon
160 assert daemon_context == daemon
161
161
162 def test_stop_is_called_on_context_exit(self):
162 def test_stop_is_called_on_context_exit(self):
163 patchers = mock.patch.multiple(
163 patchers = mock.patch.multiple(
164 hooks_daemon.ThreadedHookCallbackDaemon,
164 hooks_daemon.ThreadedHookCallbackDaemon,
165 _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT)
165 _run=mock.DEFAULT, _prepare=mock.DEFAULT, _stop=mock.DEFAULT)
166
166
167 with patchers as mocks:
167 with patchers as mocks:
168 daemon = hooks_daemon.ThreadedHookCallbackDaemon()
168 daemon = hooks_daemon.ThreadedHookCallbackDaemon()
169 with daemon as daemon_context:
169 with daemon as daemon_context:
170 assert mocks['_stop'].call_count == 0
170 assert mocks['_stop'].call_count == 0
171
171
172 mocks['_stop'].assert_called_once_with()
172 mocks['_stop'].assert_called_once_with()
173 assert daemon_context == daemon
173 assert daemon_context == daemon
174
174
175
175
176 class TestHttpHooksCallbackDaemon(object):
176 class TestHttpHooksCallbackDaemon(object):
177 def test_prepare_inits_daemon_variable(self, tcp_server, caplog):
177 def test_prepare_inits_daemon_variable(self, tcp_server, caplog):
178 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
178 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
179 daemon = hooks_daemon.HttpHooksCallbackDaemon()
179 daemon = hooks_daemon.HttpHooksCallbackDaemon()
180 assert daemon._daemon == tcp_server
180 assert daemon._daemon == tcp_server
181
181
182 assert_message_in_log(
182 assert_message_in_log(
183 caplog.records,
183 caplog.records,
184 'Preparing callback daemon and registering hook object',
184 'Preparing HTTP callback daemon and registering hook object',
185 levelno=logging.DEBUG, module='hooks_daemon')
185 levelno=logging.DEBUG, module='hooks_daemon')
186
186
187 def test_prepare_inits_hooks_uri_and_logs_it(
187 def test_prepare_inits_hooks_uri_and_logs_it(
188 self, tcp_server, caplog):
188 self, tcp_server, caplog):
189 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
189 with self._tcp_patcher(tcp_server), caplog.at_level(logging.DEBUG):
190 daemon = hooks_daemon.HttpHooksCallbackDaemon()
190 daemon = hooks_daemon.HttpHooksCallbackDaemon()
191
191
192 _, port = tcp_server.server_address
192 _, port = tcp_server.server_address
193 expected_uri = '{}:{}'.format(daemon.IP_ADDRESS, port)
193 expected_uri = '{}:{}'.format(daemon.IP_ADDRESS, port)
194 assert daemon.hooks_uri == expected_uri
194 assert daemon.hooks_uri == expected_uri
195
195
196 assert_message_in_log(
196 assert_message_in_log(
197 caplog.records, 'Hooks uri is: {}'.format(expected_uri),
197 caplog.records, 'Hooks uri is: {}'.format(expected_uri),
198 levelno=logging.DEBUG, module='hooks_daemon')
198 levelno=logging.DEBUG, module='hooks_daemon')
199
199
200 def test_run_creates_a_thread(self, tcp_server):
200 def test_run_creates_a_thread(self, tcp_server):
201 thread = mock.Mock()
201 thread = mock.Mock()
202
202
203 with self._tcp_patcher(tcp_server):
203 with self._tcp_patcher(tcp_server):
204 daemon = hooks_daemon.HttpHooksCallbackDaemon()
204 daemon = hooks_daemon.HttpHooksCallbackDaemon()
205
205
206 with self._thread_patcher(thread) as thread_mock:
206 with self._thread_patcher(thread) as thread_mock:
207 daemon._run()
207 daemon._run()
208
208
209 thread_mock.assert_called_once_with(
209 thread_mock.assert_called_once_with(
210 target=tcp_server.serve_forever,
210 target=tcp_server.serve_forever,
211 kwargs={'poll_interval': daemon.POLL_INTERVAL})
211 kwargs={'poll_interval': daemon.POLL_INTERVAL})
212 assert thread.daemon is True
212 assert thread.daemon is True
213 thread.start.assert_called_once_with()
213 thread.start.assert_called_once_with()
214
214
215 def test_run_logs(self, tcp_server, caplog):
215 def test_run_logs(self, tcp_server, caplog):
216
216
217 with self._tcp_patcher(tcp_server):
217 with self._tcp_patcher(tcp_server):
218 daemon = hooks_daemon.HttpHooksCallbackDaemon()
218 daemon = hooks_daemon.HttpHooksCallbackDaemon()
219
219
220 with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG):
220 with self._thread_patcher(mock.Mock()), caplog.at_level(logging.DEBUG):
221 daemon._run()
221 daemon._run()
222
222
223 assert_message_in_log(
223 assert_message_in_log(
224 caplog.records,
224 caplog.records,
225 'Running event loop of callback daemon in background thread',
225 'Running event loop of callback daemon in background thread',
226 levelno=logging.DEBUG, module='hooks_daemon')
226 levelno=logging.DEBUG, module='hooks_daemon')
227
227
228 def test_stop_cleans_up_the_connection(self, tcp_server, caplog):
228 def test_stop_cleans_up_the_connection(self, tcp_server, caplog):
229 thread = mock.Mock()
229 thread = mock.Mock()
230
230
231 with self._tcp_patcher(tcp_server):
231 with self._tcp_patcher(tcp_server):
232 daemon = hooks_daemon.HttpHooksCallbackDaemon()
232 daemon = hooks_daemon.HttpHooksCallbackDaemon()
233
233
234 with self._thread_patcher(thread), caplog.at_level(logging.DEBUG):
234 with self._thread_patcher(thread), caplog.at_level(logging.DEBUG):
235 with daemon:
235 with daemon:
236 assert daemon._daemon == tcp_server
236 assert daemon._daemon == tcp_server
237 assert daemon._callback_thread == thread
237 assert daemon._callback_thread == thread
238
238
239 assert daemon._daemon is None
239 assert daemon._daemon is None
240 assert daemon._callback_thread is None
240 assert daemon._callback_thread is None
241 tcp_server.shutdown.assert_called_with()
241 tcp_server.shutdown.assert_called_with()
242 thread.join.assert_called_once_with()
242 thread.join.assert_called_once_with()
243
243
244 assert_message_in_log(
244 assert_message_in_log(
245 caplog.records, 'Waiting for background thread to finish.',
245 caplog.records, 'Waiting for background thread to finish.',
246 levelno=logging.DEBUG, module='hooks_daemon')
246 levelno=logging.DEBUG, module='hooks_daemon')
247
247
248 def _tcp_patcher(self, tcp_server):
248 def _tcp_patcher(self, tcp_server):
249 return mock.patch.object(
249 return mock.patch.object(
250 hooks_daemon, 'TCPServer', return_value=tcp_server)
250 hooks_daemon, 'TCPServer', return_value=tcp_server)
251
251
252 def _thread_patcher(self, thread):
252 def _thread_patcher(self, thread):
253 return mock.patch.object(
253 return mock.patch.object(
254 hooks_daemon.threading, 'Thread', return_value=thread)
254 hooks_daemon.threading, 'Thread', return_value=thread)
255
255
256
256
257 class TestPrepareHooksDaemon(object):
257 class TestPrepareHooksDaemon(object):
258 @pytest.mark.parametrize('protocol', ('http',))
258 @pytest.mark.parametrize('protocol', ('http',))
259 def test_returns_dummy_hooks_callback_daemon_when_using_direct_calls(
259 def test_returns_dummy_hooks_callback_daemon_when_using_direct_calls(
260 self, protocol):
260 self, protocol):
261 expected_extras = {'extra1': 'value1'}
261 expected_extras = {'extra1': 'value1'}
262 callback, extras = hooks_daemon.prepare_callback_daemon(
262 callback, extras = hooks_daemon.prepare_callback_daemon(
263 expected_extras.copy(), protocol=protocol, use_direct_calls=True)
263 expected_extras.copy(), protocol=protocol, use_direct_calls=True)
264 assert isinstance(callback, hooks_daemon.DummyHooksCallbackDaemon)
264 assert isinstance(callback, hooks_daemon.DummyHooksCallbackDaemon)
265 expected_extras['hooks_module'] = 'rhodecode.lib.hooks_daemon'
265 expected_extras['hooks_module'] = 'rhodecode.lib.hooks_daemon'
266 assert extras == expected_extras
266 assert extras == expected_extras
267
267
268 @pytest.mark.parametrize('protocol, expected_class', (
268 @pytest.mark.parametrize('protocol, expected_class', (
269 ('http', hooks_daemon.HttpHooksCallbackDaemon),
269 ('http', hooks_daemon.HttpHooksCallbackDaemon),
270 ))
270 ))
271 def test_returns_real_hooks_callback_daemon_when_protocol_is_specified(
271 def test_returns_real_hooks_callback_daemon_when_protocol_is_specified(
272 self, protocol, expected_class):
272 self, protocol, expected_class):
273 expected_extras = {
273 expected_extras = {
274 'extra1': 'value1',
274 'extra1': 'value1',
275 'hooks_protocol': protocol.lower()
275 'hooks_protocol': protocol.lower()
276 }
276 }
277 callback, extras = hooks_daemon.prepare_callback_daemon(
277 callback, extras = hooks_daemon.prepare_callback_daemon(
278 expected_extras.copy(), protocol=protocol, use_direct_calls=False)
278 expected_extras.copy(), protocol=protocol, use_direct_calls=False)
279 assert isinstance(callback, expected_class)
279 assert isinstance(callback, expected_class)
280 hooks_uri = extras.pop('hooks_uri')
280 hooks_uri = extras.pop('hooks_uri')
281 assert extras == expected_extras
281 assert extras == expected_extras
282
282
283 @pytest.mark.parametrize('protocol', (
283 @pytest.mark.parametrize('protocol', (
284 'invalid',
284 'invalid',
285 'Http',
285 'Http',
286 'HTTP',
286 'HTTP',
287 ))
287 ))
288 def test_raises_on_invalid_protocol(self, protocol):
288 def test_raises_on_invalid_protocol(self, protocol):
289 expected_extras = {
289 expected_extras = {
290 'extra1': 'value1',
290 'extra1': 'value1',
291 'hooks_protocol': protocol.lower()
291 'hooks_protocol': protocol.lower()
292 }
292 }
293 with pytest.raises(Exception):
293 with pytest.raises(Exception):
294 callback, extras = hooks_daemon.prepare_callback_daemon(
294 callback, extras = hooks_daemon.prepare_callback_daemon(
295 expected_extras.copy(),
295 expected_extras.copy(),
296 protocol=protocol,
296 protocol=protocol,
297 use_direct_calls=False)
297 use_direct_calls=False)
298
298
299
299
300 class MockRequest(object):
300 class MockRequest(object):
301 def __init__(self, request):
301 def __init__(self, request):
302 self.request = request
302 self.request = request
303 self.input_stream = StringIO(b'{}'.format(self.request))
303 self.input_stream = StringIO(b'{}'.format(self.request))
304 self.output_stream = StringIO()
304 self.output_stream = StringIO()
305
305
306 def makefile(self, mode, *args, **kwargs):
306 def makefile(self, mode, *args, **kwargs):
307 return self.output_stream if mode == 'wb' else self.input_stream
307 return self.output_stream if mode == 'wb' else self.input_stream
308
308
309
309
310 class MockServer(object):
310 class MockServer(object):
311 def __init__(self, Handler, request):
311 def __init__(self, Handler, request):
312 ip_port = ('0.0.0.0', 8888)
312 ip_port = ('0.0.0.0', 8888)
313 self.request = MockRequest(request)
313 self.request = MockRequest(request)
314 self.handler = Handler(self.request, ip_port, self)
314 self.handler = Handler(self.request, ip_port, self)
315
315
316
316
317 @pytest.fixture
317 @pytest.fixture
318 def tcp_server():
318 def tcp_server():
319 server = mock.Mock()
319 server = mock.Mock()
320 server.server_address = ('127.0.0.1', 8881)
320 server.server_address = ('127.0.0.1', 8881)
321 return server
321 return server
General Comments 0
You need to be logged in to leave comments. Login now