Coverage for tests/tests_main.py: 100%
837 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-11 15:46 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-11 15:46 -0700
1'''
2Farmbot class unit tests.
3'''
5import json
6import unittest
7from unittest.mock import Mock, patch, call
8import requests
10from farmbot_sidecar_starter_pack import Farmbot
12MOCK_TOKEN = {
13 'token': {
14 'unencoded': {
15 'iss': '//my.farm.bot',
16 'mqtt': 'mqtt_url',
17 'bot': 'device_0',
18 },
19 'encoded': 'encoded_token_value'
20 }
21}
23TOKEN_REQUEST_KWARGS = {
24 'headers': {'content-type': 'application/json'},
25 'timeout': 0,
26}
28REQUEST_KWARGS_WITH_PAYLOAD = {
29 'headers': {
30 'authorization': 'encoded_token_value',
31 'content-type': 'application/json'
32 },
33 'timeout': 0,
34}
36REQUEST_KWARGS = {
37 **REQUEST_KWARGS_WITH_PAYLOAD,
38 'json': None,
39}
42class TestFarmbot(unittest.TestCase):
43 '''Farmbot tests'''
45 def setUp(self):
46 '''Set up method called before each test case'''
47 self.fb = Farmbot()
48 self.fb.set_token(MOCK_TOKEN)
49 self.fb.set_verbosity(0)
50 self.fb.state.test_env = True
51 self.fb.set_timeout(0, 'all')
52 self.fb.clear_cache()
54 @patch('requests.post')
55 def test_get_token_default_server(self, mock_post):
56 '''POSITIVE TEST: function called with email, password, and default server'''
57 mock_response = Mock()
58 expected_token = {'token': 'abc123'}
59 mock_response.json.return_value = expected_token
60 mock_response.status_code = 200
61 mock_response.text = 'text'
62 mock_post.return_value = mock_response
63 self.fb.set_token(None)
64 # Call with default server
65 self.fb.get_token('test_email@gmail.com', 'test_pass_123')
66 mock_post.assert_called_once_with(
67 url='https://my.farm.bot/api/tokens',
68 **TOKEN_REQUEST_KWARGS,
69 json={'user': {'email': 'test_email@gmail.com',
70 'password': 'test_pass_123'}},
71 )
72 self.assertEqual(self.fb.state.token, expected_token)
74 @patch('requests.post')
75 def test_get_token_custom_server(self, mock_post):
76 '''POSITIVE TEST: function called with email, password, and custom server'''
77 mock_response = Mock()
78 expected_token = {'token': 'abc123'}
79 mock_response.json.return_value = expected_token
80 mock_response.status_code = 200
81 mock_response.text = 'text'
82 mock_post.return_value = mock_response
83 self.fb.set_token(None)
84 # Call with custom server
85 self.fb.get_token('test_email@gmail.com', 'test_pass_123',
86 'https://staging.farm.bot')
87 mock_post.assert_called_once_with(
88 url='https://staging.farm.bot/api/tokens',
89 **TOKEN_REQUEST_KWARGS,
90 json={'user': {'email': 'test_email@gmail.com',
91 'password': 'test_pass_123'}},
92 )
93 self.assertEqual(self.fb.state.token, expected_token)
95 @patch('requests.post')
96 def helper_get_token_errors(self, *args, **kwargs):
97 '''Test helper for get_token errors'''
98 mock_post = args[0]
99 status_code = kwargs['status_code']
100 error_msg = kwargs['error_msg']
101 mock_response = Mock()
102 mock_response.status_code = status_code
103 mock_post.return_value = mock_response
104 self.fb.set_token(None)
105 self.fb.get_token('email@gmail.com', 'test_pass_123')
106 mock_post.assert_called_once_with(
107 url='https://my.farm.bot/api/tokens',
108 **TOKEN_REQUEST_KWARGS,
109 json={'user': {'email': 'email@gmail.com',
110 'password': 'test_pass_123'}},
111 )
112 self.assertEqual(self.fb.state.error, error_msg)
113 self.assertIsNone(self.fb.state.token)
115 def test_get_token_bad_email(self):
116 '''NEGATIVE TEST: function called with incorrect email'''
117 self.helper_get_token_errors(
118 status_code=422,
119 error_msg='HTTP ERROR: Incorrect email address or password.',
120 )
122 def test_get_token_bad_server(self):
123 '''NEGATIVE TEST: function called with incorrect server'''
124 self.helper_get_token_errors(
125 status_code=404,
126 error_msg='HTTP ERROR: The server address does not exist.',
127 )
129 def test_get_token_other_error(self):
130 '''get_token: other error'''
131 self.helper_get_token_errors(
132 status_code=500,
133 error_msg='HTTP ERROR: Unexpected status code 500',
134 )
136 @patch('requests.post')
137 def helper_get_token_exceptions(self, *args, **kwargs):
138 '''Test helper for get_token exceptions'''
139 mock_post = args[0]
140 exception = kwargs['exception']
141 error_msg = kwargs['error_msg']
142 mock_post.side_effect = exception
143 self.fb.set_token(None)
144 self.fb.get_token('email@gmail.com', 'test_pass_123')
145 mock_post.assert_called_once_with(
146 url='https://my.farm.bot/api/tokens',
147 **TOKEN_REQUEST_KWARGS,
148 json={'user': {'email': 'email@gmail.com',
149 'password': 'test_pass_123'}},
150 )
151 self.assertEqual(self.fb.state.error, error_msg)
152 self.assertIsNone(self.fb.state.token)
154 def test_get_token_server_not_found(self):
155 '''get_token: server not found'''
156 self.helper_get_token_exceptions(
157 exception=requests.exceptions.ConnectionError,
158 error_msg='DNS ERROR: The server address does not exist.',
159 )
161 def test_get_token_timeout(self):
162 '''get_token: timeout'''
163 self.helper_get_token_exceptions(
164 exception=requests.exceptions.Timeout,
165 error_msg='DNS ERROR: The request timed out.',
166 )
168 def test_get_token_problem(self):
169 '''get_token: problem'''
170 self.helper_get_token_exceptions(
171 exception=requests.exceptions.RequestException,
172 error_msg='DNS ERROR: There was a problem with the request.',
173 )
175 def test_get_token_other_exception(self):
176 '''get_token: other exception'''
177 self.helper_get_token_exceptions(
178 exception=Exception('other'),
179 error_msg='DNS ERROR: An unexpected error occurred: other',
180 )
182 @patch('requests.request')
183 def helper_api_get_error(self, *args, **kwargs):
184 '''Test helper for api_get errors'''
185 mock_request = args[0]
186 status_code = kwargs['status_code']
187 error_msg = kwargs['error_msg']
188 mock_response = Mock()
189 mock_response.status_code = status_code
190 mock_response.reason = 'reason'
191 mock_response.text = 'text'
192 mock_response.json.return_value = {'error': 'error'}
193 mock_request.return_value = mock_response
194 response = self.fb.api_get('device')
195 mock_request.assert_called_once_with(
196 method='GET',
197 url='https://my.farm.bot/api/device',
198 **REQUEST_KWARGS,
199 )
200 self.assertEqual(response, error_msg)
202 def test_api_get_errors(self):
203 '''Test api_get errors'''
204 msg_404 = 'CLIENT ERROR 404: The specified endpoint does not exist.'
205 msg_404 += ' ({\n "error": "error"\n})'
206 self.helper_api_get_error(
207 status_code=404,
208 error_msg=msg_404
209 )
210 self.helper_api_get_error(
211 status_code=500,
212 error_msg='SERVER ERROR 500: text ({\n "error": "error"\n})',
213 )
214 self.helper_api_get_error(
215 status_code=600,
216 error_msg='UNEXPECTED ERROR 600: text ({\n "error": "error"\n})',
217 )
219 @patch('requests.request')
220 def test_api_string_error_response_handling(self, mock_request):
221 '''Test API string response errors'''
222 mock_response = Mock()
223 mock_response.status_code = 404
224 mock_response.reason = 'reason'
225 mock_response.text = 'error string'
226 mock_response.json.side_effect = requests.exceptions.JSONDecodeError(
227 '', '', 0)
228 mock_request.return_value = mock_response
229 response = self.fb.api_get('device')
230 mock_request.assert_called_once_with(
231 method='GET',
232 url='https://my.farm.bot/api/device',
233 **REQUEST_KWARGS,
234 )
235 self.assertEqual(
236 response,
237 'CLIENT ERROR 404: The specified endpoint does not exist. (error string)')
239 @patch('requests.request')
240 def test_api_string_error_response_handling_html(self, mock_request):
241 '''Test API html string response errors'''
242 mock_response = Mock()
243 mock_response.status_code = 404
244 mock_response.reason = 'reason'
245 mock_response.text = '<html><h1>error0</h1><h2>error1</h2></html>'
246 mock_response.json.side_effect = requests.exceptions.JSONDecodeError(
247 '', '', 0)
248 mock_request.return_value = mock_response
249 response = self.fb.api_get('device')
250 mock_request.assert_called_once_with(
251 method='GET',
252 url='https://my.farm.bot/api/device',
253 **REQUEST_KWARGS,
254 )
255 self.assertEqual(
256 response,
257 'CLIENT ERROR 404: The specified endpoint does not exist. (error0 error1)')
259 @patch('requests.request')
260 def test_api_get_endpoint_only(self, mock_request):
261 '''POSITIVE TEST: function called with endpoint only'''
262 mock_response = Mock()
263 expected_response = {'device': 'info'}
264 mock_response.json.return_value = expected_response
265 mock_response.status_code = 200
266 mock_response.text = 'text'
267 mock_request.return_value = mock_response
268 # Call with endpoint only
269 response = self.fb.api_get('device')
270 mock_request.assert_called_once_with(
271 method='GET',
272 url='https://my.farm.bot/api/device',
273 **REQUEST_KWARGS,
274 )
275 self.assertEqual(response, expected_response)
277 @patch('requests.request')
278 def test_api_get_with_id(self, mock_request):
279 '''POSITIVE TEST: function called with valid ID'''
280 mock_response = Mock()
281 expected_response = {'peripheral': 'info'}
282 mock_response.json.return_value = expected_response
283 mock_response.status_code = 200
284 mock_response.text = 'text'
285 mock_request.return_value = mock_response
286 # Call with specific ID
287 response = self.fb.api_get('peripherals', '12345')
288 mock_request.assert_called_once_with(
289 method='GET',
290 url='https://my.farm.bot/api/peripherals/12345',
291 **REQUEST_KWARGS,
292 )
293 self.assertEqual(response, expected_response)
295 @patch('requests.request')
296 def test_check_token_api_request(self, mock_request):
297 '''Test check_token: API request'''
298 self.fb.set_token(None)
299 with self.assertRaises(ValueError) as cm:
300 self.fb.api_get('points')
301 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
302 mock_request.assert_not_called()
303 self.assertEqual(self.fb.state.error, self.fb.state.NO_TOKEN_ERROR)
305 @patch('paho.mqtt.client.Client')
306 @patch('requests.request')
307 def test_check_token_broker(self, mock_request, mock_mqtt):
308 '''Test check_token: broker'''
309 mock_client = Mock()
310 mock_mqtt.return_value = mock_client
311 self.fb.set_token(None)
312 with self.assertRaises(ValueError) as cm:
313 self.fb.on(123)
314 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
315 with self.assertRaises(ValueError) as cm:
316 self.fb.read_sensor(123)
317 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
318 with self.assertRaises(ValueError) as cm:
319 self.fb.get_xyz()
320 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
321 with self.assertRaises(ValueError) as cm:
322 self.fb.read_status()
323 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
324 mock_request.assert_not_called()
325 mock_client.publish.assert_not_called()
326 self.assertEqual(self.fb.state.error, self.fb.state.NO_TOKEN_ERROR)
328 @patch('paho.mqtt.client.Client')
329 def test_publish_disabled(self, mock_mqtt):
330 '''Test publish disabled'''
331 mock_client = Mock()
332 mock_mqtt.return_value = mock_client
333 self.fb.state.dry_run = True
334 self.fb.on(123)
335 mock_client.publish.assert_not_called()
337 @patch('requests.request')
338 def test_api_patch(self, mock_request):
339 '''test api_patch function'''
340 mock_response = Mock()
341 mock_response.status_code = 200
342 mock_response.text = 'text'
343 mock_response.json.return_value = {'name': 'new name'}
344 mock_request.return_value = mock_response
345 device_info = self.fb.api_patch('device', {'name': 'new name'})
346 mock_request.assert_has_calls([call(
347 method='PATCH',
348 url='https://my.farm.bot/api/device',
349 **REQUEST_KWARGS_WITH_PAYLOAD,
350 json={'name': 'new name'},
351 ),
352 call().json(),
353 ])
354 self.assertEqual(device_info, {'name': 'new name'})
356 @patch('requests.request')
357 def test_api_post(self, mock_request):
358 '''test api_post function'''
359 mock_response = Mock()
360 mock_response.status_code = 200
361 mock_response.text = 'text'
362 mock_response.json.return_value = {'name': 'new name'}
363 mock_request.return_value = mock_response
364 point = self.fb.api_post('points', {'name': 'new name'})
365 mock_request.assert_has_calls([call(
366 method='POST',
367 url='https://my.farm.bot/api/points',
368 **REQUEST_KWARGS_WITH_PAYLOAD,
369 json={'name': 'new name'},
370 ),
371 call().json(),
372 ])
373 self.assertEqual(point, {'name': 'new name'})
375 @patch('requests.request')
376 def test_api_delete(self, mock_request):
377 '''test api_delete function'''
378 mock_response = Mock()
379 mock_response.status_code = 200
380 mock_response.text = 'text'
381 mock_response.json.return_value = {'name': 'deleted'}
382 mock_request.return_value = mock_response
383 result = self.fb.api_delete('points', 12345)
384 mock_request.assert_called_once_with(
385 method='DELETE',
386 url='https://my.farm.bot/api/points/12345',
387 **REQUEST_KWARGS,
388 )
389 self.assertEqual(result, {'name': 'deleted'})
391 @patch('requests.request')
392 def test_api_delete_requests_disabled(self, mock_request):
393 '''test api_delete function: requests disabled'''
394 self.fb.state.dry_run = True
395 result = self.fb.api_delete('points', 12345)
396 mock_request.assert_not_called()
397 self.assertEqual(result, {"edit_requests_disabled": True})
399 @patch('requests.request')
400 def test_group_one(self, mock_request):
401 '''test group function: get one group'''
402 mock_response = Mock()
403 mock_response.json.return_value = {'name': 'Group 0'}
404 mock_response.status_code = 200
405 mock_response.text = 'text'
406 mock_request.return_value = mock_response
407 group_info = self.fb.group(12345)
408 mock_request.assert_called_once_with(
409 method='GET',
410 url='https://my.farm.bot/api/point_groups/12345',
411 **REQUEST_KWARGS,
412 )
413 self.assertEqual(group_info, {'name': 'Group 0'})
415 @patch('requests.request')
416 def test_group_all(self, mock_request):
417 '''test group function: get all groups'''
418 mock_response = Mock()
419 mock_response.json.return_value = [{'name': 'Group 0'}]
420 mock_response.status_code = 200
421 mock_response.text = 'text'
422 mock_request.return_value = mock_response
423 group_info = self.fb.group()
424 mock_request.assert_called_once_with(
425 method='GET',
426 url='https://my.farm.bot/api/point_groups',
427 **REQUEST_KWARGS,
428 )
429 self.assertEqual(group_info, [{'name': 'Group 0'}])
431 @patch('requests.request')
432 def test_curve_one(self, mock_request):
433 '''test curve function: get one curve'''
434 mock_response = Mock()
435 mock_response.json.return_value = {'name': 'Curve 0'}
436 mock_response.status_code = 200
437 mock_response.text = 'text'
438 mock_request.return_value = mock_response
439 curve_info = self.fb.curve(12345)
440 mock_request.assert_called_once_with(
441 method='GET',
442 url='https://my.farm.bot/api/curves/12345',
443 **REQUEST_KWARGS,
444 )
445 self.assertEqual(curve_info, {'name': 'Curve 0'})
447 @patch('requests.request')
448 def test_curve_all(self, mock_request):
449 '''test curve function: get all curves'''
450 mock_response = Mock()
451 mock_response.json.return_value = [{'name': 'Curve 0'}]
452 mock_response.status_code = 200
453 mock_response.text = 'text'
454 mock_request.return_value = mock_response
455 curve_info = self.fb.curve()
456 mock_request.assert_called_once_with(
457 method='GET',
458 url='https://my.farm.bot/api/curves',
459 **REQUEST_KWARGS,
460 )
461 self.assertEqual(curve_info, [{'name': 'Curve 0'}])
463 @patch('requests.request')
464 def test_safe_z(self, mock_request):
465 '''test safe_z function'''
466 mock_response = Mock()
467 mock_response.json.return_value = {'safe_height': 100}
468 mock_response.status_code = 200
469 mock_response.text = 'text'
470 mock_request.return_value = mock_response
471 safe_height = self.fb.safe_z()
472 mock_request.assert_called_once_with(
473 method='GET',
474 url='https://my.farm.bot/api/fbos_config',
475 **REQUEST_KWARGS,
476 )
477 self.assertEqual(safe_height, 100)
479 @patch('requests.request')
480 def test_garden_size(self, mock_request):
481 '''test garden_size function'''
482 mock_response = Mock()
483 mock_response.json.return_value = {
484 'movement_axis_nr_steps_x': 1000,
485 'movement_axis_nr_steps_y': 2000,
486 'movement_axis_nr_steps_z': 40000,
487 'movement_step_per_mm_x': 5,
488 'movement_step_per_mm_y': 5,
489 'movement_step_per_mm_z': 25,
490 }
491 mock_response.status_code = 200
492 mock_response.text = 'text'
493 mock_request.return_value = mock_response
494 garden_size = self.fb.garden_size()
495 mock_request.assert_called_once_with(
496 method='GET',
497 url='https://my.farm.bot/api/firmware_config',
498 **REQUEST_KWARGS,
499 )
500 self.assertEqual(garden_size, {'x': 200, 'y': 400, 'z': 1600})
502 @patch('requests.request')
503 def test_log(self, mock_request):
504 '''test log function'''
505 mock_response = Mock()
506 mock_response.status_code = 200
507 mock_response.text = 'text'
508 mock_response.json.return_value = {'message': 'test message'}
509 mock_request.return_value = mock_response
510 self.fb.log('test message', 'info', 'toast')
511 mock_request.assert_called_once_with(
512 method='POST',
513 url='https://my.farm.bot/api/logs',
514 **REQUEST_KWARGS_WITH_PAYLOAD,
515 json={
516 'message': 'test message',
517 'type': 'info',
518 'channels': ['toast'],
519 },
520 )
522 @patch('paho.mqtt.client.Client')
523 def test_connect_broker(self, mock_mqtt):
524 '''Test test_connect_broker command'''
525 mock_client = Mock()
526 mock_mqtt.return_value = mock_client
527 self.fb.connect_broker()
528 mock_client.username_pw_set.assert_called_once_with(
529 username='device_0',
530 password='encoded_token_value')
531 mock_client.connect.assert_called_once_with(
532 'mqtt_url',
533 port=1883,
534 keepalive=60)
535 mock_client.loop_start.assert_called()
537 def test_disconnect_broker(self):
538 '''Test disconnect_broker command'''
539 mock_client = Mock()
540 self.fb.broker.client = mock_client
541 self.fb.disconnect_broker()
542 mock_client.loop_stop.assert_called_once()
543 mock_client.disconnect.assert_called_once()
545 @patch('paho.mqtt.client.Client')
546 def test_listen(self, mock_mqtt):
547 '''Test listen command'''
548 mock_client = Mock()
549 mock_mqtt.return_value = mock_client
550 self.fb.listen()
552 class MockMessage:
553 '''Mock message class'''
554 topic = 'topic'
555 payload = '{"message": "test message"}'
556 mock_client.on_message('', '', MockMessage())
557 mock_client.username_pw_set.assert_called_once_with(
558 username='device_0',
559 password='encoded_token_value')
560 mock_client.connect.assert_called_once_with(
561 'mqtt_url',
562 port=1883,
563 keepalive=60)
564 mock_client.subscribe.assert_called_once_with('bot/device_0/#')
565 mock_client.loop_start.assert_called()
566 mock_client.loop_stop.assert_called()
568 @patch('math.inf', 0.1)
569 @patch('paho.mqtt.client.Client')
570 def test_listen_for_status_changes(self, mock_mqtt):
571 '''Test listen_for_status_changes command'''
572 self.maxDiff = None
573 i = 0
575 mock_client = Mock()
576 mock_mqtt.return_value = mock_client
578 class MockMessage:
579 '''Mock message class'''
581 def __init__(self):
582 self.topic = '/status'
583 payload = {
584 'location_data': {
585 'position': {
586 'x': i,
587 'y': i + 10,
588 'z': 100,
589 }}}
590 if i == 2:
591 payload['location_data']['position']['extra'] = {'idx': 2}
592 if i == 3:
593 payload['location_data']['position']['extra'] = {'idx': 3}
594 self.payload = json.dumps(payload)
596 def patched_sleep(_seconds):
597 '''Patched sleep function'''
598 nonlocal i
599 mock_message = MockMessage()
600 mock_client.on_message('', '', mock_message)
601 i += 1
603 with patch('time.sleep', new=patched_sleep):
604 self.fb.listen_for_status_changes(
605 stop_count=5,
606 info_path='location_data.position')
608 self.assertEqual(self.fb.state.last_messages['status'], [
609 {'location_data': {'position': {'x': 0, 'y': 10, 'z': 100}}},
610 {'location_data': {'position': {'x': 1, 'y': 11, 'z': 100}}},
611 {'location_data': {'position': {
612 'extra': {'idx': 2}, 'x': 2, 'y': 12, 'z': 100}}},
613 {'location_data': {'position': {
614 'extra': {'idx': 3}, 'x': 3, 'y': 13, 'z': 100}}},
615 {'location_data': {'position': {'x': 4, 'y': 14, 'z': 100}}}
616 ])
617 self.assertEqual(self.fb.state.last_messages['status_diffs'], [
618 {'x': 0, 'y': 10, 'z': 100},
619 {'x': 1, 'y': 11},
620 {'extra': {'idx': 2}, 'x': 2, 'y': 12},
621 {'extra': {'idx': 3}, 'x': 3, 'y': 13},
622 {'x': 4, 'y': 14},
623 ])
624 self.assertEqual(self.fb.state.last_messages['status_excerpt'], [
625 {'x': 0, 'y': 10, 'z': 100},
626 {'x': 1, 'y': 11, 'z': 100},
627 {'extra': {'idx': 2}, 'x': 2, 'y': 12, 'z': 100},
628 {'extra': {'idx': 3}, 'x': 3, 'y': 13, 'z': 100},
629 {'x': 4, 'y': 14, 'z': 100},
630 ])
632 @patch('paho.mqtt.client.Client')
633 def test_listen_clear_last(self, mock_mqtt):
634 '''Test listen command: clear last message'''
635 mock_client = Mock()
636 mock_mqtt.return_value = mock_client
637 self.fb.state.last_messages = [{'#': "message"}]
638 self.fb.state.test_env = False
639 self.fb.listen()
640 self.assertEqual(len(self.fb.state.last_messages['#']), 0)
642 @patch('paho.mqtt.client.Client')
643 def test_publish_apply_label(self, mock_mqtt):
644 '''Test publish command: set uuid'''
645 mock_client = Mock()
646 mock_mqtt.return_value = mock_client
647 self.fb.state.test_env = False
648 self.fb.publish({'kind': 'sync', 'args': {}})
649 label = self.fb.state.last_published.get('args', {}).get('label')
650 self.assertNotIn(label, ['test', '', None])
652 @patch('requests.request')
653 @patch('paho.mqtt.client.Client')
654 def send_command_test_helper(self, *args, **kwargs):
655 '''Helper for testing command execution'''
656 execute_command = args[0]
657 mock_mqtt = args[1]
658 mock_request = args[2]
659 expected_command = kwargs.get('expected_command')
660 extra_rpc_args = kwargs.get('extra_rpc_args')
661 mock_api_response = kwargs.get('mock_api_response')
662 error = kwargs.get('error')
663 mock_client = Mock()
664 mock_mqtt.return_value = mock_client
665 mock_response = Mock()
666 mock_response.json.return_value = mock_api_response
667 mock_response.status_code = 200
668 mock_response.text = 'text'
669 mock_request.return_value = mock_response
670 self.fb.state.last_messages['from_device'] = [{
671 'kind': 'rpc_error' if error else 'rpc_ok',
672 'args': {'label': 'test'},
673 }]
674 execute_command()
675 if expected_command is None:
676 mock_client.publish.assert_not_called()
677 return
678 expected_payload = {
679 'kind': 'rpc_request',
680 'args': {'label': 'test', **extra_rpc_args},
681 'body': [expected_command],
682 }
683 mock_client.username_pw_set.assert_called_once_with(
684 username='device_0',
685 password='encoded_token_value')
686 mock_client.connect.assert_called_once_with(
687 'mqtt_url',
688 port=1883,
689 keepalive=60)
690 mock_client.loop_start.assert_called()
691 mock_client.publish.assert_called_once_with(
692 'bot/device_0/from_clients',
693 payload=json.dumps(expected_payload))
694 if not error:
695 self.assertNotEqual(
696 self.fb.state.error,
697 'RPC error response received.')
699 def test_message(self):
700 '''Test message command'''
701 def exec_command():
702 self.fb.message('test message', 'info')
703 self.send_command_test_helper(
704 exec_command,
705 expected_command={
706 'kind': 'send_message',
707 'args': {'message': 'test message', 'message_type': 'info'},
708 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}],
709 },
710 extra_rpc_args={},
711 mock_api_response={})
713 def test_debug(self):
714 '''Test debug command'''
715 def exec_command():
716 self.fb.debug('test message')
717 self.send_command_test_helper(
718 exec_command,
719 expected_command={
720 'kind': 'send_message',
721 'args': {'message': 'test message', 'message_type': 'debug'},
722 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}],
723 },
724 extra_rpc_args={},
725 mock_api_response={})
727 def test_toast(self):
728 '''Test toast command'''
729 def exec_command():
730 self.fb.toast('test message')
731 self.send_command_test_helper(
732 exec_command,
733 expected_command={
734 'kind': 'send_message',
735 'args': {'message': 'test message', 'message_type': 'info'},
736 'body': [{'kind': 'channel', 'args': {'channel_name': 'toast'}}],
737 },
738 extra_rpc_args={},
739 mock_api_response={})
741 def test_invalid_message_type(self):
742 '''Test message_type validation'''
743 def exec_command():
744 with self.assertRaises(ValueError) as cm:
745 self.fb.message('test', message_type='nope')
746 msg = 'Invalid message type: `nope` not in '
747 msg += "['assertion', 'busy', 'debug', 'error', 'fun', 'info', 'success', 'warn']"
748 self.assertEqual(cm.exception.args[0], msg)
749 self.send_command_test_helper(
750 exec_command,
751 expected_command=None,
752 extra_rpc_args={},
753 mock_api_response={})
755 def test_invalid_message_channel(self):
756 '''Test message channel validation'''
757 def exec_command():
758 with self.assertRaises(ValueError) as cm:
759 self.fb.message('test', channel='nope')
760 self.assertEqual(
761 cm.exception.args[0],
762 "Invalid channel: nope not in ['ticker', 'toast', 'email', 'espeak']")
763 self.send_command_test_helper(
764 exec_command,
765 expected_command=None,
766 extra_rpc_args={},
767 mock_api_response={})
769 def test_read_status(self):
770 '''Test read_status command'''
771 def exec_command():
772 self.fb.state.last_messages['status'] = [{
773 'location_data': {'position': {'x': 100}},
774 }]
775 result = self.fb.read_status()
776 self.assertEqual(
777 result,
778 {'location_data': {'position': {'x': 100}}})
779 self.send_command_test_helper(
780 exec_command,
781 expected_command={
782 'kind': 'read_status',
783 'args': {},
784 },
785 extra_rpc_args={},
786 mock_api_response={})
788 def test_read_status_path(self):
789 '''Test read_status command: specific path'''
790 def exec_command():
791 self.fb.state.last_messages['status'] = [{
792 'location_data': {'position': {'x': 100}},
793 }]
794 result = self.fb.read_status('location_data.position.x')
795 self.assertEqual(result, 100)
796 self.send_command_test_helper(
797 exec_command,
798 expected_command={
799 'kind': 'read_status',
800 'args': {},
801 },
802 extra_rpc_args={},
803 mock_api_response={})
805 def test_read_pin(self):
806 '''Test read_pin command'''
807 def exec_command():
808 self.fb.read_pin(13)
809 self.send_command_test_helper(
810 exec_command,
811 expected_command={
812 'kind': 'read_pin',
813 'args': {
814 'pin_number': 13,
815 'label': '---',
816 'pin_mode': 0,
817 },
818 },
819 extra_rpc_args={},
820 mock_api_response={})
822 def test_read_sensor(self):
823 '''Test read_sensor command'''
824 def exec_command():
825 self.fb.read_sensor('Tool Verification')
826 self.send_command_test_helper(
827 exec_command,
828 expected_command={
829 'kind': 'read_pin',
830 'args': {
831 'pin_mode': 0,
832 'label': '---',
833 'pin_number': {
834 'kind': 'named_pin',
835 'args': {'pin_type': 'Sensor', 'pin_id': 123},
836 },
837 },
838 },
839 extra_rpc_args={},
840 mock_api_response=[{'id': 123, 'label': 'Tool Verification', 'mode': 0}])
842 def test_read_sensor_not_found(self):
843 '''Test read_sensor command: sensor not found'''
844 def exec_command():
845 self.fb.read_sensor('Temperature')
846 self.send_command_test_helper(
847 exec_command,
848 expected_command=None,
849 extra_rpc_args={},
850 mock_api_response=[{'label': 'Tool Verification'}])
851 self.assertEqual(
852 self.fb.state.error,
853 "ERROR: 'Temperature' not in sensors: ['Tool Verification'].")
855 def test_assertion(self):
856 '''Test assertion command'''
857 def exec_command():
858 self.fb.assertion('return true', 'abort')
859 self.send_command_test_helper(
860 exec_command,
861 expected_command={
862 'kind': 'assertion',
863 'args': {
864 'assertion_type': 'abort',
865 'lua': 'return true',
866 '_then': {'kind': 'nothing', 'args': {}},
867 }
868 },
869 extra_rpc_args={},
870 mock_api_response={})
872 def test_assertion_with_recovery_sequence(self):
873 '''Test assertion command with recovery sequence'''
874 def exec_command():
875 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
876 self.send_command_test_helper(
877 exec_command,
878 expected_command={
879 'kind': 'assertion',
880 'args': {
881 'assertion_type': 'abort',
882 'lua': 'return true',
883 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
884 }
885 },
886 extra_rpc_args={},
887 mock_api_response=[{'id': 123, 'name': 'Recovery Sequence'}])
889 def test_assertion_recovery_sequence_not_found(self):
890 '''Test assertion command: recovery sequence not found'''
891 def exec_command():
892 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
893 self.send_command_test_helper(
894 exec_command,
895 expected_command=None,
896 extra_rpc_args={},
897 mock_api_response=[])
898 self.assertEqual(
899 self.fb.state.error,
900 "ERROR: 'Recovery Sequence' not in sequences: [].")
902 def test_assertion_invalid_assertion_type(self):
903 '''Test assertion command: invalid assertion type'''
904 def exec_command():
905 with self.assertRaises(ValueError) as cm:
906 self.fb.assertion('return true', 'nope')
907 msg = 'Invalid assertion_type: nope not in '
908 msg += "['abort', 'recover', 'abort_recover', 'continue']"
909 self.assertEqual(cm.exception.args[0], msg)
910 self.send_command_test_helper(
911 exec_command,
912 expected_command=None,
913 extra_rpc_args={},
914 mock_api_response={})
916 def test_wait(self):
917 '''Test wait command'''
918 def exec_command():
919 self.fb.wait(123)
920 self.send_command_test_helper(
921 exec_command,
922 expected_command={
923 'kind': 'wait',
924 'args': {'milliseconds': 123},
925 },
926 extra_rpc_args={},
927 mock_api_response={})
929 def test_unlock(self):
930 '''Test unlock command'''
931 def exec_command():
932 self.fb.unlock()
933 self.send_command_test_helper(
934 exec_command,
935 expected_command={
936 'kind': 'emergency_unlock',
937 'args': {},
938 },
939 extra_rpc_args={'priority': 9000},
940 mock_api_response={})
942 def test_e_stop(self):
943 '''Test e_stop command'''
944 def exec_command():
945 self.fb.e_stop()
946 self.send_command_test_helper(
947 exec_command,
948 expected_command={
949 'kind': 'emergency_lock',
950 'args': {},
951 },
952 extra_rpc_args={'priority': 9000},
953 mock_api_response={})
955 def test_find_home(self):
956 '''Test find_home command'''
957 def exec_command():
958 self.fb.find_home()
959 self.send_command_test_helper(
960 exec_command,
961 expected_command={
962 'kind': 'find_home',
963 'args': {'axis': 'all', 'speed': 100},
964 },
965 extra_rpc_args={},
966 mock_api_response={})
968 def test_find_home_speed_error(self):
969 '''Test find_home command: speed error'''
970 def exec_command():
971 self.fb.find_home('all', 0)
972 self.send_command_test_helper(
973 exec_command,
974 expected_command=None,
975 extra_rpc_args={},
976 mock_api_response={})
977 self.assertEqual(
978 self.fb.state.error,
979 'ERROR: Speed constrained to 1-100.')
981 def test_find_home_invalid_axis(self):
982 '''Test find_home command: invalid axis'''
983 def exec_command():
984 with self.assertRaises(ValueError) as cm:
985 self.fb.find_home('nope')
986 self.assertEqual(
987 cm.exception.args[0],
988 "Invalid axis: nope not in ['x', 'y', 'z', 'all']")
989 self.send_command_test_helper(
990 exec_command,
991 expected_command=None,
992 extra_rpc_args={},
993 mock_api_response={})
995 def test_set_home(self):
996 '''Test set_home command'''
997 def exec_command():
998 self.fb.set_home()
999 self.send_command_test_helper(
1000 exec_command,
1001 expected_command={
1002 'kind': 'zero',
1003 'args': {'axis': 'all'},
1004 },
1005 extra_rpc_args={},
1006 mock_api_response={})
1008 def test_toggle_peripheral(self):
1009 '''Test toggle_peripheral command'''
1010 def exec_command():
1011 self.fb.toggle_peripheral('New Peripheral')
1012 self.send_command_test_helper(
1013 exec_command,
1014 expected_command={
1015 'kind': 'toggle_pin',
1016 'args': {
1017 'pin_number': {
1018 'kind': 'named_pin',
1019 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1020 },
1021 },
1022 },
1023 extra_rpc_args={},
1024 mock_api_response=[{'label': 'New Peripheral', 'id': 123}])
1026 def test_toggle_peripheral_not_found(self):
1027 '''Test toggle_peripheral command: peripheral not found'''
1028 def exec_command():
1029 self.fb.toggle_peripheral('New Peripheral')
1030 self.send_command_test_helper(
1031 exec_command,
1032 expected_command=None,
1033 extra_rpc_args={},
1034 mock_api_response=[])
1035 self.assertEqual(
1036 self.fb.state.error,
1037 'ERROR: \'New Peripheral\' not in peripherals: [].')
1039 @patch('requests.request')
1040 @patch('paho.mqtt.client.Client')
1041 def test_toggle_peripheral_use_cache(self, mock_mqtt, mock_request):
1042 '''Test toggle_peripheral command: use cache'''
1043 mock_client = Mock()
1044 mock_mqtt.return_value = mock_client
1045 mock_response = Mock()
1046 mock_response.json.return_value = [
1047 {'label': 'Peripheral 4', 'id': 123},
1048 {'label': 'Peripheral 5', 'id': 456}
1049 ]
1050 mock_response.status_code = 200
1051 mock_response.text = 'text'
1052 mock_request.return_value = mock_response
1053 # save cache
1054 self.fb.toggle_peripheral('Peripheral 4')
1055 mock_request.assert_called()
1056 mock_client.publish.assert_called()
1057 mock_request.reset_mock()
1058 mock_client.reset_mock()
1059 # use cache
1060 self.fb.toggle_peripheral('Peripheral 5')
1061 mock_request.assert_not_called()
1062 mock_client.publish.assert_called()
1063 mock_request.reset_mock()
1064 mock_client.reset_mock()
1065 # clear cache
1066 self.fb.toggle_peripheral('Peripheral 6')
1067 mock_request.assert_not_called()
1068 mock_client.publish.assert_not_called()
1069 mock_request.reset_mock()
1070 mock_client.reset_mock()
1071 # save cache
1072 self.fb.toggle_peripheral('Peripheral 4')
1073 mock_request.assert_called()
1074 mock_client.publish.assert_called()
1075 mock_request.reset_mock()
1076 mock_client.reset_mock()
1078 def test_on_digital(self):
1079 '''Test on command: digital'''
1080 def exec_command():
1081 self.fb.on(13)
1082 self.send_command_test_helper(
1083 exec_command,
1084 expected_command={
1085 'kind': 'write_pin',
1086 'args': {
1087 'pin_value': 1,
1088 'pin_mode': 0,
1089 'pin_number': 13,
1090 },
1091 },
1092 extra_rpc_args={},
1093 mock_api_response={})
1095 def test_off(self):
1096 '''Test off command'''
1097 def exec_command():
1098 self.fb.off(13)
1099 self.send_command_test_helper(
1100 exec_command,
1101 expected_command={
1102 'kind': 'write_pin',
1103 'args': {
1104 'pin_value': 0,
1105 'pin_mode': 0,
1106 'pin_number': 13,
1107 },
1108 },
1109 extra_rpc_args={},
1110 mock_api_response={})
1112 def test_move(self):
1113 '''Test move command'''
1114 def exec_command():
1115 self.fb.move()
1116 self.send_command_test_helper(
1117 exec_command,
1118 expected_command={
1119 'kind': 'move',
1120 'args': {},
1121 'body': [],
1122 },
1123 extra_rpc_args={},
1124 mock_api_response={})
1126 def test_move_extras(self):
1127 '''Test move command with extras'''
1128 def exec_command():
1129 self.fb.move(1, 2, 3, safe_z=True, speed=50)
1130 self.send_command_test_helper(
1131 exec_command,
1132 expected_command={
1133 'kind': 'move',
1134 'args': {},
1135 'body': [
1136 {'kind': 'axis_overwrite', 'args': {
1137 'axis': 'x',
1138 'axis_operand': {'kind': 'numeric', 'args': {'number': 1}}}},
1139 {'kind': 'axis_overwrite', 'args': {
1140 'axis': 'y',
1141 'axis_operand': {'kind': 'numeric', 'args': {'number': 2}}}},
1142 {'kind': 'axis_overwrite', 'args': {
1143 'axis': 'z',
1144 'axis_operand': {'kind': 'numeric', 'args': {'number': 3}}}},
1145 {'kind': 'speed_overwrite', 'args': {
1146 'axis': 'x',
1147 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}},
1148 {'kind': 'speed_overwrite', 'args': {
1149 'axis': 'y',
1150 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}},
1151 {'kind': 'speed_overwrite', 'args': {
1152 'axis': 'z',
1153 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}},
1154 {'kind': 'safe_z', 'args': {}},
1155 ],
1156 },
1157 extra_rpc_args={},
1158 mock_api_response={})
1160 def test_reboot(self):
1161 '''Test reboot command'''
1162 def exec_command():
1163 self.fb.reboot()
1164 self.send_command_test_helper(
1165 exec_command,
1166 expected_command={
1167 'kind': 'reboot',
1168 'args': {'package': 'farmbot_os'},
1169 },
1170 extra_rpc_args={},
1171 mock_api_response={})
1173 def test_shutdown(self):
1174 '''Test shutdown command'''
1175 def exec_command():
1176 self.fb.shutdown()
1177 self.send_command_test_helper(
1178 exec_command,
1179 expected_command={
1180 'kind': 'power_off',
1181 'args': {},
1182 },
1183 extra_rpc_args={},
1184 mock_api_response={})
1186 def test_find_axis_length(self):
1187 '''Test find_axis_length command'''
1188 def exec_command():
1189 self.fb.find_axis_length()
1190 self.send_command_test_helper(
1191 exec_command,
1192 expected_command={
1193 'kind': 'calibrate',
1194 'args': {'axis': 'all'},
1195 },
1196 extra_rpc_args={},
1197 mock_api_response={})
1199 def test_write_pin(self):
1200 '''Test write_pin command'''
1201 def exec_command():
1202 self.fb.write_pin(13, 1, 'analog')
1203 self.send_command_test_helper(
1204 exec_command,
1205 expected_command={
1206 'kind': 'write_pin',
1207 'args': {
1208 'pin_number': 13,
1209 'pin_value': 1,
1210 'pin_mode': 1,
1211 },
1212 },
1213 extra_rpc_args={},
1214 mock_api_response={})
1216 def test_write_pin_invalid_mode(self):
1217 '''Test write_pin command: invalid mode'''
1218 def exec_command():
1219 with self.assertRaises(ValueError) as cm:
1220 self.fb.write_pin(13, 1, 1)
1221 self.assertEqual(
1222 cm.exception.args[0],
1223 "Invalid mode: 1 not in ['digital', 'analog']")
1224 self.send_command_test_helper(
1225 exec_command,
1226 expected_command=None,
1227 extra_rpc_args={},
1228 mock_api_response={})
1230 def test_control_peripheral(self):
1231 '''Test control_peripheral command'''
1232 def exec_command():
1233 self.fb.control_peripheral('New Peripheral', 1)
1234 self.send_command_test_helper(
1235 exec_command,
1236 expected_command={
1237 'kind': 'write_pin',
1238 'args': {
1239 'pin_value': 1,
1240 'pin_mode': 0,
1241 'pin_number': {
1242 'kind': 'named_pin',
1243 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1244 },
1245 },
1246 },
1247 extra_rpc_args={},
1248 mock_api_response=[{'label': 'New Peripheral', 'mode': 0, 'id': 123}])
1250 def test_control_peripheral_analog(self):
1251 '''Test control_peripheral command: analog'''
1252 def exec_command():
1253 self.fb.control_peripheral('New Peripheral', 1, 'analog')
1254 self.send_command_test_helper(
1255 exec_command,
1256 expected_command={
1257 'kind': 'write_pin',
1258 'args': {
1259 'pin_value': 1,
1260 'pin_mode': 1,
1261 'pin_number': {
1262 'kind': 'named_pin',
1263 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1264 },
1265 },
1266 },
1267 extra_rpc_args={},
1268 mock_api_response=[{'label': 'New Peripheral', 'mode': 0, 'id': 123}])
1270 def test_control_peripheral_not_found(self):
1271 '''Test control_peripheral command: peripheral not found'''
1272 def exec_command():
1273 self.fb.control_peripheral('New Peripheral', 1)
1274 self.send_command_test_helper(
1275 exec_command,
1276 expected_command=None,
1277 extra_rpc_args={},
1278 mock_api_response=[{'label': 'Pump'}, {'label': 'Lights'}])
1279 self.assertEqual(
1280 self.fb.state.error,
1281 "ERROR: 'New Peripheral' not in peripherals: ['Pump', 'Lights'].")
1283 def test_measure_soil_height(self):
1284 '''Test measure_soil_height command'''
1285 def exec_command():
1286 self.fb.measure_soil_height()
1287 self.send_command_test_helper(
1288 exec_command,
1289 expected_command={
1290 'kind': 'execute_script',
1291 'args': {'label': 'Measure Soil Height'},
1292 },
1293 extra_rpc_args={},
1294 mock_api_response={})
1296 def test_detect_weeds(self):
1297 '''Test detect_weeds command'''
1298 def exec_command():
1299 self.fb.detect_weeds()
1300 self.send_command_test_helper(
1301 exec_command,
1302 expected_command={
1303 'kind': 'execute_script',
1304 'args': {'label': 'plant-detection'},
1305 },
1306 extra_rpc_args={},
1307 mock_api_response={})
1309 def test_calibrate_camera(self):
1310 '''Test calibrate_camera command'''
1311 def exec_command():
1312 self.fb.calibrate_camera()
1313 self.send_command_test_helper(
1314 exec_command,
1315 expected_command={
1316 'kind': 'execute_script',
1317 'args': {'label': 'camera-calibration'},
1318 },
1319 extra_rpc_args={},
1320 mock_api_response={})
1322 def test_sequence(self):
1323 '''Test sequence command'''
1324 def exec_command():
1325 self.fb.sequence('My Sequence')
1326 self.send_command_test_helper(
1327 exec_command,
1328 expected_command={
1329 'kind': 'execute',
1330 'args': {'sequence_id': 123},
1331 },
1332 extra_rpc_args={},
1333 mock_api_response=[{'name': 'My Sequence', 'id': 123}])
1335 def test_sequence_not_found(self):
1336 '''Test sequence command: sequence not found'''
1337 def exec_command():
1338 self.fb.sequence('My Sequence')
1339 self.send_command_test_helper(
1340 exec_command,
1341 expected_command=None,
1342 extra_rpc_args={},
1343 mock_api_response=[{'name': 'Water'}])
1344 self.assertEqual(
1345 self.fb.state.error,
1346 "ERROR: 'My Sequence' not in sequences: ['Water'].")
1348 def test_take_photo(self):
1349 '''Test take_photo command'''
1350 def exec_command():
1351 self.fb.take_photo()
1352 self.send_command_test_helper(
1353 exec_command,
1354 expected_command={
1355 'kind': 'take_photo',
1356 'args': {},
1357 },
1358 extra_rpc_args={},
1359 mock_api_response={})
1361 def test_control_servo(self):
1362 '''Test control_servo command'''
1363 def exec_command():
1364 self.fb.control_servo(4, 100)
1365 self.send_command_test_helper(
1366 exec_command,
1367 expected_command={
1368 'kind': 'set_servo_angle',
1369 'args': {
1370 'pin_number': 4,
1371 'pin_value': 100,
1372 },
1373 },
1374 extra_rpc_args={},
1375 mock_api_response={'mode': 0})
1377 def test_control_servo_error(self):
1378 '''Test control_servo command: error'''
1379 def exec_command():
1380 self.fb.control_servo(4, 200)
1381 self.send_command_test_helper(
1382 exec_command,
1383 expected_command=None,
1384 extra_rpc_args={},
1385 mock_api_response={'mode': 0})
1387 def test_get_xyz(self):
1388 '''Test get_xyz command'''
1389 def exec_command():
1390 self.fb.state.last_messages['status'] = [{
1391 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1392 }]
1393 position = self.fb.get_xyz()
1394 self.assertEqual(position, {'x': 1, 'y': 2, 'z': 3})
1395 self.send_command_test_helper(
1396 exec_command,
1397 expected_command={
1398 'kind': 'read_status',
1399 'args': {},
1400 },
1401 extra_rpc_args={},
1402 mock_api_response={})
1404 def test_get_xyz_no_status(self):
1405 '''Test get_xyz command: no status'''
1406 def exec_command():
1407 self.fb.state.last_messages['status'] = []
1408 position = self.fb.get_xyz()
1409 self.assertIsNone(position)
1410 self.send_command_test_helper(
1411 exec_command,
1412 expected_command={
1413 'kind': 'read_status',
1414 'args': {},
1415 },
1416 extra_rpc_args={},
1417 mock_api_response={})
1419 def test_check_position(self):
1420 '''Test check_position command: at position'''
1421 def exec_command():
1422 self.fb.state.last_messages['status'] = [{
1423 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1424 }]
1425 at_position = self.fb.check_position({'x': 1, 'y': 2, 'z': 3}, 0)
1426 self.assertTrue(at_position)
1427 self.send_command_test_helper(
1428 exec_command,
1429 expected_command={
1430 'kind': 'read_status',
1431 'args': {},
1432 },
1433 extra_rpc_args={},
1434 mock_api_response={})
1436 def test_check_position_false(self):
1437 '''Test check_position command: not at position'''
1438 def exec_command():
1439 self.fb.state.last_messages['status'] = [{
1440 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1441 }]
1442 at_position = self.fb.check_position({'x': 0, 'y': 0, 'z': 0}, 2)
1443 self.assertFalse(at_position)
1444 self.send_command_test_helper(
1445 exec_command,
1446 expected_command={
1447 'kind': 'read_status',
1448 'args': {},
1449 },
1450 extra_rpc_args={},
1451 mock_api_response={})
1453 def test_check_position_no_status(self):
1454 '''Test check_position command: no status'''
1455 def exec_command():
1456 self.fb.state.last_messages['status'] = []
1457 at_position = self.fb.check_position({'x': 0, 'y': 0, 'z': 0}, 2)
1458 self.assertFalse(at_position)
1459 self.send_command_test_helper(
1460 exec_command,
1461 expected_command={
1462 'kind': 'read_status',
1463 'args': {},
1464 },
1465 extra_rpc_args={},
1466 mock_api_response={})
1468 def test_mount_tool(self):
1469 '''Test mount_tool command'''
1470 def exec_command():
1471 self.fb.mount_tool('Weeder')
1472 self.send_command_test_helper(
1473 exec_command,
1474 expected_command={
1475 'kind': 'lua',
1476 'args': {'lua': 'mount_tool("Weeder")'},
1477 },
1478 extra_rpc_args={},
1479 mock_api_response={})
1481 def test_dismount_tool(self):
1482 '''Test dismount_tool command'''
1483 def exec_command():
1484 self.fb.dismount_tool()
1485 self.send_command_test_helper(
1486 exec_command,
1487 expected_command={
1488 'kind': 'lua',
1489 'args': {'lua': 'dismount_tool()'},
1490 },
1491 extra_rpc_args={},
1492 mock_api_response={})
1494 def test_water(self):
1495 '''Test water command'''
1496 def exec_command():
1497 self.fb.water(123)
1498 self.send_command_test_helper(
1499 exec_command,
1500 expected_command={
1501 'kind': 'lua',
1502 'args': {'lua': '''plant = api({
1503 method = "GET",
1504 url = "/api/points/123"
1505 })
1506 water(plant)'''},
1507 },
1508 extra_rpc_args={},
1509 mock_api_response={})
1511 def test_dispense(self):
1512 '''Test dispense command'''
1513 def exec_command():
1514 self.fb.dispense(100)
1515 self.send_command_test_helper(
1516 exec_command,
1517 expected_command={
1518 'kind': 'lua',
1519 'args': {
1520 'lua': 'dispense(100)',
1521 },
1522 },
1523 extra_rpc_args={},
1524 mock_api_response={})
1526 def test_dispense_all_args(self):
1527 '''Test dispense command with all args'''
1528 def exec_command():
1529 self.fb.dispense(100, 'Nutrient Sprayer', 4)
1530 self.send_command_test_helper(
1531 exec_command,
1532 expected_command={
1533 'kind': 'lua',
1534 'args': {
1535 'lua': 'dispense(100, {tool_name = "Nutrient Sprayer", pin = 4})',
1536 },
1537 },
1538 extra_rpc_args={},
1539 mock_api_response={})
1541 def test_dispense_only_pin(self):
1542 '''Test dispense command'''
1543 def exec_command():
1544 self.fb.dispense(100, pin=4)
1545 self.send_command_test_helper(
1546 exec_command,
1547 expected_command={
1548 'kind': 'lua',
1549 'args': {
1550 'lua': 'dispense(100, {pin = 4})',
1551 },
1552 },
1553 extra_rpc_args={},
1554 mock_api_response={})
1556 def test_dispense_only_tool_name(self):
1557 '''Test dispense command'''
1558 def exec_command():
1559 self.fb.dispense(100, "Nutrient Sprayer")
1560 self.send_command_test_helper(
1561 exec_command,
1562 expected_command={
1563 'kind': 'lua',
1564 'args': {
1565 'lua': 'dispense(100, {tool_name = "Nutrient Sprayer"})',
1566 },
1567 },
1568 extra_rpc_args={},
1569 mock_api_response={})
1571 @patch('requests.request')
1572 def helper_get_seed_tray_cell(self, *args, **kwargs):
1573 '''Test helper for get_seed_tray_cell command'''
1574 mock_request = args[0]
1575 tray_data = kwargs['tray_data']
1576 cell = kwargs['cell']
1577 expected_xyz = kwargs['expected_xyz']
1578 self.fb.clear_cache()
1579 mock_response = Mock()
1580 mock_api_response = [
1581 {
1582 'id': 123,
1583 'name': 'Seed Tray',
1584 'pointer_type': '', # not an actual data field
1585 },
1586 {
1587 'pointer_type': 'ToolSlot',
1588 'pullout_direction': 1,
1589 'x': 0,
1590 'y': 0,
1591 'z': 0,
1592 'tool_id': 123,
1593 'name': '', # not an actual data field
1594 **tray_data,
1595 },
1596 ]
1597 mock_response.json.return_value = mock_api_response
1598 mock_response.status_code = 200
1599 mock_response.text = 'text'
1600 mock_request.return_value = mock_response
1601 cell = self.fb.get_seed_tray_cell('Seed Tray', cell)
1602 mock_request.assert_has_calls([
1603 call(
1604 method='GET',
1605 url='https://my.farm.bot/api/tools',
1606 **REQUEST_KWARGS,
1607 ),
1608 call().json(),
1609 call(
1610 method='GET',
1611 url='https://my.farm.bot/api/points',
1612 **REQUEST_KWARGS,
1613 ),
1614 call().json(),
1615 ])
1616 self.assertEqual(cell, expected_xyz, kwargs)
1618 def test_get_seed_tray_cell(self):
1619 '''Test get_seed_tray_cell'''
1620 test_cases = [
1621 {
1622 'tray_data': {'pullout_direction': 1},
1623 'cell': 'a1',
1624 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1625 },
1626 {
1627 'tray_data': {'pullout_direction': 1},
1628 'cell': 'b2',
1629 'expected_xyz': {'x': -11.25, 'y': -6.25, 'z': 0},
1630 },
1631 {
1632 'tray_data': {'pullout_direction': 1},
1633 'cell': 'd4',
1634 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1635 },
1636 {
1637 'tray_data': {'pullout_direction': 2},
1638 'cell': 'a1',
1639 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1640 },
1641 {
1642 'tray_data': {'pullout_direction': 2},
1643 'cell': 'b2',
1644 'expected_xyz': {'x': -23.75, 'y': 6.25, 'z': 0},
1645 },
1646 {
1647 'tray_data': {'pullout_direction': 2},
1648 'cell': 'd4',
1649 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1650 },
1651 {
1652 'tray_data': {'pullout_direction': 2, 'x': 100, 'y': 200, 'z': -100},
1653 'cell': 'd4',
1654 'expected_xyz': {'x': 101.25, 'y': 181.25, 'z': -100},
1655 },
1656 ]
1657 for test_case in test_cases:
1658 self.helper_get_seed_tray_cell(**test_case)
1660 @patch('requests.request')
1661 def helper_get_seed_tray_cell_error(self, *args, **kwargs):
1662 '''Test helper for get_seed_tray_cell command errors'''
1663 mock_request = args[0]
1664 tray_data = kwargs['tray_data']
1665 cell = kwargs['cell']
1666 error = kwargs['error']
1667 mock_response = Mock()
1668 mock_api_response = [
1669 {
1670 'id': 123,
1671 'name': 'Seed Tray',
1672 'pointer_type': '', # not an actual data field
1673 },
1674 {
1675 'pointer_type': 'ToolSlot',
1676 'pullout_direction': 1,
1677 'x': 0,
1678 'y': 0,
1679 'z': 0,
1680 'tool_id': 123,
1681 'name': '', # not an actual data field
1682 **tray_data,
1683 },
1684 ]
1685 mock_response.json.return_value = mock_api_response
1686 mock_response.status_code = 200
1687 mock_response.text = 'text'
1688 mock_request.return_value = mock_response
1689 with self.assertRaises(ValueError) as cm:
1690 self.fb.get_seed_tray_cell('Seed Tray', cell)
1691 self.assertEqual(cm.exception.args[0], error)
1692 mock_request.assert_has_calls([
1693 call(
1694 method='GET',
1695 url='https://my.farm.bot/api/tools',
1696 **REQUEST_KWARGS,
1697 ),
1698 call().json(),
1699 call(
1700 method='GET',
1701 url='https://my.farm.bot/api/points',
1702 **REQUEST_KWARGS,
1703 ),
1704 call().json(),
1705 ])
1707 def test_get_seed_tray_cell_invalid_cell_name(self):
1708 '''Test get_seed_tray_cell: invalid cell name'''
1709 self.helper_get_seed_tray_cell_error(
1710 tray_data={},
1711 cell='e4',
1712 error='Seed Tray Cell must be one of **A1** through **D4**',
1713 )
1715 def test_get_seed_tray_cell_invalid_pullout_direction(self):
1716 '''Test get_seed_tray_cell: invalid pullout direction'''
1717 self.helper_get_seed_tray_cell_error(
1718 tray_data={'pullout_direction': 0},
1719 cell='d4',
1720 error='Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`',
1721 )
1723 @patch('requests.request')
1724 def test_get_seed_tray_cell_no_tray(self, mock_request):
1725 '''Test get_seed_tray_cell: no seed tray'''
1726 mock_response = Mock()
1727 mock_api_response = []
1728 mock_response.json.return_value = mock_api_response
1729 mock_response.status_code = 200
1730 mock_response.text = 'text'
1731 mock_request.return_value = mock_response
1732 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1733 mock_request.assert_has_calls([
1734 call(
1735 method='GET',
1736 url='https://my.farm.bot/api/tools',
1737 **REQUEST_KWARGS,
1738 ),
1739 call().json(),
1740 ])
1741 self.assertIsNone(result)
1743 @patch('requests.request')
1744 def test_get_seed_tray_cell_not_mounted(self, mock_request):
1745 '''Test get_seed_tray_cell: seed tray not mounted'''
1746 mock_response = Mock()
1747 mock_api_response = [{
1748 'id': 123,
1749 'name': 'Seed Tray',
1750 'pointer_type': '', # not an actual data field,
1751 }]
1752 mock_response.json.return_value = mock_api_response
1753 mock_response.status_code = 200
1754 mock_response.text = 'text'
1755 mock_request.return_value = mock_response
1756 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1757 mock_request.assert_has_calls([
1758 call(
1759 method='GET',
1760 url='https://my.farm.bot/api/tools',
1761 **REQUEST_KWARGS,
1762 ),
1763 call().json(),
1764 ])
1765 self.assertIsNone(result)
1767 def test_get_job_one(self):
1768 '''Test get_job command: get one job'''
1769 def exec_command():
1770 self.fb.state.last_messages['status'] = [{
1771 'jobs': {
1772 'job name': {'status': 'working'},
1773 },
1774 }]
1775 job = self.fb.get_job('job name')
1776 self.assertEqual(job, {'status': 'working'})
1777 self.send_command_test_helper(
1778 exec_command,
1779 expected_command={
1780 'kind': 'read_status',
1781 'args': {},
1782 },
1783 extra_rpc_args={},
1784 mock_api_response={})
1786 def test_get_job_all(self):
1787 '''Test get_job command: get all jobs'''
1788 def exec_command():
1789 self.fb.state.last_messages['status'] = [{
1790 'jobs': {
1791 'job name': {'status': 'working'},
1792 },
1793 }]
1794 jobs = self.fb.get_job()
1795 self.assertEqual(jobs, {'job name': {'status': 'working'}})
1796 self.send_command_test_helper(
1797 exec_command,
1798 expected_command={
1799 'kind': 'read_status',
1800 'args': {},
1801 },
1802 extra_rpc_args={},
1803 mock_api_response={})
1805 def test_get_job_no_status(self):
1806 '''Test get_job command: no status'''
1807 def exec_command():
1808 self.fb.state.last_messages['status'] = []
1809 job = self.fb.get_job('job name')
1810 self.assertIsNone(job)
1811 self.send_command_test_helper(
1812 exec_command,
1813 expected_command={
1814 'kind': 'read_status',
1815 'args': {},
1816 },
1817 extra_rpc_args={},
1818 mock_api_response={})
1820 def test_set_job(self):
1821 '''Test set_job command'''
1822 def exec_command():
1823 self.fb.set_job('job name', 'working', 50)
1824 self.send_command_test_helper(
1825 exec_command,
1826 expected_command={
1827 'kind': 'lua',
1828 'args': {'lua': '''local job_name = "job name"
1829 set_job(job_name)
1831 -- Update the job's status and percent:
1832 set_job(job_name, {
1833 status = "working",
1834 percent = 50
1835 })'''},
1836 },
1837 extra_rpc_args={},
1838 mock_api_response={})
1840 def test_complete_job(self):
1841 '''Test complete_job command'''
1842 def exec_command():
1843 self.fb.complete_job('job name')
1844 self.send_command_test_helper(
1845 exec_command,
1846 expected_command={
1847 'kind': 'lua',
1848 'args': {'lua': 'complete_job("job name")'},
1849 },
1850 extra_rpc_args={},
1851 mock_api_response={})
1853 def test_lua(self):
1854 '''Test lua command'''
1855 def exec_command():
1856 self.fb.lua('return true')
1857 self.send_command_test_helper(
1858 exec_command,
1859 expected_command={
1860 'kind': 'lua',
1861 'args': {'lua': 'return true'},
1862 },
1863 extra_rpc_args={},
1864 mock_api_response={})
1866 def test_if_statement(self):
1867 '''Test if_statement command'''
1868 def exec_command():
1869 self.fb.if_statement('pin10', 'is', 0)
1870 self.send_command_test_helper(
1871 exec_command,
1872 expected_command={
1873 'kind': '_if',
1874 'args': {
1875 'lhs': 'pin10',
1876 'op': 'is',
1877 'rhs': 0,
1878 '_then': {'kind': 'nothing', 'args': {}},
1879 '_else': {'kind': 'nothing', 'args': {}},
1880 }
1881 },
1882 extra_rpc_args={},
1883 mock_api_response=[])
1885 def test_if_statement_with_named_pin(self):
1886 '''Test if_statement command with named pin'''
1887 def exec_command():
1888 self.fb.if_statement(
1889 'Lights', 'is', 0,
1890 named_pin_type='Peripheral')
1891 self.send_command_test_helper(
1892 exec_command,
1893 expected_command={
1894 'kind': '_if',
1895 'args': {
1896 'lhs': {
1897 'kind': 'named_pin',
1898 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1899 },
1900 'op': 'is',
1901 'rhs': 0,
1902 '_then': {'kind': 'nothing', 'args': {}},
1903 '_else': {'kind': 'nothing', 'args': {}},
1904 }
1905 },
1906 extra_rpc_args={},
1907 mock_api_response=[{'id': 123, 'label': 'Lights', 'mode': 0}])
1909 def test_if_statement_with_named_pin_not_found(self):
1910 '''Test if_statement command: named pin not found'''
1911 def exec_command():
1912 self.fb.if_statement(
1913 'Lights', 'is', 0,
1914 named_pin_type='Peripheral')
1915 self.send_command_test_helper(
1916 exec_command,
1917 expected_command=None,
1918 extra_rpc_args={},
1919 mock_api_response=[{'label': 'Pump'}])
1920 self.assertEqual(
1921 self.fb.state.error,
1922 "ERROR: 'Lights' not in peripherals: ['Pump'].")
1924 def test_if_statement_with_sequences(self):
1925 '''Test if_statement command with sequences'''
1926 def exec_command():
1927 self.fb.if_statement(
1928 'pin10', '<', 0,
1929 'Watering Sequence',
1930 'Drying Sequence')
1931 self.send_command_test_helper(
1932 exec_command,
1933 expected_command={
1934 'kind': '_if',
1935 'args': {
1936 'lhs': 'pin10',
1937 'op': '<',
1938 'rhs': 0,
1939 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
1940 '_else': {'kind': 'execute', 'args': {'sequence_id': 456}},
1941 }
1942 },
1943 extra_rpc_args={},
1944 mock_api_response=[
1945 {'id': 123, 'name': 'Watering Sequence'},
1946 {'id': 456, 'name': 'Drying Sequence'},
1947 ])
1949 def test_if_statement_with_sequence_not_found(self):
1950 '''Test if_statement command: sequence not found'''
1951 def exec_command():
1952 self.fb.if_statement(
1953 'pin10', '<', 0,
1954 'Watering Sequence',
1955 'Drying Sequence')
1956 self.send_command_test_helper(
1957 exec_command,
1958 expected_command=None,
1959 extra_rpc_args={},
1960 mock_api_response=[])
1961 self.assertEqual(
1962 self.fb.state.error,
1963 "ERROR: 'Watering Sequence' not in sequences: [].")
1965 def test_if_statement_invalid_operator(self):
1966 '''Test if_statement command: invalid operator'''
1967 def exec_command():
1968 with self.assertRaises(ValueError) as cm:
1969 self.fb.if_statement('pin10', 'nope', 0)
1970 self.assertEqual(
1971 cm.exception.args[0],
1972 "Invalid operator: nope not in ['<', '>', 'is', 'not', 'is_undefined']")
1973 self.send_command_test_helper(
1974 exec_command,
1975 expected_command=None,
1976 extra_rpc_args={},
1977 mock_api_response=[])
1979 def test_if_statement_invalid_variable(self):
1980 '''Test if_statement command: invalid variable'''
1981 variables = ["x", "y", "z", *[f"pin{str(i)}" for i in range(70)]]
1983 def exec_command():
1984 with self.assertRaises(ValueError) as cm:
1985 self.fb.if_statement('nope', '<', 0)
1986 self.assertEqual(
1987 cm.exception.args[0],
1988 f"Invalid variable: nope not in {variables}")
1989 self.send_command_test_helper(
1990 exec_command,
1991 expected_command=None,
1992 extra_rpc_args={},
1993 mock_api_response=[])
1995 def test_if_statement_invalid_named_pin_type(self):
1996 '''Test if_statement command: invalid named pin type'''
1997 def exec_command():
1998 with self.assertRaises(ValueError) as cm:
1999 self.fb.if_statement('pin10', '<', 0, named_pin_type='nope')
2000 self.assertEqual(
2001 cm.exception.args[0],
2002 "Invalid named_pin_type: nope not in ['Peripheral', 'Sensor']")
2003 self.send_command_test_helper(
2004 exec_command,
2005 expected_command=None,
2006 extra_rpc_args={},
2007 mock_api_response=[])
2009 def test_rpc_error(self):
2010 '''Test rpc error handling'''
2011 def exec_command():
2012 self.fb.wait(100)
2013 self.assertEqual(
2014 self.fb.state.error,
2015 'RPC error response received.')
2016 self.send_command_test_helper(
2017 exec_command,
2018 error=True,
2019 expected_command={
2020 'kind': 'wait',
2021 'args': {'milliseconds': 100}},
2022 extra_rpc_args={},
2023 mock_api_response=[])
2025 def test_rpc_response_timeout(self):
2026 '''Test rpc response timeout handling'''
2027 def exec_command():
2028 self.fb.state.last_messages['from_device'] = [
2029 {'kind': 'rpc_ok', 'args': {'label': 'wrong label'}},
2030 ]
2031 self.fb.wait(100)
2032 self.assertEqual(
2033 self.fb.state.error,
2034 'Timed out waiting for RPC response.')
2035 self.send_command_test_helper(
2036 exec_command,
2037 expected_command={
2038 'kind': 'wait',
2039 'args': {'milliseconds': 100}},
2040 extra_rpc_args={},
2041 mock_api_response=[])
2043 def test_set_verbosity(self):
2044 '''Test set_verbosity.'''
2045 self.assertEqual(self.fb.state.verbosity, 0)
2046 self.fb.set_verbosity(1)
2047 self.assertEqual(self.fb.state.verbosity, 1)
2049 def test_set_timeout(self):
2050 '''Test set_timeout.'''
2051 self.assertEqual(self.fb.state.timeout['listen'], 0)
2052 self.fb.set_timeout(15)
2053 self.assertEqual(self.fb.state.timeout['listen'], 15)
2055 @staticmethod
2056 def helper_get_print_strings(mock_print):
2057 '''Test helper to get print call strings.'''
2058 return [string[1][0] for string in mock_print.mock_calls if len(string[1]) > 0]
2060 @patch('builtins.print')
2061 def test_print_status(self, mock_print):
2062 '''Test print_status.'''
2063 self.fb.set_verbosity(0)
2064 self.fb.state.print_status(description="testing")
2065 mock_print.assert_not_called()
2066 self.fb.set_verbosity(1)
2067 self.fb.state.print_status(description="testing")
2068 call_strings = self.helper_get_print_strings(mock_print)
2069 self.assertIn('testing', call_strings)
2070 mock_print.reset_mock()
2071 self.fb.set_verbosity(2)
2072 self.fb.state.print_status(endpoint_json=["testing"])
2073 call_strings = self.helper_get_print_strings(mock_print)
2074 call_strings = [s.split('(')[0].strip('`') for s in call_strings]
2075 self.assertIn('[\n "testing"\n]', call_strings)
2076 self.assertIn('test_print_status', call_strings)