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