Coverage for tests/tests_main.py: 100%
826 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-12 12:18 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-12 12:18 -0700
1'''
2Farmbot class unit tests.
3'''
5import json
6import unittest
7from unittest.mock import Mock, patch, call
8import requests
10from farmbot 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 helper_test_get_curve(self, *args, **kwargs):
401 '''get_curve function test helper'''
402 mock_request = args[0]
403 mock_response = Mock()
404 mock_response.json.return_value = {
405 'name': 'Curve 0',
406 **kwargs.get('api_data'),
407 }
408 mock_response.status_code = 200
409 mock_response.text = 'text'
410 mock_request.return_value = mock_response
411 curve_info = self.fb.get_curve(12345)
412 mock_request.assert_called_once_with(
413 method='GET',
414 url='https://my.farm.bot/api/curves/12345',
415 **REQUEST_KWARGS,
416 )
417 self.assertEqual(curve_info['name'], 'Curve 0')
418 self.assertEqual(curve_info['type'], kwargs.get('type'))
419 self.assertEqual(curve_info['unit'], kwargs.get('unit'))
420 self.assertEqual(curve_info.day(50), kwargs.get('value'))
422 def test_get_curve(self):
423 '''test get_curve function'''
424 self.helper_test_get_curve(
425 api_data={
426 'type': 'water',
427 'data': {'1': 1, '100': 100},
428 },
429 type='water',
430 unit='mL',
431 value=50,
432 )
433 self.helper_test_get_curve(
434 api_data={
435 'type': 'water',
436 'data': {'100': 1, '200': 100},
437 },
438 type='water',
439 unit='mL',
440 value=1,
441 )
442 self.helper_test_get_curve(
443 api_data={
444 'type': 'water',
445 'data': {'1': 1, '2': 100},
446 },
447 type='water',
448 unit='mL',
449 value=100,
450 )
451 self.helper_test_get_curve(
452 api_data={
453 'type': 'height',
454 'data': {'1': 1, '50': 500, '100': 100},
455 },
456 type='height',
457 unit='mm',
458 value=500,
459 )
461 @patch('requests.request')
462 def test_get_curve_error(self, mock_request):
463 '''test get_curve function: error'''
464 mock_response = Mock()
465 mock_response.json.return_value = None
466 mock_response.status_code = 400
467 mock_response.text = 'text'
468 mock_request.return_value = mock_response
469 curve_info = self.fb.get_curve(12345)
470 mock_request.assert_called_once_with(
471 method='GET',
472 url='https://my.farm.bot/api/curves/12345',
473 **REQUEST_KWARGS,
474 )
475 self.assertIsNone(curve_info)
477 @patch('requests.request')
478 def test_safe_z(self, mock_request):
479 '''test safe_z function'''
480 mock_response = Mock()
481 mock_response.json.return_value = {'safe_height': 100}
482 mock_response.status_code = 200
483 mock_response.text = 'text'
484 mock_request.return_value = mock_response
485 safe_height = self.fb.safe_z()
486 mock_request.assert_called_once_with(
487 method='GET',
488 url='https://my.farm.bot/api/fbos_config',
489 **REQUEST_KWARGS,
490 )
491 self.assertEqual(safe_height, 100)
493 @patch('requests.request')
494 def test_garden_size(self, mock_request):
495 '''test garden_size function'''
496 mock_response = Mock()
497 mock_response.json.return_value = {
498 'movement_axis_nr_steps_x': 1000,
499 'movement_axis_nr_steps_y': 2000,
500 'movement_axis_nr_steps_z': 40000,
501 'movement_step_per_mm_x': 5,
502 'movement_step_per_mm_y': 5,
503 'movement_step_per_mm_z': 25,
504 }
505 mock_response.status_code = 200
506 mock_response.text = 'text'
507 mock_request.return_value = mock_response
508 garden_size = self.fb.garden_size()
509 mock_request.assert_called_once_with(
510 method='GET',
511 url='https://my.farm.bot/api/firmware_config',
512 **REQUEST_KWARGS,
513 )
514 self.assertEqual(garden_size, {'x': 200, 'y': 400, 'z': 1600})
516 @patch('requests.request')
517 def test_log(self, mock_request):
518 '''test log function'''
519 mock_response = Mock()
520 mock_response.status_code = 200
521 mock_response.text = 'text'
522 mock_response.json.return_value = {'message': 'test message'}
523 mock_request.return_value = mock_response
524 self.fb.log('test message', 'info', ['toast'])
525 mock_request.assert_called_once_with(
526 method='POST',
527 url='https://my.farm.bot/api/logs',
528 **REQUEST_KWARGS_WITH_PAYLOAD,
529 json={
530 'message': 'test message',
531 'type': 'info',
532 'channels': ['toast'],
533 },
534 )
536 @patch('paho.mqtt.client.Client')
537 def test_connect_broker(self, mock_mqtt):
538 '''Test test_connect_broker command'''
539 mock_client = Mock()
540 mock_mqtt.return_value = mock_client
541 self.fb.connect_broker()
542 mock_client.username_pw_set.assert_called_once_with(
543 username='device_0',
544 password='encoded_token_value')
545 mock_client.connect.assert_called_once_with(
546 'mqtt_url',
547 port=1883,
548 keepalive=60)
549 mock_client.loop_start.assert_called()
551 def test_disconnect_broker(self):
552 '''Test disconnect_broker command'''
553 mock_client = Mock()
554 self.fb.broker.client = mock_client
555 self.fb.disconnect_broker()
556 mock_client.loop_stop.assert_called_once()
557 mock_client.disconnect.assert_called_once()
559 @patch('paho.mqtt.client.Client')
560 def test_listen(self, mock_mqtt):
561 '''Test listen command'''
562 mock_client = Mock()
563 mock_mqtt.return_value = mock_client
564 self.fb.listen()
566 class MockMessage:
567 '''Mock message class'''
568 topic = 'topic'
569 payload = '{"message": "test message"}'
570 mock_client.on_message('', '', MockMessage())
571 mock_client.username_pw_set.assert_called_once_with(
572 username='device_0',
573 password='encoded_token_value')
574 mock_client.connect.assert_called_once_with(
575 'mqtt_url',
576 port=1883,
577 keepalive=60)
578 mock_client.subscribe.assert_called_once_with('bot/device_0/#')
579 mock_client.loop_start.assert_called()
580 mock_client.loop_stop.assert_called()
582 @patch('math.inf', 0.1)
583 @patch('paho.mqtt.client.Client')
584 def test_listen_for_status_changes(self, mock_mqtt):
585 '''Test listen_for_status_changes command'''
586 self.maxDiff = None
587 i = 0
589 mock_client = Mock()
590 mock_mqtt.return_value = mock_client
592 class MockMessage:
593 '''Mock message class'''
595 def __init__(self):
596 self.topic = '/status'
597 payload = {
598 'location_data': {
599 'position': {
600 'x': i,
601 'y': i + 10,
602 'z': 100,
603 }}}
604 if i == 2:
605 payload['location_data']['position']['extra'] = {'idx': 2}
606 if i == 3:
607 payload['location_data']['position']['extra'] = {'idx': 3}
608 self.payload = json.dumps(payload)
610 def patched_sleep(_seconds):
611 '''Patched sleep function'''
612 nonlocal i
613 mock_message = MockMessage()
614 mock_client.on_message('', '', mock_message)
615 i += 1
617 with patch('time.sleep', new=patched_sleep):
618 self.fb.listen_for_status_changes(
619 stop_count=5,
620 info_path='location_data.position')
622 self.assertEqual(self.fb.state.last_messages['status'], [
623 {'location_data': {'position': {'x': 0, 'y': 10, 'z': 100}}},
624 {'location_data': {'position': {'x': 1, 'y': 11, 'z': 100}}},
625 {'location_data': {'position': {
626 'extra': {'idx': 2}, 'x': 2, 'y': 12, 'z': 100}}},
627 {'location_data': {'position': {
628 'extra': {'idx': 3}, 'x': 3, 'y': 13, 'z': 100}}},
629 {'location_data': {'position': {'x': 4, 'y': 14, 'z': 100}}}
630 ])
631 self.assertEqual(self.fb.state.last_messages['status_diffs'], [
632 {'x': 0, 'y': 10, 'z': 100},
633 {'x': 1, 'y': 11},
634 {'extra': {'idx': 2}, 'x': 2, 'y': 12},
635 {'extra': {'idx': 3}, 'x': 3, 'y': 13},
636 {'x': 4, 'y': 14},
637 ])
638 self.assertEqual(self.fb.state.last_messages['status_excerpt'], [
639 {'x': 0, 'y': 10, 'z': 100},
640 {'x': 1, 'y': 11, 'z': 100},
641 {'extra': {'idx': 2}, 'x': 2, 'y': 12, 'z': 100},
642 {'extra': {'idx': 3}, 'x': 3, 'y': 13, 'z': 100},
643 {'x': 4, 'y': 14, 'z': 100},
644 ])
646 @patch('paho.mqtt.client.Client')
647 def test_listen_clear_last(self, mock_mqtt):
648 '''Test listen command: clear last message'''
649 mock_client = Mock()
650 mock_mqtt.return_value = mock_client
651 self.fb.state.last_messages = [{'#': "message"}]
652 self.fb.state.test_env = False
653 self.fb.listen()
654 self.assertEqual(len(self.fb.state.last_messages['#']), 0)
656 @patch('paho.mqtt.client.Client')
657 def test_publish_apply_label(self, mock_mqtt):
658 '''Test publish command: set uuid'''
659 mock_client = Mock()
660 mock_mqtt.return_value = mock_client
661 self.fb.state.test_env = False
662 self.fb.publish({'kind': 'sync', 'args': {}})
663 label = self.fb.state.last_published.get('args', {}).get('label')
664 self.assertNotIn(label, ['test', '', None])
666 @patch('requests.request')
667 @patch('paho.mqtt.client.Client')
668 def send_command_test_helper(self, *args, **kwargs):
669 '''Helper for testing command execution'''
670 execute_command = args[0]
671 mock_mqtt = args[1]
672 mock_request = args[2]
673 expected_command = kwargs.get('expected_command')
674 extra_rpc_args = kwargs.get('extra_rpc_args')
675 mock_api_response = kwargs.get('mock_api_response')
676 error = kwargs.get('error')
677 mock_client = Mock()
678 mock_mqtt.return_value = mock_client
679 mock_response = Mock()
680 mock_response.json.return_value = mock_api_response
681 mock_response.status_code = 200
682 mock_response.text = 'text'
683 mock_request.return_value = mock_response
684 self.fb.state.last_messages['from_device'] = [{
685 'kind': 'rpc_error' if error else 'rpc_ok',
686 'args': {'label': 'test'},
687 }]
688 execute_command()
689 if expected_command is None:
690 mock_client.publish.assert_not_called()
691 return
692 expected_payload = {
693 'kind': 'rpc_request',
694 'args': {'label': 'test', **extra_rpc_args},
695 'body': [expected_command],
696 }
697 mock_client.username_pw_set.assert_called_once_with(
698 username='device_0',
699 password='encoded_token_value')
700 mock_client.connect.assert_called_once_with(
701 'mqtt_url',
702 port=1883,
703 keepalive=60)
704 mock_client.loop_start.assert_called()
705 mock_client.publish.assert_called_once_with(
706 'bot/device_0/from_clients',
707 payload=json.dumps(expected_payload))
708 if not error:
709 self.assertNotEqual(
710 self.fb.state.error,
711 'RPC error response received.')
713 def test_message(self):
714 '''Test message command'''
715 def exec_command():
716 self.fb.send_message('test message', 'info')
717 self.send_command_test_helper(
718 exec_command,
719 expected_command={
720 'kind': 'send_message',
721 'args': {'message': 'test message', 'message_type': 'info'},
722 'body': [],
723 },
724 extra_rpc_args={},
725 mock_api_response={})
727 def test_debug(self):
728 '''Test debug command'''
729 def exec_command():
730 self.fb.debug('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': 'debug'},
736 'body': [],
737 },
738 extra_rpc_args={},
739 mock_api_response={})
741 def test_toast(self):
742 '''Test toast command'''
743 def exec_command():
744 self.fb.toast('test message')
745 self.send_command_test_helper(
746 exec_command,
747 expected_command={
748 'kind': 'send_message',
749 'args': {'message': 'test message', 'message_type': 'info'},
750 'body': [{'kind': 'channel', 'args': {'channel_name': 'toast'}}],
751 },
752 extra_rpc_args={},
753 mock_api_response={})
755 def test_invalid_message_type(self):
756 '''Test message_type validation'''
757 def exec_command():
758 with self.assertRaises(ValueError) as cm:
759 self.fb.send_message('test', message_type='nope')
760 msg = 'Invalid message type: `nope` not in '
761 msg += "['assertion', 'busy', 'debug', 'error', 'fun', 'info', 'success', 'warn']"
762 self.assertEqual(cm.exception.args[0], msg)
763 self.send_command_test_helper(
764 exec_command,
765 expected_command=None,
766 extra_rpc_args={},
767 mock_api_response={})
769 def test_invalid_message_channel(self):
770 '''Test message channel validation'''
771 def exec_command():
772 with self.assertRaises(ValueError) as cm:
773 self.fb.send_message('test', channels=['nope'])
774 self.assertEqual(
775 cm.exception.args[0],
776 "Invalid channel: nope not in ['ticker', 'toast', 'email', 'espeak']")
777 self.send_command_test_helper(
778 exec_command,
779 expected_command=None,
780 extra_rpc_args={},
781 mock_api_response={})
783 def test_read_status(self):
784 '''Test read_status command'''
785 def exec_command():
786 self.fb.state.last_messages['status'] = [{
787 'location_data': {'position': {'x': 100}},
788 }]
789 result = self.fb.read_status()
790 self.assertEqual(
791 result,
792 {'location_data': {'position': {'x': 100}}})
793 self.send_command_test_helper(
794 exec_command,
795 expected_command={
796 'kind': 'read_status',
797 'args': {},
798 },
799 extra_rpc_args={},
800 mock_api_response={})
802 def test_read_status_path(self):
803 '''Test read_status command: specific path'''
804 def exec_command():
805 self.fb.state.last_messages['status'] = [{
806 'location_data': {'position': {'x': 100}},
807 }]
808 result = self.fb.read_status('location_data.position.x')
809 self.assertEqual(result, 100)
810 self.send_command_test_helper(
811 exec_command,
812 expected_command={
813 'kind': 'read_status',
814 'args': {},
815 },
816 extra_rpc_args={},
817 mock_api_response={})
819 def test_read_pin(self):
820 '''Test read_pin command'''
821 def exec_command():
822 self.fb.read_pin(13)
823 self.send_command_test_helper(
824 exec_command,
825 expected_command={
826 'kind': 'read_pin',
827 'args': {
828 'pin_number': 13,
829 'label': '---',
830 'pin_mode': 0,
831 },
832 },
833 extra_rpc_args={},
834 mock_api_response={})
836 def test_read_sensor(self):
837 '''Test read_sensor command'''
838 def exec_command():
839 self.fb.read_sensor('Tool Verification')
840 self.send_command_test_helper(
841 exec_command,
842 expected_command={
843 'kind': 'read_pin',
844 'args': {
845 'pin_mode': 0,
846 'label': '---',
847 'pin_number': {
848 'kind': 'named_pin',
849 'args': {'pin_type': 'Sensor', 'pin_id': 123},
850 },
851 },
852 },
853 extra_rpc_args={},
854 mock_api_response=[{'id': 123, 'label': 'Tool Verification', 'mode': 0}])
856 def test_read_sensor_not_found(self):
857 '''Test read_sensor command: sensor not found'''
858 def exec_command():
859 self.fb.read_sensor('Temperature')
860 self.send_command_test_helper(
861 exec_command,
862 expected_command=None,
863 extra_rpc_args={},
864 mock_api_response=[{'label': 'Tool Verification'}])
865 self.assertEqual(
866 self.fb.state.error,
867 "ERROR: 'Temperature' not in sensors: ['Tool Verification'].")
869 def test_assertion(self):
870 '''Test assertion command'''
871 def exec_command():
872 self.fb.assertion('return true', 'abort')
873 self.send_command_test_helper(
874 exec_command,
875 expected_command={
876 'kind': 'assertion',
877 'args': {
878 'assertion_type': 'abort',
879 'lua': 'return true',
880 '_then': {'kind': 'nothing', 'args': {}},
881 }
882 },
883 extra_rpc_args={},
884 mock_api_response={})
886 def test_assertion_with_recovery_sequence(self):
887 '''Test assertion command with recovery sequence'''
888 def exec_command():
889 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
890 self.send_command_test_helper(
891 exec_command,
892 expected_command={
893 'kind': 'assertion',
894 'args': {
895 'assertion_type': 'abort',
896 'lua': 'return true',
897 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
898 }
899 },
900 extra_rpc_args={},
901 mock_api_response=[{'id': 123, 'name': 'Recovery Sequence'}])
903 def test_assertion_recovery_sequence_not_found(self):
904 '''Test assertion command: recovery sequence not found'''
905 def exec_command():
906 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
907 self.send_command_test_helper(
908 exec_command,
909 expected_command=None,
910 extra_rpc_args={},
911 mock_api_response=[])
912 self.assertEqual(
913 self.fb.state.error,
914 "ERROR: 'Recovery Sequence' not in sequences: [].")
916 def test_assertion_invalid_assertion_type(self):
917 '''Test assertion command: invalid assertion type'''
918 def exec_command():
919 with self.assertRaises(ValueError) as cm:
920 self.fb.assertion('return true', 'nope')
921 msg = 'Invalid assertion_type: nope not in '
922 msg += "['abort', 'recover', 'abort_recover', 'continue']"
923 self.assertEqual(cm.exception.args[0], msg)
924 self.send_command_test_helper(
925 exec_command,
926 expected_command=None,
927 extra_rpc_args={},
928 mock_api_response={})
930 def test_wait(self):
931 '''Test wait command'''
932 def exec_command():
933 self.fb.wait(123)
934 self.send_command_test_helper(
935 exec_command,
936 expected_command={
937 'kind': 'wait',
938 'args': {'milliseconds': 123},
939 },
940 extra_rpc_args={},
941 mock_api_response={})
943 def test_unlock(self):
944 '''Test unlock command'''
945 def exec_command():
946 self.fb.unlock()
947 self.send_command_test_helper(
948 exec_command,
949 expected_command={
950 'kind': 'emergency_unlock',
951 'args': {},
952 },
953 extra_rpc_args={'priority': 9000},
954 mock_api_response={})
956 def test_e_stop(self):
957 '''Test e_stop command'''
958 def exec_command():
959 self.fb.e_stop()
960 self.send_command_test_helper(
961 exec_command,
962 expected_command={
963 'kind': 'emergency_lock',
964 'args': {},
965 },
966 extra_rpc_args={'priority': 9000},
967 mock_api_response={})
969 def test_find_home(self):
970 '''Test find_home command'''
971 def exec_command():
972 self.fb.find_home()
973 self.send_command_test_helper(
974 exec_command,
975 expected_command={
976 'kind': 'find_home',
977 'args': {'axis': 'all', 'speed': 100},
978 },
979 extra_rpc_args={},
980 mock_api_response={})
982 def test_find_home_speed_error(self):
983 '''Test find_home command: speed error'''
984 def exec_command():
985 self.fb.find_home('all', 0)
986 self.send_command_test_helper(
987 exec_command,
988 expected_command=None,
989 extra_rpc_args={},
990 mock_api_response={})
991 self.assertEqual(
992 self.fb.state.error,
993 'ERROR: Speed constrained to 1-100.')
995 def test_find_home_invalid_axis(self):
996 '''Test find_home command: invalid axis'''
997 def exec_command():
998 with self.assertRaises(ValueError) as cm:
999 self.fb.find_home('nope')
1000 self.assertEqual(
1001 cm.exception.args[0],
1002 "Invalid axis: nope not in ['x', 'y', 'z', 'all']")
1003 self.send_command_test_helper(
1004 exec_command,
1005 expected_command=None,
1006 extra_rpc_args={},
1007 mock_api_response={})
1009 def test_set_home(self):
1010 '''Test set_home command'''
1011 def exec_command():
1012 self.fb.set_home()
1013 self.send_command_test_helper(
1014 exec_command,
1015 expected_command={
1016 'kind': 'zero',
1017 'args': {'axis': 'all'},
1018 },
1019 extra_rpc_args={},
1020 mock_api_response={})
1022 def test_toggle_peripheral(self):
1023 '''Test toggle_peripheral command'''
1024 def exec_command():
1025 self.fb.toggle_peripheral('New Peripheral')
1026 self.send_command_test_helper(
1027 exec_command,
1028 expected_command={
1029 'kind': 'toggle_pin',
1030 'args': {
1031 'pin_number': {
1032 'kind': 'named_pin',
1033 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1034 },
1035 },
1036 },
1037 extra_rpc_args={},
1038 mock_api_response=[{'label': 'New Peripheral', 'id': 123}])
1040 def test_toggle_peripheral_not_found(self):
1041 '''Test toggle_peripheral command: peripheral not found'''
1042 def exec_command():
1043 self.fb.toggle_peripheral('New Peripheral')
1044 self.send_command_test_helper(
1045 exec_command,
1046 expected_command=None,
1047 extra_rpc_args={},
1048 mock_api_response=[])
1049 self.assertEqual(
1050 self.fb.state.error,
1051 'ERROR: \'New Peripheral\' not in peripherals: [].')
1053 @patch('requests.request')
1054 @patch('paho.mqtt.client.Client')
1055 def test_toggle_peripheral_use_cache(self, mock_mqtt, mock_request):
1056 '''Test toggle_peripheral command: use cache'''
1057 mock_client = Mock()
1058 mock_mqtt.return_value = mock_client
1059 mock_response = Mock()
1060 mock_response.json.return_value = [
1061 {'label': 'Peripheral 4', 'id': 123},
1062 {'label': 'Peripheral 5', 'id': 456}
1063 ]
1064 mock_response.status_code = 200
1065 mock_response.text = 'text'
1066 mock_request.return_value = mock_response
1067 # save cache
1068 self.fb.toggle_peripheral('Peripheral 4')
1069 mock_request.assert_called()
1070 mock_client.publish.assert_called()
1071 mock_request.reset_mock()
1072 mock_client.reset_mock()
1073 # use cache
1074 self.fb.toggle_peripheral('Peripheral 5')
1075 mock_request.assert_not_called()
1076 mock_client.publish.assert_called()
1077 mock_request.reset_mock()
1078 mock_client.reset_mock()
1079 # clear cache
1080 self.fb.toggle_peripheral('Peripheral 6')
1081 mock_request.assert_not_called()
1082 mock_client.publish.assert_not_called()
1083 mock_request.reset_mock()
1084 mock_client.reset_mock()
1085 # save cache
1086 self.fb.toggle_peripheral('Peripheral 4')
1087 mock_request.assert_called()
1088 mock_client.publish.assert_called()
1089 mock_request.reset_mock()
1090 mock_client.reset_mock()
1092 def test_on_digital(self):
1093 '''Test on command: digital'''
1094 def exec_command():
1095 self.fb.on(13)
1096 self.send_command_test_helper(
1097 exec_command,
1098 expected_command={
1099 'kind': 'write_pin',
1100 'args': {
1101 'pin_value': 1,
1102 'pin_mode': 0,
1103 'pin_number': 13,
1104 },
1105 },
1106 extra_rpc_args={},
1107 mock_api_response={})
1109 def test_off(self):
1110 '''Test off command'''
1111 def exec_command():
1112 self.fb.off(13)
1113 self.send_command_test_helper(
1114 exec_command,
1115 expected_command={
1116 'kind': 'write_pin',
1117 'args': {
1118 'pin_value': 0,
1119 'pin_mode': 0,
1120 'pin_number': 13,
1121 },
1122 },
1123 extra_rpc_args={},
1124 mock_api_response={})
1126 def test_move(self):
1127 '''Test move command'''
1128 def exec_command():
1129 self.fb.move()
1130 self.send_command_test_helper(
1131 exec_command,
1132 expected_command={
1133 'kind': 'move',
1134 'args': {},
1135 'body': [],
1136 },
1137 extra_rpc_args={},
1138 mock_api_response={})
1140 def test_move_extras(self):
1141 '''Test move command with extras'''
1142 def exec_command():
1143 self.fb.move(1, 2, 3, safe_z=True, speed=50)
1144 self.send_command_test_helper(
1145 exec_command,
1146 expected_command={
1147 'kind': 'move',
1148 'args': {},
1149 'body': [
1150 {'kind': 'axis_overwrite', 'args': {
1151 'axis': 'x',
1152 'axis_operand': {'kind': 'numeric', 'args': {'number': 1}}}},
1153 {'kind': 'axis_overwrite', 'args': {
1154 'axis': 'y',
1155 'axis_operand': {'kind': 'numeric', 'args': {'number': 2}}}},
1156 {'kind': 'axis_overwrite', 'args': {
1157 'axis': 'z',
1158 'axis_operand': {'kind': 'numeric', 'args': {'number': 3}}}},
1159 {'kind': 'speed_overwrite', 'args': {
1160 'axis': 'x',
1161 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}},
1162 {'kind': 'speed_overwrite', 'args': {
1163 'axis': 'y',
1164 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}},
1165 {'kind': 'speed_overwrite', 'args': {
1166 'axis': 'z',
1167 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}},
1168 {'kind': 'safe_z', 'args': {}},
1169 ],
1170 },
1171 extra_rpc_args={},
1172 mock_api_response={})
1174 def test_reboot(self):
1175 '''Test reboot command'''
1176 def exec_command():
1177 self.fb.reboot()
1178 self.send_command_test_helper(
1179 exec_command,
1180 expected_command={
1181 'kind': 'reboot',
1182 'args': {'package': 'farmbot_os'},
1183 },
1184 extra_rpc_args={},
1185 mock_api_response={})
1187 def test_shutdown(self):
1188 '''Test shutdown command'''
1189 def exec_command():
1190 self.fb.shutdown()
1191 self.send_command_test_helper(
1192 exec_command,
1193 expected_command={
1194 'kind': 'power_off',
1195 'args': {},
1196 },
1197 extra_rpc_args={},
1198 mock_api_response={})
1200 def test_find_axis_length(self):
1201 '''Test find_axis_length command'''
1202 def exec_command():
1203 self.fb.find_axis_length()
1204 self.send_command_test_helper(
1205 exec_command,
1206 expected_command={
1207 'kind': 'calibrate',
1208 'args': {'axis': 'all'},
1209 },
1210 extra_rpc_args={},
1211 mock_api_response={})
1213 def test_write_pin(self):
1214 '''Test write_pin command'''
1215 def exec_command():
1216 self.fb.write_pin(13, 1, 'analog')
1217 self.send_command_test_helper(
1218 exec_command,
1219 expected_command={
1220 'kind': 'write_pin',
1221 'args': {
1222 'pin_number': 13,
1223 'pin_value': 1,
1224 'pin_mode': 1,
1225 },
1226 },
1227 extra_rpc_args={},
1228 mock_api_response={})
1230 def test_write_pin_invalid_mode(self):
1231 '''Test write_pin command: invalid mode'''
1232 def exec_command():
1233 with self.assertRaises(ValueError) as cm:
1234 self.fb.write_pin(13, 1, 1)
1235 self.assertEqual(
1236 cm.exception.args[0],
1237 "Invalid mode: 1 not in ['digital', 'analog']")
1238 self.send_command_test_helper(
1239 exec_command,
1240 expected_command=None,
1241 extra_rpc_args={},
1242 mock_api_response={})
1244 def test_control_peripheral(self):
1245 '''Test control_peripheral command'''
1246 def exec_command():
1247 self.fb.control_peripheral('New Peripheral', 1)
1248 self.send_command_test_helper(
1249 exec_command,
1250 expected_command={
1251 'kind': 'write_pin',
1252 'args': {
1253 'pin_value': 1,
1254 'pin_mode': 0,
1255 'pin_number': {
1256 'kind': 'named_pin',
1257 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1258 },
1259 },
1260 },
1261 extra_rpc_args={},
1262 mock_api_response=[{'label': 'New Peripheral', 'mode': 0, 'id': 123}])
1264 def test_control_peripheral_analog(self):
1265 '''Test control_peripheral command: analog'''
1266 def exec_command():
1267 self.fb.control_peripheral('New Peripheral', 1, 'analog')
1268 self.send_command_test_helper(
1269 exec_command,
1270 expected_command={
1271 'kind': 'write_pin',
1272 'args': {
1273 'pin_value': 1,
1274 'pin_mode': 1,
1275 'pin_number': {
1276 'kind': 'named_pin',
1277 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1278 },
1279 },
1280 },
1281 extra_rpc_args={},
1282 mock_api_response=[{'label': 'New Peripheral', 'mode': 0, 'id': 123}])
1284 def test_control_peripheral_not_found(self):
1285 '''Test control_peripheral command: peripheral not found'''
1286 def exec_command():
1287 self.fb.control_peripheral('New Peripheral', 1)
1288 self.send_command_test_helper(
1289 exec_command,
1290 expected_command=None,
1291 extra_rpc_args={},
1292 mock_api_response=[{'label': 'Pump'}, {'label': 'Lights'}])
1293 self.assertEqual(
1294 self.fb.state.error,
1295 "ERROR: 'New Peripheral' not in peripherals: ['Pump', 'Lights'].")
1297 def test_measure_soil_height(self):
1298 '''Test measure_soil_height command'''
1299 def exec_command():
1300 self.fb.measure_soil_height()
1301 self.send_command_test_helper(
1302 exec_command,
1303 expected_command={
1304 'kind': 'execute_script',
1305 'args': {'label': 'Measure Soil Height'},
1306 },
1307 extra_rpc_args={},
1308 mock_api_response={})
1310 def test_detect_weeds(self):
1311 '''Test detect_weeds command'''
1312 def exec_command():
1313 self.fb.detect_weeds()
1314 self.send_command_test_helper(
1315 exec_command,
1316 expected_command={
1317 'kind': 'execute_script',
1318 'args': {'label': 'plant-detection'},
1319 },
1320 extra_rpc_args={},
1321 mock_api_response={})
1323 def test_calibrate_camera(self):
1324 '''Test calibrate_camera command'''
1325 def exec_command():
1326 self.fb.calibrate_camera()
1327 self.send_command_test_helper(
1328 exec_command,
1329 expected_command={
1330 'kind': 'execute_script',
1331 'args': {'label': 'camera-calibration'},
1332 },
1333 extra_rpc_args={},
1334 mock_api_response={})
1336 def test_sequence(self):
1337 '''Test sequence command'''
1338 def exec_command():
1339 self.fb.sequence('My Sequence')
1340 self.send_command_test_helper(
1341 exec_command,
1342 expected_command={
1343 'kind': 'execute',
1344 'args': {'sequence_id': 123},
1345 },
1346 extra_rpc_args={},
1347 mock_api_response=[{'name': 'My Sequence', 'id': 123}])
1349 def test_sequence_not_found(self):
1350 '''Test sequence command: sequence not found'''
1351 def exec_command():
1352 self.fb.sequence('My Sequence')
1353 self.send_command_test_helper(
1354 exec_command,
1355 expected_command=None,
1356 extra_rpc_args={},
1357 mock_api_response=[{'name': 'Water'}])
1358 self.assertEqual(
1359 self.fb.state.error,
1360 "ERROR: 'My Sequence' not in sequences: ['Water'].")
1362 def test_take_photo(self):
1363 '''Test take_photo command'''
1364 def exec_command():
1365 self.fb.take_photo()
1366 self.send_command_test_helper(
1367 exec_command,
1368 expected_command={
1369 'kind': 'take_photo',
1370 'args': {},
1371 },
1372 extra_rpc_args={},
1373 mock_api_response={})
1375 def test_control_servo(self):
1376 '''Test control_servo command'''
1377 def exec_command():
1378 self.fb.control_servo(4, 100)
1379 self.send_command_test_helper(
1380 exec_command,
1381 expected_command={
1382 'kind': 'set_servo_angle',
1383 'args': {
1384 'pin_number': 4,
1385 'pin_value': 100,
1386 },
1387 },
1388 extra_rpc_args={},
1389 mock_api_response={'mode': 0})
1391 def test_control_servo_error(self):
1392 '''Test control_servo command: error'''
1393 def exec_command():
1394 self.fb.control_servo(4, 200)
1395 self.send_command_test_helper(
1396 exec_command,
1397 expected_command=None,
1398 extra_rpc_args={},
1399 mock_api_response={'mode': 0})
1401 def test_get_xyz(self):
1402 '''Test get_xyz command'''
1403 def exec_command():
1404 self.fb.state.last_messages['status'] = [{
1405 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1406 }]
1407 position = self.fb.get_xyz()
1408 self.assertEqual(position, {'x': 1, 'y': 2, 'z': 3})
1409 self.send_command_test_helper(
1410 exec_command,
1411 expected_command={
1412 'kind': 'read_status',
1413 'args': {},
1414 },
1415 extra_rpc_args={},
1416 mock_api_response={})
1418 def test_get_xyz_no_status(self):
1419 '''Test get_xyz command: no status'''
1420 def exec_command():
1421 self.fb.state.last_messages['status'] = []
1422 position = self.fb.get_xyz()
1423 self.assertIsNone(position)
1424 self.send_command_test_helper(
1425 exec_command,
1426 expected_command={
1427 'kind': 'read_status',
1428 'args': {},
1429 },
1430 extra_rpc_args={},
1431 mock_api_response={})
1433 def test_check_position(self):
1434 '''Test check_position command: at position'''
1435 def exec_command():
1436 self.fb.state.last_messages['status'] = [{
1437 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1438 }]
1439 at_position = self.fb.check_position({'x': 1, 'y': 2, 'z': 3}, 0)
1440 self.assertTrue(at_position)
1441 self.send_command_test_helper(
1442 exec_command,
1443 expected_command={
1444 'kind': 'read_status',
1445 'args': {},
1446 },
1447 extra_rpc_args={},
1448 mock_api_response={})
1450 def test_check_position_false(self):
1451 '''Test check_position command: not at position'''
1452 def exec_command():
1453 self.fb.state.last_messages['status'] = [{
1454 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1455 }]
1456 at_position = self.fb.check_position({'x': 0, 'y': 0, 'z': 0}, 2)
1457 self.assertFalse(at_position)
1458 self.send_command_test_helper(
1459 exec_command,
1460 expected_command={
1461 'kind': 'read_status',
1462 'args': {},
1463 },
1464 extra_rpc_args={},
1465 mock_api_response={})
1467 def test_check_position_no_status(self):
1468 '''Test check_position command: no status'''
1469 def exec_command():
1470 self.fb.state.last_messages['status'] = []
1471 at_position = self.fb.check_position({'x': 0, 'y': 0, 'z': 0}, 2)
1472 self.assertFalse(at_position)
1473 self.send_command_test_helper(
1474 exec_command,
1475 expected_command={
1476 'kind': 'read_status',
1477 'args': {},
1478 },
1479 extra_rpc_args={},
1480 mock_api_response={})
1482 def test_mount_tool(self):
1483 '''Test mount_tool command'''
1484 def exec_command():
1485 self.fb.mount_tool('Weeder')
1486 self.send_command_test_helper(
1487 exec_command,
1488 expected_command={
1489 'kind': 'lua',
1490 'args': {'lua': 'mount_tool("Weeder")'},
1491 },
1492 extra_rpc_args={},
1493 mock_api_response={})
1495 def test_dismount_tool(self):
1496 '''Test dismount_tool command'''
1497 def exec_command():
1498 self.fb.dismount_tool()
1499 self.send_command_test_helper(
1500 exec_command,
1501 expected_command={
1502 'kind': 'lua',
1503 'args': {'lua': 'dismount_tool()'},
1504 },
1505 extra_rpc_args={},
1506 mock_api_response={})
1508 def test_water(self):
1509 '''Test water command'''
1510 def exec_command():
1511 self.fb.water(123)
1512 self.send_command_test_helper(
1513 exec_command,
1514 expected_command={
1515 'kind': 'lua',
1516 'args': {'lua': '''plant = api({
1517 method = "GET",
1518 url = "/api/points/123"
1519 })
1520 water(plant)'''},
1521 },
1522 extra_rpc_args={},
1523 mock_api_response={})
1525 def test_dispense(self):
1526 '''Test dispense command'''
1527 def exec_command():
1528 self.fb.dispense(100)
1529 self.send_command_test_helper(
1530 exec_command,
1531 expected_command={
1532 'kind': 'lua',
1533 'args': {
1534 'lua': 'dispense(100)',
1535 },
1536 },
1537 extra_rpc_args={},
1538 mock_api_response={})
1540 def test_dispense_all_args(self):
1541 '''Test dispense command with all args'''
1542 def exec_command():
1543 self.fb.dispense(100, 'Nutrient Sprayer', 4)
1544 self.send_command_test_helper(
1545 exec_command,
1546 expected_command={
1547 'kind': 'lua',
1548 'args': {
1549 'lua': 'dispense(100, {tool_name = "Nutrient Sprayer", pin = 4})',
1550 },
1551 },
1552 extra_rpc_args={},
1553 mock_api_response={})
1555 def test_dispense_only_pin(self):
1556 '''Test dispense command'''
1557 def exec_command():
1558 self.fb.dispense(100, pin=4)
1559 self.send_command_test_helper(
1560 exec_command,
1561 expected_command={
1562 'kind': 'lua',
1563 'args': {
1564 'lua': 'dispense(100, {pin = 4})',
1565 },
1566 },
1567 extra_rpc_args={},
1568 mock_api_response={})
1570 def test_dispense_only_tool_name(self):
1571 '''Test dispense command'''
1572 def exec_command():
1573 self.fb.dispense(100, "Nutrient Sprayer")
1574 self.send_command_test_helper(
1575 exec_command,
1576 expected_command={
1577 'kind': 'lua',
1578 'args': {
1579 'lua': 'dispense(100, {tool_name = "Nutrient Sprayer"})',
1580 },
1581 },
1582 extra_rpc_args={},
1583 mock_api_response={})
1585 @patch('requests.request')
1586 def helper_get_seed_tray_cell(self, *args, **kwargs):
1587 '''Test helper for get_seed_tray_cell command'''
1588 mock_request = args[0]
1589 tray_data = kwargs['tray_data']
1590 cell = kwargs['cell']
1591 expected_xyz = kwargs['expected_xyz']
1592 self.fb.clear_cache()
1593 mock_response = Mock()
1594 mock_api_response = [
1595 {
1596 'id': 123,
1597 'name': 'Seed Tray',
1598 'pointer_type': '', # not an actual data field
1599 },
1600 {
1601 'pointer_type': 'ToolSlot',
1602 'pullout_direction': 1,
1603 'x': 0,
1604 'y': 0,
1605 'z': 0,
1606 'tool_id': 123,
1607 'name': '', # not an actual data field
1608 **tray_data,
1609 },
1610 ]
1611 mock_response.json.return_value = mock_api_response
1612 mock_response.status_code = 200
1613 mock_response.text = 'text'
1614 mock_request.return_value = mock_response
1615 cell = self.fb.get_seed_tray_cell('Seed Tray', cell)
1616 mock_request.assert_has_calls([
1617 call(
1618 method='GET',
1619 url='https://my.farm.bot/api/tools',
1620 **REQUEST_KWARGS,
1621 ),
1622 call().json(),
1623 call(
1624 method='GET',
1625 url='https://my.farm.bot/api/points',
1626 **REQUEST_KWARGS,
1627 ),
1628 call().json(),
1629 ])
1630 self.assertEqual(cell, expected_xyz, kwargs)
1632 def test_get_seed_tray_cell(self):
1633 '''Test get_seed_tray_cell'''
1634 test_cases = [
1635 {
1636 'tray_data': {'pullout_direction': 1},
1637 'cell': 'a1',
1638 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1639 },
1640 {
1641 'tray_data': {'pullout_direction': 1},
1642 'cell': 'b2',
1643 'expected_xyz': {'x': -11.25, 'y': -6.25, 'z': 0},
1644 },
1645 {
1646 'tray_data': {'pullout_direction': 1},
1647 'cell': 'd4',
1648 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1649 },
1650 {
1651 'tray_data': {'pullout_direction': 2},
1652 'cell': 'a1',
1653 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1654 },
1655 {
1656 'tray_data': {'pullout_direction': 2},
1657 'cell': 'b2',
1658 'expected_xyz': {'x': -23.75, 'y': 6.25, 'z': 0},
1659 },
1660 {
1661 'tray_data': {'pullout_direction': 2},
1662 'cell': 'd4',
1663 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1664 },
1665 {
1666 'tray_data': {'pullout_direction': 2, 'x': 100, 'y': 200, 'z': -100},
1667 'cell': 'd4',
1668 'expected_xyz': {'x': 101.25, 'y': 181.25, 'z': -100},
1669 },
1670 ]
1671 for test_case in test_cases:
1672 self.helper_get_seed_tray_cell(**test_case)
1674 @patch('requests.request')
1675 def helper_get_seed_tray_cell_error(self, *args, **kwargs):
1676 '''Test helper for get_seed_tray_cell command errors'''
1677 mock_request = args[0]
1678 tray_data = kwargs['tray_data']
1679 cell = kwargs['cell']
1680 error = kwargs['error']
1681 mock_response = Mock()
1682 mock_api_response = [
1683 {
1684 'id': 123,
1685 'name': 'Seed Tray',
1686 'pointer_type': '', # not an actual data field
1687 },
1688 {
1689 'pointer_type': 'ToolSlot',
1690 'pullout_direction': 1,
1691 'x': 0,
1692 'y': 0,
1693 'z': 0,
1694 'tool_id': 123,
1695 'name': '', # not an actual data field
1696 **tray_data,
1697 },
1698 ]
1699 mock_response.json.return_value = mock_api_response
1700 mock_response.status_code = 200
1701 mock_response.text = 'text'
1702 mock_request.return_value = mock_response
1703 with self.assertRaises(ValueError) as cm:
1704 self.fb.get_seed_tray_cell('Seed Tray', cell)
1705 self.assertEqual(cm.exception.args[0], error)
1706 mock_request.assert_has_calls([
1707 call(
1708 method='GET',
1709 url='https://my.farm.bot/api/tools',
1710 **REQUEST_KWARGS,
1711 ),
1712 call().json(),
1713 call(
1714 method='GET',
1715 url='https://my.farm.bot/api/points',
1716 **REQUEST_KWARGS,
1717 ),
1718 call().json(),
1719 ])
1721 def test_get_seed_tray_cell_invalid_cell_name(self):
1722 '''Test get_seed_tray_cell: invalid cell name'''
1723 self.helper_get_seed_tray_cell_error(
1724 tray_data={},
1725 cell='e4',
1726 error='Seed Tray Cell must be one of **A1** through **D4**',
1727 )
1729 def test_get_seed_tray_cell_invalid_pullout_direction(self):
1730 '''Test get_seed_tray_cell: invalid pullout direction'''
1731 self.helper_get_seed_tray_cell_error(
1732 tray_data={'pullout_direction': 0},
1733 cell='d4',
1734 error='Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`',
1735 )
1737 @patch('requests.request')
1738 def test_get_seed_tray_cell_no_tray(self, mock_request):
1739 '''Test get_seed_tray_cell: no seed tray'''
1740 mock_response = Mock()
1741 mock_api_response = []
1742 mock_response.json.return_value = mock_api_response
1743 mock_response.status_code = 200
1744 mock_response.text = 'text'
1745 mock_request.return_value = mock_response
1746 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1747 mock_request.assert_has_calls([
1748 call(
1749 method='GET',
1750 url='https://my.farm.bot/api/tools',
1751 **REQUEST_KWARGS,
1752 ),
1753 call().json(),
1754 ])
1755 self.assertIsNone(result)
1757 @patch('requests.request')
1758 def test_get_seed_tray_cell_not_mounted(self, mock_request):
1759 '''Test get_seed_tray_cell: seed tray not mounted'''
1760 mock_response = Mock()
1761 mock_api_response = [{
1762 'id': 123,
1763 'name': 'Seed Tray',
1764 'pointer_type': '', # not an actual data field,
1765 }]
1766 mock_response.json.return_value = mock_api_response
1767 mock_response.status_code = 200
1768 mock_response.text = 'text'
1769 mock_request.return_value = mock_response
1770 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1771 mock_request.assert_has_calls([
1772 call(
1773 method='GET',
1774 url='https://my.farm.bot/api/tools',
1775 **REQUEST_KWARGS,
1776 ),
1777 call().json(),
1778 ])
1779 self.assertIsNone(result)
1781 def test_get_job_one(self):
1782 '''Test get_job command: get one job'''
1783 def exec_command():
1784 self.fb.state.last_messages['status'] = [{
1785 'jobs': {
1786 'job name': {'status': 'working'},
1787 },
1788 }]
1789 job = self.fb.get_job('job name')
1790 self.assertEqual(job, {'status': 'working'})
1791 self.send_command_test_helper(
1792 exec_command,
1793 expected_command={
1794 'kind': 'read_status',
1795 'args': {},
1796 },
1797 extra_rpc_args={},
1798 mock_api_response={})
1800 def test_get_job_all(self):
1801 '''Test get_job command: get all jobs'''
1802 def exec_command():
1803 self.fb.state.last_messages['status'] = [{
1804 'jobs': {
1805 'job name': {'status': 'working'},
1806 },
1807 }]
1808 jobs = self.fb.get_job()
1809 self.assertEqual(jobs, {'job name': {'status': 'working'}})
1810 self.send_command_test_helper(
1811 exec_command,
1812 expected_command={
1813 'kind': 'read_status',
1814 'args': {},
1815 },
1816 extra_rpc_args={},
1817 mock_api_response={})
1819 def test_get_job_no_status(self):
1820 '''Test get_job command: no status'''
1821 def exec_command():
1822 self.fb.state.last_messages['status'] = []
1823 job = self.fb.get_job('job name')
1824 self.assertIsNone(job)
1825 self.send_command_test_helper(
1826 exec_command,
1827 expected_command={
1828 'kind': 'read_status',
1829 'args': {},
1830 },
1831 extra_rpc_args={},
1832 mock_api_response={})
1834 def test_set_job(self):
1835 '''Test set_job command'''
1836 def exec_command():
1837 self.fb.set_job('job name', 'working', 50)
1838 self.send_command_test_helper(
1839 exec_command,
1840 expected_command={
1841 'kind': 'lua',
1842 'args': {'lua': '''local job_name = "job name"
1843 set_job(job_name)
1845 -- Update the job's status and percent:
1846 set_job(job_name, {
1847 status = "working",
1848 percent = 50
1849 })'''},
1850 },
1851 extra_rpc_args={},
1852 mock_api_response={})
1854 def test_complete_job(self):
1855 '''Test complete_job command'''
1856 def exec_command():
1857 self.fb.complete_job('job name')
1858 self.send_command_test_helper(
1859 exec_command,
1860 expected_command={
1861 'kind': 'lua',
1862 'args': {'lua': 'complete_job("job name")'},
1863 },
1864 extra_rpc_args={},
1865 mock_api_response={})
1867 def test_lua(self):
1868 '''Test lua command'''
1869 def exec_command():
1870 self.fb.lua('return true')
1871 self.send_command_test_helper(
1872 exec_command,
1873 expected_command={
1874 'kind': 'lua',
1875 'args': {'lua': 'return true'},
1876 },
1877 extra_rpc_args={},
1878 mock_api_response={})
1880 def test_if_statement(self):
1881 '''Test if_statement command'''
1882 def exec_command():
1883 self.fb.if_statement('pin10', 'is', 0)
1884 self.send_command_test_helper(
1885 exec_command,
1886 expected_command={
1887 'kind': '_if',
1888 'args': {
1889 'lhs': 'pin10',
1890 'op': 'is',
1891 'rhs': 0,
1892 '_then': {'kind': 'nothing', 'args': {}},
1893 '_else': {'kind': 'nothing', 'args': {}},
1894 }
1895 },
1896 extra_rpc_args={},
1897 mock_api_response=[])
1899 def test_if_statement_with_named_pin(self):
1900 '''Test if_statement command with named pin'''
1901 def exec_command():
1902 self.fb.if_statement(
1903 'Lights', 'is', 0,
1904 named_pin_type='Peripheral')
1905 self.send_command_test_helper(
1906 exec_command,
1907 expected_command={
1908 'kind': '_if',
1909 'args': {
1910 'lhs': {
1911 'kind': 'named_pin',
1912 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1913 },
1914 'op': 'is',
1915 'rhs': 0,
1916 '_then': {'kind': 'nothing', 'args': {}},
1917 '_else': {'kind': 'nothing', 'args': {}},
1918 }
1919 },
1920 extra_rpc_args={},
1921 mock_api_response=[{'id': 123, 'label': 'Lights', 'mode': 0}])
1923 def test_if_statement_with_named_pin_not_found(self):
1924 '''Test if_statement command: named pin not found'''
1925 def exec_command():
1926 self.fb.if_statement(
1927 'Lights', 'is', 0,
1928 named_pin_type='Peripheral')
1929 self.send_command_test_helper(
1930 exec_command,
1931 expected_command=None,
1932 extra_rpc_args={},
1933 mock_api_response=[{'label': 'Pump'}])
1934 self.assertEqual(
1935 self.fb.state.error,
1936 "ERROR: 'Lights' not in peripherals: ['Pump'].")
1938 def test_if_statement_with_sequences(self):
1939 '''Test if_statement command with sequences'''
1940 def exec_command():
1941 self.fb.if_statement(
1942 'pin10', '<', 0,
1943 'Watering Sequence',
1944 'Drying Sequence')
1945 self.send_command_test_helper(
1946 exec_command,
1947 expected_command={
1948 'kind': '_if',
1949 'args': {
1950 'lhs': 'pin10',
1951 'op': '<',
1952 'rhs': 0,
1953 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
1954 '_else': {'kind': 'execute', 'args': {'sequence_id': 456}},
1955 }
1956 },
1957 extra_rpc_args={},
1958 mock_api_response=[
1959 {'id': 123, 'name': 'Watering Sequence'},
1960 {'id': 456, 'name': 'Drying Sequence'},
1961 ])
1963 def test_if_statement_with_sequence_not_found(self):
1964 '''Test if_statement command: sequence not found'''
1965 def exec_command():
1966 self.fb.if_statement(
1967 'pin10', '<', 0,
1968 'Watering Sequence',
1969 'Drying Sequence')
1970 self.send_command_test_helper(
1971 exec_command,
1972 expected_command=None,
1973 extra_rpc_args={},
1974 mock_api_response=[])
1975 self.assertEqual(
1976 self.fb.state.error,
1977 "ERROR: 'Watering Sequence' not in sequences: [].")
1979 def test_if_statement_invalid_operator(self):
1980 '''Test if_statement command: invalid operator'''
1981 def exec_command():
1982 with self.assertRaises(ValueError) as cm:
1983 self.fb.if_statement('pin10', 'nope', 0)
1984 self.assertEqual(
1985 cm.exception.args[0],
1986 "Invalid operator: nope not in ['<', '>', 'is', 'not', 'is_undefined']")
1987 self.send_command_test_helper(
1988 exec_command,
1989 expected_command=None,
1990 extra_rpc_args={},
1991 mock_api_response=[])
1993 def test_if_statement_invalid_variable(self):
1994 '''Test if_statement command: invalid variable'''
1995 variables = ["x", "y", "z", *[f"pin{str(i)}" for i in range(70)]]
1997 def exec_command():
1998 with self.assertRaises(ValueError) as cm:
1999 self.fb.if_statement('nope', '<', 0)
2000 self.assertEqual(
2001 cm.exception.args[0],
2002 f"Invalid variable: nope not in {variables}")
2003 self.send_command_test_helper(
2004 exec_command,
2005 expected_command=None,
2006 extra_rpc_args={},
2007 mock_api_response=[])
2009 def test_if_statement_invalid_named_pin_type(self):
2010 '''Test if_statement command: invalid named pin type'''
2011 def exec_command():
2012 with self.assertRaises(ValueError) as cm:
2013 self.fb.if_statement('pin10', '<', 0, named_pin_type='nope')
2014 self.assertEqual(
2015 cm.exception.args[0],
2016 "Invalid named_pin_type: nope not in ['Peripheral', 'Sensor']")
2017 self.send_command_test_helper(
2018 exec_command,
2019 expected_command=None,
2020 extra_rpc_args={},
2021 mock_api_response=[])
2023 def test_rpc_error(self):
2024 '''Test rpc error handling'''
2025 def exec_command():
2026 self.fb.wait(100)
2027 self.assertEqual(
2028 self.fb.state.error,
2029 'RPC error response received.')
2030 self.send_command_test_helper(
2031 exec_command,
2032 error=True,
2033 expected_command={
2034 'kind': 'wait',
2035 'args': {'milliseconds': 100}},
2036 extra_rpc_args={},
2037 mock_api_response=[])
2039 def test_rpc_response_timeout(self):
2040 '''Test rpc response timeout handling'''
2041 def exec_command():
2042 self.fb.state.last_messages['from_device'] = [
2043 {'kind': 'rpc_ok', 'args': {'label': 'wrong label'}},
2044 ]
2045 self.fb.wait(100)
2046 self.assertEqual(
2047 self.fb.state.error,
2048 'Timed out waiting for RPC response.')
2049 self.send_command_test_helper(
2050 exec_command,
2051 expected_command={
2052 'kind': 'wait',
2053 'args': {'milliseconds': 100}},
2054 extra_rpc_args={},
2055 mock_api_response=[])
2057 def test_set_verbosity(self):
2058 '''Test set_verbosity.'''
2059 self.assertEqual(self.fb.state.verbosity, 0)
2060 self.fb.set_verbosity(1)
2061 self.assertEqual(self.fb.state.verbosity, 1)
2063 def test_set_timeout(self):
2064 '''Test set_timeout.'''
2065 self.assertEqual(self.fb.state.timeout['listen'], 0)
2066 self.fb.set_timeout(15)
2067 self.assertEqual(self.fb.state.timeout['listen'], 15)
2069 @staticmethod
2070 def helper_get_print_strings(mock_print):
2071 '''Test helper to get print call strings.'''
2072 return [string[1][0] for string in mock_print.mock_calls if len(string[1]) > 0]
2074 @patch('builtins.print')
2075 def test_print_status(self, mock_print):
2076 '''Test print_status.'''
2077 self.fb.set_verbosity(0)
2078 self.fb.state.print_status(description="testing")
2079 mock_print.assert_not_called()
2080 self.fb.set_verbosity(1)
2081 self.fb.state.print_status(description="testing")
2082 call_strings = self.helper_get_print_strings(mock_print)
2083 self.assertIn('testing', call_strings)
2084 mock_print.reset_mock()
2085 self.fb.set_verbosity(2)
2086 self.fb.state.print_status(endpoint_json=["testing"])
2087 call_strings = self.helper_get_print_strings(mock_print)
2088 call_strings = [s.split('(')[0].strip('`') for s in call_strings]
2089 self.assertIn('[\n "testing"\n]', call_strings)
2090 self.assertIn('test_print_status', call_strings)