Coverage for tests/tests_main.py: 100%
720 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-08-31 14:00 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-08-31 14:00 -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}
24class TestFarmbot(unittest.TestCase):
25 '''Farmbot tests'''
27 def setUp(self):
28 '''Set up method called before each test case'''
29 self.fb = Farmbot()
30 self.fb.set_token(MOCK_TOKEN)
31 self.fb.set_verbosity(0)
32 self.fb.state.test_env = True
33 self.fb.state.broker_listen_duration = 0
35 @patch('requests.post')
36 def test_get_token_default_server(self, mock_post):
37 '''POSITIVE TEST: function called with email, password, and default server'''
38 mock_response = Mock()
39 expected_token = {'token': 'abc123'}
40 mock_response.json.return_value = expected_token
41 mock_response.status_code = 200
42 mock_response.text = 'text'
43 mock_post.return_value = mock_response
44 self.fb.set_token(None)
45 # Call with default server
46 self.fb.get_token('test_email@gmail.com', 'test_pass_123')
47 mock_post.assert_called_once_with(
48 'https://my.farm.bot/api/tokens',
49 headers={'content-type': 'application/json'},
50 json={'user': {'email': 'test_email@gmail.com',
51 'password': 'test_pass_123'}}
52 )
53 self.assertEqual(self.fb.state.token, expected_token)
55 @patch('requests.post')
56 def test_get_token_custom_server(self, mock_post):
57 '''POSITIVE TEST: function called with email, password, and custom server'''
58 mock_response = Mock()
59 expected_token = {'token': 'abc123'}
60 mock_response.json.return_value = expected_token
61 mock_response.status_code = 200
62 mock_response.text = 'text'
63 mock_post.return_value = mock_response
64 self.fb.set_token(None)
65 # Call with custom server
66 self.fb.get_token('test_email@gmail.com', 'test_pass_123',
67 'https://staging.farm.bot')
68 mock_post.assert_called_once_with(
69 'https://staging.farm.bot/api/tokens',
70 headers={'content-type': 'application/json'},
71 json={'user': {'email': 'test_email@gmail.com',
72 'password': 'test_pass_123'}}
73 )
74 self.assertEqual(self.fb.state.token, expected_token)
76 @patch('requests.post')
77 def helper_get_token_errors(self, *args, **kwargs):
78 '''Test helper for get_token errors'''
79 mock_post = args[0]
80 status_code = kwargs['status_code']
81 error_msg = kwargs['error_msg']
82 mock_response = Mock()
83 mock_response.status_code = status_code
84 mock_post.return_value = mock_response
85 self.fb.set_token(None)
86 self.fb.get_token('email@gmail.com', 'test_pass_123')
87 mock_post.assert_called_once_with(
88 'https://my.farm.bot/api/tokens',
89 headers={'content-type': 'application/json'},
90 json={'user': {'email': 'email@gmail.com',
91 'password': 'test_pass_123'}}
92 )
93 self.assertEqual(self.fb.state.error, error_msg)
94 self.assertIsNone(self.fb.state.token)
96 def test_get_token_bad_email(self):
97 '''NEGATIVE TEST: function called with incorrect email'''
98 self.helper_get_token_errors(
99 status_code=422,
100 error_msg='HTTP ERROR: Incorrect email address or password.',
101 )
103 def test_get_token_bad_server(self):
104 '''NEGATIVE TEST: function called with incorrect server'''
105 self.helper_get_token_errors(
106 status_code=404,
107 error_msg='HTTP ERROR: The server address does not exist.',
108 )
110 def test_get_token_other_error(self):
111 '''get_token: other error'''
112 self.helper_get_token_errors(
113 status_code=500,
114 error_msg='HTTP ERROR: Unexpected status code 500',
115 )
117 @patch('requests.post')
118 def helper_get_token_exceptions(self, *args, **kwargs):
119 '''Test helper for get_token exceptions'''
120 mock_post = args[0]
121 exception = kwargs['exception']
122 error_msg = kwargs['error_msg']
123 mock_post.side_effect = exception
124 self.fb.set_token(None)
125 self.fb.get_token('email@gmail.com', 'test_pass_123')
126 mock_post.assert_called_once_with(
127 'https://my.farm.bot/api/tokens',
128 headers={'content-type': 'application/json'},
129 json={'user': {'email': 'email@gmail.com',
130 'password': 'test_pass_123'}}
131 )
132 self.assertEqual(self.fb.state.error, error_msg)
133 self.assertIsNone(self.fb.state.token)
135 def test_get_token_server_not_found(self):
136 '''get_token: server not found'''
137 self.helper_get_token_exceptions(
138 exception=requests.exceptions.ConnectionError,
139 error_msg='DNS ERROR: The server address does not exist.',
140 )
142 def test_get_token_timeout(self):
143 '''get_token: timeout'''
144 self.helper_get_token_exceptions(
145 exception=requests.exceptions.Timeout,
146 error_msg='DNS ERROR: The request timed out.',
147 )
149 def test_get_token_problem(self):
150 '''get_token: problem'''
151 self.helper_get_token_exceptions(
152 exception=requests.exceptions.RequestException,
153 error_msg='DNS ERROR: There was a problem with the request.',
154 )
156 def test_get_token_other_exception(self):
157 '''get_token: other exception'''
158 self.helper_get_token_exceptions(
159 exception=Exception('other'),
160 error_msg='DNS ERROR: An unexpected error occurred: other',
161 )
163 @patch('requests.request')
164 def helper_api_get_error(self, *args, **kwargs):
165 '''Test helper for api_get errors'''
166 mock_request = args[0]
167 status_code = kwargs['status_code']
168 error_msg = kwargs['error_msg']
169 mock_response = Mock()
170 mock_response.status_code = status_code
171 mock_response.reason = 'reason'
172 mock_response.text = 'text'
173 mock_response.json.return_value = {'error': 'error'}
174 mock_request.return_value = mock_response
175 response = self.fb.api_get('device')
176 mock_request.assert_called_once_with(
177 'GET',
178 'https://my.farm.bot/api/device',
179 headers={
180 'authorization': 'encoded_token_value',
181 'content-type': 'application/json'
182 },
183 json=None,
184 )
185 self.assertEqual(response, error_msg)
187 def test_api_get_errors(self):
188 '''Test api_get errors'''
189 self.helper_api_get_error(
190 status_code=404,
191 error_msg='CLIENT ERROR 404: The specified endpoint does not exist. ({\n "error": "error"\n})',
192 )
193 self.helper_api_get_error(
194 status_code=500,
195 error_msg='SERVER ERROR 500: text ({\n "error": "error"\n})',
196 )
197 self.helper_api_get_error(
198 status_code=600,
199 error_msg='UNEXPECTED ERROR 600: text ({\n "error": "error"\n})',
200 )
202 @patch('requests.request')
203 def test_api_string_error_response_handling(self, mock_request):
204 '''Test API string response errors'''
205 mock_response = Mock()
206 mock_response.status_code = 404
207 mock_response.reason = 'reason'
208 mock_response.text = 'error string'
209 mock_response.json.side_effect = requests.exceptions.JSONDecodeError('', '', 0)
210 mock_request.return_value = mock_response
211 response = self.fb.api_get('device')
212 mock_request.assert_called_once_with(
213 'GET',
214 'https://my.farm.bot/api/device',
215 headers={
216 'authorization': 'encoded_token_value',
217 'content-type': 'application/json'
218 },
219 json=None,
220 )
221 self.assertEqual(response, 'CLIENT ERROR 404: The specified endpoint does not exist. (error string)')
223 @patch('requests.request')
224 def test_api_string_error_response_handling_html(self, mock_request):
225 '''Test API html string response errors'''
226 mock_response = Mock()
227 mock_response.status_code = 404
228 mock_response.reason = 'reason'
229 mock_response.text = '<html><h1>error0</h1><h2>error1</h2></html>'
230 mock_response.json.side_effect = requests.exceptions.JSONDecodeError('', '', 0)
231 mock_request.return_value = mock_response
232 response = self.fb.api_get('device')
233 mock_request.assert_called_once_with(
234 'GET',
235 'https://my.farm.bot/api/device',
236 headers={
237 'authorization': 'encoded_token_value',
238 'content-type': 'application/json'
239 },
240 json=None,
241 )
242 self.assertEqual(response, 'CLIENT ERROR 404: The specified endpoint does not exist. (error0 error1)')
244 @patch('requests.request')
245 def test_api_get_endpoint_only(self, mock_request):
246 '''POSITIVE TEST: function called with endpoint only'''
247 mock_response = Mock()
248 expected_response = {'device': 'info'}
249 mock_response.json.return_value = expected_response
250 mock_response.status_code = 200
251 mock_response.text = 'text'
252 mock_request.return_value = mock_response
253 # Call with endpoint only
254 response = self.fb.api_get('device')
255 mock_request.assert_called_once_with(
256 'GET',
257 'https://my.farm.bot/api/device',
258 headers={
259 'authorization': 'encoded_token_value',
260 'content-type': 'application/json'
261 },
262 json=None,
263 )
264 self.assertEqual(response, expected_response)
266 @patch('requests.request')
267 def test_api_get_with_id(self, mock_request):
268 '''POSITIVE TEST: function called with valid ID'''
269 mock_response = Mock()
270 expected_response = {'peripheral': 'info'}
271 mock_response.json.return_value = expected_response
272 mock_response.status_code = 200
273 mock_response.text = 'text'
274 mock_request.return_value = mock_response
275 # Call with specific ID
276 response = self.fb.api_get('peripherals', '12345')
277 mock_request.assert_called_once_with(
278 'GET',
279 'https://my.farm.bot/api/peripherals/12345',
280 headers={
281 'authorization': 'encoded_token_value',
282 'content-type': 'application/json',
283 },
284 json=None,
285 )
286 self.assertEqual(response, expected_response)
288 @patch('requests.request')
289 def test_check_token_api_request(self, mock_request):
290 '''Test check_token: API request'''
291 self.fb.set_token(None)
292 with self.assertRaises(ValueError) as cm:
293 self.fb.api_get('points')
294 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
295 mock_request.assert_not_called()
296 self.assertEqual(self.fb.state.error, self.fb.state.NO_TOKEN_ERROR)
298 @patch('paho.mqtt.client.Client')
299 @patch('requests.request')
300 def test_check_token_broker(self, mock_request, mock_mqtt):
301 '''Test check_token: broker'''
302 mock_client = Mock()
303 mock_mqtt.return_value = mock_client
304 self.fb.set_token(None)
305 with self.assertRaises(ValueError) as cm:
306 self.fb.on(123)
307 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
308 with self.assertRaises(ValueError) as cm:
309 self.fb.read_sensor(123)
310 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
311 with self.assertRaises(ValueError) as cm:
312 self.fb.get_xyz()
313 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
314 with self.assertRaises(ValueError) as cm:
315 self.fb.read_status()
316 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR)
317 mock_request.assert_not_called()
318 mock_client.publish.assert_not_called()
319 self.assertEqual(self.fb.state.error, self.fb.state.NO_TOKEN_ERROR)
321 @patch('paho.mqtt.client.Client')
322 def test_publish_disabled(self, mock_mqtt):
323 '''Test publish disabled'''
324 mock_client = Mock()
325 mock_mqtt.return_value = mock_client
326 self.fb.state.dry_run = True
327 self.fb.on(123)
328 mock_client.publish.assert_not_called()
330 @patch('requests.request')
331 def test_api_patch(self, mock_request):
332 '''test api_patch function'''
333 mock_response = Mock()
334 mock_response.status_code = 200
335 mock_response.text = 'text'
336 mock_response.json.return_value = {'name': 'new name'}
337 mock_request.return_value = mock_response
338 device_info = self.fb.api_patch('device', {'name': 'new name'})
339 mock_request.assert_has_calls([call(
340 'PATCH',
341 'https://my.farm.bot/api/device',
342 headers={
343 'authorization': 'encoded_token_value',
344 'content-type': 'application/json',
345 },
346 json={'name': 'new name'},
347 ),
348 call().json(),
349 ])
350 self.assertEqual(device_info, {'name': 'new name'})
352 @patch('requests.request')
353 def test_api_post(self, mock_request):
354 '''test api_post function'''
355 mock_response = Mock()
356 mock_response.status_code = 200
357 mock_response.text = 'text'
358 mock_response.json.return_value = {'name': 'new name'}
359 mock_request.return_value = mock_response
360 point = self.fb.api_post('points', {'name': 'new name'})
361 mock_request.assert_has_calls([call(
362 'POST',
363 'https://my.farm.bot/api/points',
364 headers={
365 'authorization': 'encoded_token_value',
366 'content-type': 'application/json',
367 },
368 json={'name': 'new name'},
369 ),
370 call().json(),
371 ])
372 self.assertEqual(point, {'name': 'new name'})
374 @patch('requests.request')
375 def test_api_delete(self, mock_request):
376 '''test api_delete function'''
377 mock_response = Mock()
378 mock_response.status_code = 200
379 mock_response.text = 'text'
380 mock_response.json.return_value = {'name': 'deleted'}
381 mock_request.return_value = mock_response
382 result = self.fb.api_delete('points', 12345)
383 mock_request.assert_called_once_with(
384 'DELETE',
385 'https://my.farm.bot/api/points/12345',
386 headers={
387 'authorization': 'encoded_token_value',
388 'content-type': 'application/json',
389 },
390 json=None,
391 )
392 self.assertEqual(result, {'name': 'deleted'})
394 @patch('requests.request')
395 def test_api_delete_requests_disabled(self, mock_request):
396 '''test api_delete function: requests disabled'''
397 self.fb.state.dry_run = True
398 result = self.fb.api_delete('points', 12345)
399 mock_request.assert_not_called()
400 self.assertEqual(result, {"edit_requests_disabled": True})
402 @patch('requests.request')
403 def test_group_one(self, mock_request):
404 '''test group function: get one group'''
405 mock_response = Mock()
406 mock_response.json.return_value = {'name': 'Group 0'}
407 mock_response.status_code = 200
408 mock_response.text = 'text'
409 mock_request.return_value = mock_response
410 group_info = self.fb.group(12345)
411 mock_request.assert_called_once_with(
412 'GET',
413 'https://my.farm.bot/api/point_groups/12345',
414 headers={
415 'authorization': 'encoded_token_value',
416 'content-type': 'application/json',
417 },
418 json=None,
419 )
420 self.assertEqual(group_info, {'name': 'Group 0'})
422 @patch('requests.request')
423 def test_group_all(self, mock_request):
424 '''test group function: get all groups'''
425 mock_response = Mock()
426 mock_response.json.return_value = [{'name': 'Group 0'}]
427 mock_response.status_code = 200
428 mock_response.text = 'text'
429 mock_request.return_value = mock_response
430 group_info = self.fb.group()
431 mock_request.assert_called_once_with(
432 'GET',
433 'https://my.farm.bot/api/point_groups',
434 headers={
435 'authorization': 'encoded_token_value',
436 'content-type': 'application/json',
437 },
438 json=None,
439 )
440 self.assertEqual(group_info, [{'name': 'Group 0'}])
442 @patch('requests.request')
443 def test_curve_one(self, mock_request):
444 '''test curve function: get one curve'''
445 mock_response = Mock()
446 mock_response.json.return_value = {'name': 'Curve 0'}
447 mock_response.status_code = 200
448 mock_response.text = 'text'
449 mock_request.return_value = mock_response
450 curve_info = self.fb.curve(12345)
451 mock_request.assert_called_once_with(
452 'GET',
453 'https://my.farm.bot/api/curves/12345',
454 headers={
455 'authorization': 'encoded_token_value',
456 'content-type': 'application/json',
457 },
458 json=None,
459 )
460 self.assertEqual(curve_info, {'name': 'Curve 0'})
462 @patch('requests.request')
463 def test_curve_all(self, mock_request):
464 '''test curve function: get all curves'''
465 mock_response = Mock()
466 mock_response.json.return_value = [{'name': 'Curve 0'}]
467 mock_response.status_code = 200
468 mock_response.text = 'text'
469 mock_request.return_value = mock_response
470 curve_info = self.fb.curve()
471 mock_request.assert_called_once_with(
472 'GET',
473 'https://my.farm.bot/api/curves',
474 headers={
475 'authorization': 'encoded_token_value',
476 'content-type': 'application/json',
477 },
478 json=None,
479 )
480 self.assertEqual(curve_info, [{'name': 'Curve 0'}])
482 @patch('requests.request')
483 def test_safe_z(self, mock_request):
484 '''test safe_z function'''
485 mock_response = Mock()
486 mock_response.json.return_value = {'safe_height': 100}
487 mock_response.status_code = 200
488 mock_response.text = 'text'
489 mock_request.return_value = mock_response
490 safe_height = self.fb.safe_z()
491 mock_request.assert_called_once_with(
492 'GET',
493 'https://my.farm.bot/api/fbos_config',
494 headers={
495 'authorization': 'encoded_token_value',
496 'content-type': 'application/json',
497 },
498 json=None,
499 )
500 self.assertEqual(safe_height, 100)
502 @patch('requests.request')
503 def test_garden_size(self, mock_request):
504 '''test garden_size function'''
505 mock_response = Mock()
506 mock_response.json.return_value = {
507 'movement_axis_nr_steps_x': 1000,
508 'movement_axis_nr_steps_y': 2000,
509 'movement_axis_nr_steps_z': 40000,
510 'movement_step_per_mm_x': 5,
511 'movement_step_per_mm_y': 5,
512 'movement_step_per_mm_z': 25,
513 }
514 mock_response.status_code = 200
515 mock_response.text = 'text'
516 mock_request.return_value = mock_response
517 garden_size = self.fb.garden_size()
518 mock_request.assert_called_once_with(
519 'GET',
520 'https://my.farm.bot/api/firmware_config',
521 headers={
522 'authorization': 'encoded_token_value',
523 'content-type': 'application/json',
524 },
525 json=None,
526 )
527 self.assertEqual(garden_size, {'x': 200, 'y': 400, 'z': 1600})
529 @patch('requests.request')
530 def test_log(self, mock_request):
531 '''test log function'''
532 mock_response = Mock()
533 mock_response.status_code = 200
534 mock_response.text = 'text'
535 mock_response.json.return_value = {'message': 'test message'}
536 mock_request.return_value = mock_response
537 self.fb.log('test message', 'info', 'toast')
538 mock_request.assert_called_once_with(
539 'POST',
540 'https://my.farm.bot/api/logs',
541 headers={
542 'authorization': 'encoded_token_value',
543 'content-type': 'application/json',
544 },
545 json={
546 'message': 'test message',
547 'type': 'info',
548 'channels': ['toast'],
549 },
550 )
552 @patch('paho.mqtt.client.Client')
553 def test_connect_broker(self, mock_mqtt):
554 '''Test test_connect_broker command'''
555 mock_client = Mock()
556 mock_mqtt.return_value = mock_client
557 self.fb.connect_broker()
558 mock_client.username_pw_set.assert_called_once_with(
559 username='device_0',
560 password='encoded_token_value')
561 mock_client.connect.assert_called_once_with(
562 'mqtt_url',
563 port=1883,
564 keepalive=60)
565 mock_client.loop_start.assert_called()
567 def test_disconnect_broker(self):
568 '''Test disconnect_broker command'''
569 mock_client = Mock()
570 self.fb.broker.client = mock_client
571 self.fb.disconnect_broker()
572 mock_client.loop_stop.assert_called_once()
573 mock_client.disconnect.assert_called_once()
575 @patch('paho.mqtt.client.Client')
576 def test_listen(self, mock_mqtt):
577 '''Test listen command'''
578 mock_client = Mock()
579 mock_mqtt.return_value = mock_client
580 self.fb.listen()
582 class MockMessage:
583 '''Mock message class'''
584 topic = 'topic'
585 payload = '{"message": "test message"}'
586 mock_client.on_message('', '', MockMessage())
587 mock_client.username_pw_set.assert_called_once_with(
588 username='device_0',
589 password='encoded_token_value')
590 mock_client.connect.assert_called_once_with(
591 'mqtt_url',
592 port=1883,
593 keepalive=60)
594 mock_client.subscribe.assert_called_once_with('bot/device_0/#')
595 mock_client.loop_start.assert_called()
596 mock_client.loop_stop.assert_called()
598 @patch('paho.mqtt.client.Client')
599 def test_listen_clear_last(self, mock_mqtt):
600 '''Test listen command: clear last message'''
601 mock_client = Mock()
602 mock_mqtt.return_value = mock_client
603 self.fb.state.last_messages = {'#': "message"}
604 self.fb.state.test_env = False
605 self.fb.listen()
606 self.assertIsNone(self.fb.state.last_messages['#'])
608 @patch('paho.mqtt.client.Client')
609 def test_publish_apply_label(self, mock_mqtt):
610 '''Test publish command: set uuid'''
611 mock_client = Mock()
612 mock_mqtt.return_value = mock_client
613 self.fb.state.test_env = False
614 self.fb.broker.publish({'kind': 'sync', 'args': {}})
615 self.assertNotIn(self.fb.state.last_published.get('args', {}).get('label'), ['test', '', None])
617 @patch('requests.request')
618 @patch('paho.mqtt.client.Client')
619 def send_command_test_helper(self, *args, **kwargs):
620 '''Helper for testing command execution'''
621 execute_command = args[0]
622 mock_mqtt = args[1]
623 mock_request = args[2]
624 expected_command = kwargs.get('expected_command')
625 extra_rpc_args = kwargs.get('extra_rpc_args')
626 mock_api_response = kwargs.get('mock_api_response')
627 error = kwargs.get('error')
628 mock_client = Mock()
629 mock_mqtt.return_value = mock_client
630 mock_response = Mock()
631 mock_response.json.return_value = mock_api_response
632 mock_response.status_code = 200
633 mock_response.text = 'text'
634 mock_request.return_value = mock_response
635 self.fb.state.last_messages['from_device'] = {
636 'kind': 'rpc_error' if error else 'rpc_ok',
637 'args': {'label': 'test'},
638 }
639 execute_command()
640 if expected_command is None:
641 mock_client.publish.assert_not_called()
642 return
643 expected_payload = {
644 'kind': 'rpc_request',
645 'args': {'label': 'test', **extra_rpc_args},
646 'body': [expected_command],
647 }
648 mock_client.username_pw_set.assert_called_once_with(
649 username='device_0',
650 password='encoded_token_value')
651 mock_client.connect.assert_called_once_with(
652 'mqtt_url',
653 port=1883,
654 keepalive=60)
655 mock_client.loop_start.assert_called()
656 mock_client.publish.assert_called_once_with(
657 'bot/device_0/from_clients',
658 payload=json.dumps(expected_payload))
659 if not error:
660 self.assertNotEqual(self.fb.state.error, 'RPC error response received.')
662 def test_message(self):
663 '''Test message command'''
664 def exec_command():
665 self.fb.message('test message', 'info')
666 self.send_command_test_helper(
667 exec_command,
668 expected_command={
669 'kind': 'send_message',
670 'args': {'message': 'test message', 'message_type': 'info'},
671 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}],
672 },
673 extra_rpc_args={},
674 mock_api_response={})
676 def test_debug(self):
677 '''Test debug command'''
678 def exec_command():
679 self.fb.debug('test message')
680 self.send_command_test_helper(
681 exec_command,
682 expected_command={
683 'kind': 'send_message',
684 'args': {'message': 'test message', 'message_type': 'debug'},
685 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}],
686 },
687 extra_rpc_args={},
688 mock_api_response={})
690 def test_toast(self):
691 '''Test toast command'''
692 def exec_command():
693 self.fb.toast('test message')
694 self.send_command_test_helper(
695 exec_command,
696 expected_command={
697 'kind': 'send_message',
698 'args': {'message': 'test message', 'message_type': 'info'},
699 'body': [{'kind': 'channel', 'args': {'channel_name': 'toast'}}],
700 },
701 extra_rpc_args={},
702 mock_api_response={})
704 def test_invalid_message_type(self):
705 '''Test message_type validation'''
706 def exec_command():
707 with self.assertRaises(ValueError) as cm:
708 self.fb.message('test', message_type='nope')
709 self.assertEqual(
710 cm.exception.args[0],
711 "Invalid message type: `nope` not in ['assertion', 'busy', 'debug', 'error', 'fun', 'info', 'success', 'warn']")
712 self.send_command_test_helper(
713 exec_command,
714 expected_command=None,
715 extra_rpc_args={},
716 mock_api_response={})
718 def test_invalid_message_channel(self):
719 '''Test message channel validation'''
720 def exec_command():
721 with self.assertRaises(ValueError) as cm:
722 self.fb.message('test', channel='nope')
723 self.assertEqual(
724 cm.exception.args[0],
725 "Invalid channel: nope not in ['ticker', 'toast', 'email', 'espeak']")
726 self.send_command_test_helper(
727 exec_command,
728 expected_command=None,
729 extra_rpc_args={},
730 mock_api_response={})
732 def test_read_status(self):
733 '''Test read_status command'''
734 def exec_command():
735 self.fb.read_status()
736 self.send_command_test_helper(
737 exec_command,
738 expected_command={
739 'kind': 'read_status',
740 'args': {},
741 },
742 extra_rpc_args={},
743 mock_api_response={})
745 def test_read_sensor(self):
746 '''Test read_sensor command'''
747 def exec_command():
748 self.fb.read_sensor('Tool Verification')
749 self.send_command_test_helper(
750 exec_command,
751 expected_command={
752 'kind': 'read_pin',
753 'args': {
754 'pin_mode': 0,
755 'label': '---',
756 'pin_number': {
757 'kind': 'named_pin',
758 'args': {'pin_type': 'Sensor', 'pin_id': 123},
759 },
760 },
761 },
762 extra_rpc_args={},
763 mock_api_response=[{'id': 123, 'label': 'Tool Verification', 'mode': 0}])
765 def test_read_sensor_not_found(self):
766 '''Test read_sensor command: sensor not found'''
767 def exec_command():
768 self.fb.read_sensor('Temperature')
769 self.send_command_test_helper(
770 exec_command,
771 expected_command=None,
772 extra_rpc_args={},
773 mock_api_response=[{'label': 'Tool Verification'}])
774 self.assertEqual(self.fb.state.error, "ERROR: 'Temperature' not in sensors: ['Tool Verification'].")
776 def test_assertion(self):
777 '''Test assertion command'''
778 def exec_command():
779 self.fb.assertion('return true', 'abort')
780 self.send_command_test_helper(
781 exec_command,
782 expected_command={
783 'kind': 'assertion',
784 'args': {
785 'assertion_type': 'abort',
786 'lua': 'return true',
787 '_then': {'kind': 'nothing', 'args': {}},
788 }
789 },
790 extra_rpc_args={},
791 mock_api_response={})
793 def test_assertion_with_recovery_sequence(self):
794 '''Test assertion command with recovery sequence'''
795 def exec_command():
796 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
797 self.send_command_test_helper(
798 exec_command,
799 expected_command={
800 'kind': 'assertion',
801 'args': {
802 'assertion_type': 'abort',
803 'lua': 'return true',
804 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
805 }
806 },
807 extra_rpc_args={},
808 mock_api_response=[{'id': 123, 'name': 'Recovery Sequence'}])
810 def test_assertion_recovery_sequence_not_found(self):
811 '''Test assertion command: recovery sequence not found'''
812 def exec_command():
813 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
814 self.send_command_test_helper(
815 exec_command,
816 expected_command=None,
817 extra_rpc_args={},
818 mock_api_response=[])
819 self.assertEqual(self.fb.state.error, "ERROR: 'Recovery Sequence' not in sequences: [].")
821 def test_assertion_invalid_assertion_type(self):
822 '''Test assertion command: invalid assertion type'''
823 def exec_command():
824 with self.assertRaises(ValueError) as cm:
825 self.fb.assertion('return true', 'nope')
826 self.assertEqual(
827 cm.exception.args[0],
828 "Invalid assertion_type: nope not in ['abort', 'recover', 'abort_recover', 'continue']")
829 self.send_command_test_helper(
830 exec_command,
831 expected_command=None,
832 extra_rpc_args={},
833 mock_api_response={})
835 def test_wait(self):
836 '''Test wait command'''
837 def exec_command():
838 self.fb.wait(123)
839 self.send_command_test_helper(
840 exec_command,
841 expected_command={
842 'kind': 'wait',
843 'args': {'milliseconds': 123},
844 },
845 extra_rpc_args={},
846 mock_api_response={})
848 def test_unlock(self):
849 '''Test unlock command'''
850 def exec_command():
851 self.fb.unlock()
852 self.send_command_test_helper(
853 exec_command,
854 expected_command={
855 'kind': 'emergency_unlock',
856 'args': {},
857 },
858 extra_rpc_args={'priority': 9000},
859 mock_api_response={})
861 def test_e_stop(self):
862 '''Test e_stop command'''
863 def exec_command():
864 self.fb.e_stop()
865 self.send_command_test_helper(
866 exec_command,
867 expected_command={
868 'kind': 'emergency_lock',
869 'args': {},
870 },
871 extra_rpc_args={'priority': 9000},
872 mock_api_response={})
874 def test_find_home(self):
875 '''Test find_home command'''
876 def exec_command():
877 self.fb.find_home()
878 self.send_command_test_helper(
879 exec_command,
880 expected_command={
881 'kind': 'find_home',
882 'args': {'axis': 'all', 'speed': 100},
883 },
884 extra_rpc_args={},
885 mock_api_response={})
887 def test_find_home_speed_error(self):
888 '''Test find_home command: speed error'''
889 def exec_command():
890 self.fb.find_home('all', 0)
891 self.send_command_test_helper(
892 exec_command,
893 expected_command=None,
894 extra_rpc_args={},
895 mock_api_response={})
896 self.assertEqual(self.fb.state.error, 'ERROR: Speed constrained to 1-100.')
898 def test_find_home_invalid_axis(self):
899 '''Test find_home command: invalid axis'''
900 def exec_command():
901 with self.assertRaises(ValueError) as cm:
902 self.fb.find_home('nope')
903 self.assertEqual(
904 cm.exception.args[0],
905 "Invalid axis: nope not in ['x', 'y', 'z', 'all']")
906 self.send_command_test_helper(
907 exec_command,
908 expected_command=None,
909 extra_rpc_args={},
910 mock_api_response={})
912 def test_set_home(self):
913 '''Test set_home command'''
914 def exec_command():
915 self.fb.set_home()
916 self.send_command_test_helper(
917 exec_command,
918 expected_command={
919 'kind': 'zero',
920 'args': {'axis': 'all'},
921 },
922 extra_rpc_args={},
923 mock_api_response={})
925 def test_toggle_peripheral(self):
926 '''Test toggle_peripheral command'''
927 def exec_command():
928 self.fb.toggle_peripheral('New Peripheral')
929 self.send_command_test_helper(
930 exec_command,
931 expected_command={
932 'kind': 'toggle_pin',
933 'args': {
934 'pin_number': {
935 'kind': 'named_pin',
936 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
937 },
938 },
939 },
940 extra_rpc_args={},
941 mock_api_response=[{'label': 'New Peripheral', 'id': 123}])
943 def test_toggle_peripheral_not_found(self):
944 '''Test toggle_peripheral command: peripheral not found'''
945 def exec_command():
946 self.fb.toggle_peripheral('New Peripheral')
947 self.send_command_test_helper(
948 exec_command,
949 expected_command=None,
950 extra_rpc_args={},
951 mock_api_response=[])
952 self.assertEqual(self.fb.state.error, 'ERROR: \'New Peripheral\' not in peripherals: [].')
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 mock_response = Mock()
1331 mock_api_response = [
1332 {
1333 'id': 123,
1334 'name': 'Seed Tray',
1335 'pointer_type': '', # not an actual data field
1336 },
1337 {
1338 'pointer_type': 'ToolSlot',
1339 'pullout_direction': 1,
1340 'x': 0,
1341 'y': 0,
1342 'z': 0,
1343 'tool_id': 123,
1344 'name': '', # not an actual data field
1345 **tray_data,
1346 },
1347 ]
1348 mock_response.json.return_value = mock_api_response
1349 mock_response.status_code = 200
1350 mock_response.text = 'text'
1351 mock_request.return_value = mock_response
1352 cell = self.fb.get_seed_tray_cell('Seed Tray', cell)
1353 mock_request.assert_has_calls([
1354 call(
1355 'GET',
1356 'https://my.farm.bot/api/tools',
1357 headers={
1358 'authorization': 'encoded_token_value',
1359 'content-type': 'application/json',
1360 },
1361 json=None,
1362 ),
1363 call().json(),
1364 call(
1365 'GET',
1366 'https://my.farm.bot/api/points',
1367 headers={
1368 'authorization': 'encoded_token_value',
1369 'content-type': 'application/json',
1370 },
1371 json=None,
1372 ),
1373 call().json(),
1374 ])
1375 self.assertEqual(cell, expected_xyz, kwargs)
1377 def test_get_seed_tray_cell(self):
1378 '''Test get_seed_tray_cell'''
1379 test_cases = [
1380 {
1381 'tray_data': {'pullout_direction': 1},
1382 'cell': 'a1',
1383 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1384 },
1385 {
1386 'tray_data': {'pullout_direction': 1},
1387 'cell': 'b2',
1388 'expected_xyz': {'x': -11.25, 'y': -6.25, 'z': 0},
1389 },
1390 {
1391 'tray_data': {'pullout_direction': 1},
1392 'cell': 'd4',
1393 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1394 },
1395 {
1396 'tray_data': {'pullout_direction': 2},
1397 'cell': 'a1',
1398 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1399 },
1400 {
1401 'tray_data': {'pullout_direction': 2},
1402 'cell': 'b2',
1403 'expected_xyz': {'x': -23.75, 'y': 6.25, 'z': 0},
1404 },
1405 {
1406 'tray_data': {'pullout_direction': 2},
1407 'cell': 'd4',
1408 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1409 },
1410 {
1411 'tray_data': {'pullout_direction': 2, 'x': 100, 'y': 200, 'z': -100},
1412 'cell': 'd4',
1413 'expected_xyz': {'x': 101.25, 'y': 181.25, 'z': -100},
1414 },
1415 ]
1416 for test_case in test_cases:
1417 self.helper_get_seed_tray_cell(**test_case)
1419 @patch('requests.request')
1420 def helper_get_seed_tray_cell_error(self, *args, **kwargs):
1421 '''Test helper for get_seed_tray_cell command errors'''
1422 mock_request = args[0]
1423 tray_data = kwargs['tray_data']
1424 cell = kwargs['cell']
1425 error = kwargs['error']
1426 mock_response = Mock()
1427 mock_api_response = [
1428 {
1429 'id': 123,
1430 'name': 'Seed Tray',
1431 'pointer_type': '', # not an actual data field
1432 },
1433 {
1434 'pointer_type': 'ToolSlot',
1435 'pullout_direction': 1,
1436 'x': 0,
1437 'y': 0,
1438 'z': 0,
1439 'tool_id': 123,
1440 'name': '', # not an actual data field
1441 **tray_data,
1442 },
1443 ]
1444 mock_response.json.return_value = mock_api_response
1445 mock_response.status_code = 200
1446 mock_response.text = 'text'
1447 mock_request.return_value = mock_response
1448 with self.assertRaises(ValueError) as cm:
1449 self.fb.get_seed_tray_cell('Seed Tray', cell)
1450 self.assertEqual(cm.exception.args[0], error)
1451 mock_request.assert_has_calls([
1452 call(
1453 'GET',
1454 'https://my.farm.bot/api/tools',
1455 headers={
1456 'authorization': 'encoded_token_value',
1457 'content-type': 'application/json',
1458 },
1459 json=None,
1460 ),
1461 call().json(),
1462 call(
1463 'GET',
1464 'https://my.farm.bot/api/points',
1465 headers={
1466 'authorization': 'encoded_token_value',
1467 'content-type': 'application/json',
1468 },
1469 json=None,
1470 ),
1471 call().json(),
1472 ])
1474 def test_get_seed_tray_cell_invalid_cell_name(self):
1475 '''Test get_seed_tray_cell: invalid cell name'''
1476 self.helper_get_seed_tray_cell_error(
1477 tray_data={},
1478 cell='e4',
1479 error='Seed Tray Cell must be one of **A1** through **D4**',
1480 )
1482 def test_get_seed_tray_cell_invalid_pullout_direction(self):
1483 '''Test get_seed_tray_cell: invalid pullout direction'''
1484 self.helper_get_seed_tray_cell_error(
1485 tray_data={'pullout_direction': 0},
1486 cell='d4',
1487 error='Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`',
1488 )
1490 @patch('requests.request')
1491 def test_get_seed_tray_cell_no_tray(self, mock_request):
1492 '''Test get_seed_tray_cell: no seed tray'''
1493 mock_response = Mock()
1494 mock_api_response = []
1495 mock_response.json.return_value = mock_api_response
1496 mock_response.status_code = 200
1497 mock_response.text = 'text'
1498 mock_request.return_value = mock_response
1499 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1500 mock_request.assert_has_calls([
1501 call(
1502 'GET',
1503 'https://my.farm.bot/api/tools',
1504 headers={
1505 'authorization': 'encoded_token_value',
1506 'content-type': 'application/json',
1507 },
1508 json=None,
1509 ),
1510 call().json(),
1511 ])
1512 self.assertIsNone(result)
1514 @patch('requests.request')
1515 def test_get_seed_tray_cell_not_mounted(self, mock_request):
1516 '''Test get_seed_tray_cell: seed tray not mounted'''
1517 mock_response = Mock()
1518 mock_api_response = [{
1519 'id': 123,
1520 'name': 'Seed Tray',
1521 'pointer_type': '', # not an actual data field,
1522 }]
1523 mock_response.json.return_value = mock_api_response
1524 mock_response.status_code = 200
1525 mock_response.text = 'text'
1526 mock_request.return_value = mock_response
1527 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1528 mock_request.assert_has_calls([
1529 call(
1530 'GET',
1531 'https://my.farm.bot/api/tools',
1532 headers={
1533 'authorization': 'encoded_token_value',
1534 'content-type': 'application/json',
1535 },
1536 json=None,
1537 ),
1538 call().json(),
1539 ])
1540 self.assertIsNone(result)
1542 def test_get_job_one(self):
1543 '''Test get_job command: get one job'''
1544 def exec_command():
1545 self.fb.state.last_messages['status'] = {
1546 'jobs': {
1547 'job name': {'status': 'working'},
1548 },
1549 }
1550 job = self.fb.get_job('job name')
1551 self.assertEqual(job, {'status': 'working'})
1552 self.send_command_test_helper(
1553 exec_command,
1554 expected_command={
1555 'kind': 'read_status',
1556 'args': {},
1557 },
1558 extra_rpc_args={},
1559 mock_api_response={})
1561 def test_get_job_all(self):
1562 '''Test get_job command: get all jobs'''
1563 def exec_command():
1564 self.fb.state.last_messages['status'] = {
1565 'jobs': {
1566 'job name': {'status': 'working'},
1567 },
1568 }
1569 jobs = self.fb.get_job()
1570 self.assertEqual(jobs, {'job name': {'status': 'working'}})
1571 self.send_command_test_helper(
1572 exec_command,
1573 expected_command={
1574 'kind': 'read_status',
1575 'args': {},
1576 },
1577 extra_rpc_args={},
1578 mock_api_response={})
1580 def test_get_job_no_status(self):
1581 '''Test get_job command: no status'''
1582 def exec_command():
1583 self.fb.state.last_messages['status'] = None
1584 job = self.fb.get_job('job name')
1585 self.assertIsNone(job)
1586 self.send_command_test_helper(
1587 exec_command,
1588 expected_command={
1589 'kind': 'read_status',
1590 'args': {},
1591 },
1592 extra_rpc_args={},
1593 mock_api_response={})
1595 def test_set_job(self):
1596 '''Test set_job command'''
1597 def exec_command():
1598 self.fb.set_job('job name', 'working', 50)
1599 self.send_command_test_helper(
1600 exec_command,
1601 expected_command={
1602 'kind': 'lua',
1603 'args': {'lua': '''local job_name = "job name"
1604 set_job(job_name)
1606 -- Update the job's status and percent:
1607 set_job(job_name, {
1608 status = "working",
1609 percent = 50
1610 })'''},
1611 },
1612 extra_rpc_args={},
1613 mock_api_response={})
1615 def test_complete_job(self):
1616 '''Test complete_job command'''
1617 def exec_command():
1618 self.fb.complete_job('job name')
1619 self.send_command_test_helper(
1620 exec_command,
1621 expected_command={
1622 'kind': 'lua',
1623 'args': {'lua': 'complete_job("job name")'},
1624 },
1625 extra_rpc_args={},
1626 mock_api_response={})
1628 def test_lua(self):
1629 '''Test lua command'''
1630 def exec_command():
1631 self.fb.lua('return true')
1632 self.send_command_test_helper(
1633 exec_command,
1634 expected_command={
1635 'kind': 'lua',
1636 'args': {'lua': 'return true'},
1637 },
1638 extra_rpc_args={},
1639 mock_api_response={})
1641 def test_if_statement(self):
1642 '''Test if_statement command'''
1643 def exec_command():
1644 self.fb.if_statement('pin10', 'is', 0)
1645 self.send_command_test_helper(
1646 exec_command,
1647 expected_command={
1648 'kind': '_if',
1649 'args': {
1650 'lhs': 'pin10',
1651 'op': 'is',
1652 'rhs': 0,
1653 '_then': {'kind': 'nothing', 'args': {}},
1654 '_else': {'kind': 'nothing', 'args': {}},
1655 }
1656 },
1657 extra_rpc_args={},
1658 mock_api_response=[])
1660 def test_if_statement_with_named_pin(self):
1661 '''Test if_statement command with named pin'''
1662 def exec_command():
1663 self.fb.if_statement('Lights', 'is', 0, named_pin_type='Peripheral')
1664 self.send_command_test_helper(
1665 exec_command,
1666 expected_command={
1667 'kind': '_if',
1668 'args': {
1669 'lhs': {
1670 'kind': 'named_pin',
1671 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1672 },
1673 'op': 'is',
1674 'rhs': 0,
1675 '_then': {'kind': 'nothing', 'args': {}},
1676 '_else': {'kind': 'nothing', 'args': {}},
1677 }
1678 },
1679 extra_rpc_args={},
1680 mock_api_response=[{'id': 123, 'label': 'Lights', 'mode': 0}])
1682 def test_if_statement_with_named_pin_not_found(self):
1683 '''Test if_statement command: named pin not found'''
1684 def exec_command():
1685 self.fb.if_statement('Lights', 'is', 0, named_pin_type='Peripheral')
1686 self.send_command_test_helper(
1687 exec_command,
1688 expected_command=None,
1689 extra_rpc_args={},
1690 mock_api_response=[{'label': 'Pump'}])
1691 self.assertEqual(self.fb.state.error, "ERROR: 'Lights' not in peripherals: ['Pump'].")
1693 def test_if_statement_with_sequences(self):
1694 '''Test if_statement command with sequences'''
1695 def exec_command():
1696 self.fb.if_statement('pin10', '<', 0, 'Watering Sequence', 'Drying Sequence')
1697 self.send_command_test_helper(
1698 exec_command,
1699 expected_command={
1700 'kind': '_if',
1701 'args': {
1702 'lhs': 'pin10',
1703 'op': '<',
1704 'rhs': 0,
1705 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
1706 '_else': {'kind': 'execute', 'args': {'sequence_id': 456}},
1707 }
1708 },
1709 extra_rpc_args={},
1710 mock_api_response=[
1711 {'id': 123, 'name': 'Watering Sequence'},
1712 {'id': 456, 'name': 'Drying Sequence'},
1713 ])
1715 def test_if_statement_with_sequence_not_found(self):
1716 '''Test if_statement command: sequence not found'''
1717 def exec_command():
1718 self.fb.if_statement('pin10', '<', 0, 'Watering Sequence', 'Drying Sequence')
1719 self.send_command_test_helper(
1720 exec_command,
1721 expected_command=None,
1722 extra_rpc_args={},
1723 mock_api_response=[])
1724 self.assertEqual(self.fb.state.error, "ERROR: 'Watering Sequence' not in sequences: [].")
1726 def test_if_statement_invalid_operator(self):
1727 '''Test if_statement command: invalid operator'''
1728 def exec_command():
1729 with self.assertRaises(ValueError) as cm:
1730 self.fb.if_statement('pin10', 'nope', 0)
1731 self.assertEqual(
1732 cm.exception.args[0],
1733 "Invalid operator: nope not in ['<', '>', 'is', 'not', 'is_undefined']")
1734 self.send_command_test_helper(
1735 exec_command,
1736 expected_command=None,
1737 extra_rpc_args={},
1738 mock_api_response=[])
1740 def test_if_statement_invalid_variable(self):
1741 '''Test if_statement command: invalid variable'''
1742 variables = ["x", "y", "z", *[f"pin{str(i)}" for i in range(70)]]
1743 def exec_command():
1744 with self.assertRaises(ValueError) as cm:
1745 self.fb.if_statement('nope', '<', 0)
1746 self.assertEqual(
1747 cm.exception.args[0],
1748 f"Invalid variable: nope not in {variables}")
1749 self.send_command_test_helper(
1750 exec_command,
1751 expected_command=None,
1752 extra_rpc_args={},
1753 mock_api_response=[])
1755 def test_if_statement_invalid_named_pin_type(self):
1756 '''Test if_statement command: invalid named pin type'''
1757 def exec_command():
1758 with self.assertRaises(ValueError) as cm:
1759 self.fb.if_statement('pin10', '<', 0, named_pin_type='nope')
1760 self.assertEqual(
1761 cm.exception.args[0],
1762 "Invalid named_pin_type: nope not in ['Peripheral', 'Sensor']")
1763 self.send_command_test_helper(
1764 exec_command,
1765 expected_command=None,
1766 extra_rpc_args={},
1767 mock_api_response=[])
1769 def test_rpc_error(self):
1770 '''Test rpc error handling'''
1771 def exec_command():
1772 self.fb.wait(100)
1773 self.assertEqual(self.fb.state.error, 'RPC error response received.')
1774 self.send_command_test_helper(
1775 exec_command,
1776 error=True,
1777 expected_command={
1778 'kind': 'wait',
1779 'args': {'milliseconds': 100}},
1780 extra_rpc_args={},
1781 mock_api_response=[])
1783 def test_rpc_response_timeout(self):
1784 '''Test rpc response timeout handling'''
1785 def exec_command():
1786 self.fb.state.last_messages['from_device'] = {'kind': 'rpc_ok', 'args': {'label': 'wrong label'}}
1787 self.fb.wait(100)
1788 self.assertEqual(self.fb.state.error, 'Timed out waiting for RPC response.')
1789 self.send_command_test_helper(
1790 exec_command,
1791 expected_command={
1792 'kind': 'wait',
1793 'args': {'milliseconds': 100}},
1794 extra_rpc_args={},
1795 mock_api_response=[])
1797 @staticmethod
1798 def helper_get_print_strings(mock_print):
1799 '''Test helper to get print call strings.'''
1800 return [string[1][0] for string in mock_print.mock_calls if len(string[1]) > 0]
1802 @patch('builtins.print')
1803 def test_print_status(self, mock_print):
1804 '''Test print_status.'''
1805 self.fb.set_verbosity(0)
1806 self.fb.state.print_status(description="testing")
1807 mock_print.assert_not_called()
1808 self.fb.set_verbosity(1)
1809 self.fb.state.print_status(description="testing")
1810 call_strings = self.helper_get_print_strings(mock_print)
1811 self.assertIn('testing', call_strings)
1812 mock_print.reset_mock()
1813 self.fb.set_verbosity(2)
1814 self.fb.state.print_status(endpoint_json=["testing"])
1815 call_strings = self.helper_get_print_strings(mock_print)
1816 call_strings = [s.split('(')[0].strip('`') for s in call_strings]
1817 self.assertIn('[\n "testing"\n]', call_strings)
1818 self.assertIn('test_print_status', call_strings)