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