Coverage for tests/tests_main.py: 100%
700 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-08-30 13:00 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-08-30 13: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.3
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(1)
581 mock_client.on_connect('', '', '', '')
583 class MockMessage:
584 '''Mock message class'''
585 topic = 'topic'
586 payload = '{"message": "test message"}'
587 mock_client.on_message('', '', MockMessage())
588 mock_client.username_pw_set.assert_called_once_with(
589 username='device_0',
590 password='encoded_token_value')
591 mock_client.connect.assert_called_once_with(
592 'mqtt_url',
593 port=1883,
594 keepalive=60)
595 mock_client.subscribe.assert_called_once_with('bot/device_0/#')
596 mock_client.loop_start.assert_called()
597 mock_client.loop_stop.assert_called()
598 mock_client.disconnect.assert_called()
600 @patch('paho.mqtt.client.Client')
601 def test_listen_clear_last(self, mock_mqtt):
602 '''Test listen command: clear last message'''
603 mock_client = Mock()
604 mock_mqtt.return_value = mock_client
605 self.fb.state.last_messages = {'#': "message"}
606 self.fb.state.test_env = False
607 self.fb.listen(1)
608 self.assertIsNone(self.fb.state.last_messages['#'])
610 @patch('requests.request')
611 @patch('paho.mqtt.client.Client')
612 def send_command_test_helper(self, *args, **kwargs):
613 '''Helper for testing command execution'''
614 execute_command = args[0]
615 mock_mqtt = args[1]
616 mock_request = args[2]
617 expected_command = kwargs.get('expected_command')
618 extra_rpc_args = kwargs.get('extra_rpc_args')
619 mock_api_response = kwargs.get('mock_api_response')
620 mock_client = Mock()
621 mock_mqtt.return_value = mock_client
622 mock_response = Mock()
623 mock_response.json.return_value = mock_api_response
624 mock_response.status_code = 200
625 mock_response.text = 'text'
626 mock_request.return_value = mock_response
627 execute_command()
628 if expected_command is None:
629 mock_client.publish.assert_not_called()
630 return
631 expected_payload = {
632 'kind': 'rpc_request',
633 'args': {'label': '', **extra_rpc_args},
634 'body': [expected_command],
635 }
636 mock_client.username_pw_set.assert_called_once_with(
637 username='device_0',
638 password='encoded_token_value')
639 mock_client.connect.assert_called_once_with(
640 'mqtt_url',
641 port=1883,
642 keepalive=60)
643 mock_client.loop_start.assert_called()
644 mock_client.publish.assert_called_once_with(
645 'bot/device_0/from_clients',
646 payload=json.dumps(expected_payload))
648 def test_message(self):
649 '''Test message command'''
650 def exec_command():
651 self.fb.message('test message', 'info')
652 self.send_command_test_helper(
653 exec_command,
654 expected_command={
655 'kind': 'send_message',
656 'args': {'message': 'test message', 'message_type': 'info'},
657 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}],
658 },
659 extra_rpc_args={},
660 mock_api_response={})
662 def test_debug(self):
663 '''Test debug command'''
664 def exec_command():
665 self.fb.debug('test message')
666 self.send_command_test_helper(
667 exec_command,
668 expected_command={
669 'kind': 'send_message',
670 'args': {'message': 'test message', 'message_type': 'debug'},
671 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}],
672 },
673 extra_rpc_args={},
674 mock_api_response={})
676 def test_toast(self):
677 '''Test toast command'''
678 def exec_command():
679 self.fb.toast('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': 'info'},
685 'body': [{'kind': 'channel', 'args': {'channel_name': 'toast'}}],
686 },
687 extra_rpc_args={},
688 mock_api_response={})
690 def test_invalid_message_type(self):
691 '''Test message_type validation'''
692 def exec_command():
693 with self.assertRaises(ValueError) as cm:
694 self.fb.message('test', message_type='nope')
695 self.assertEqual(
696 cm.exception.args[0],
697 "Invalid message type: `nope` not in ['assertion', 'busy', 'debug', 'error', 'fun', 'info', 'success', 'warn']")
698 self.send_command_test_helper(
699 exec_command,
700 expected_command=None,
701 extra_rpc_args={},
702 mock_api_response={})
704 def test_invalid_message_channel(self):
705 '''Test message channel validation'''
706 def exec_command():
707 with self.assertRaises(ValueError) as cm:
708 self.fb.message('test', channel='nope')
709 self.assertEqual(
710 cm.exception.args[0],
711 "Invalid channel: nope not in ['ticker', 'toast', 'email', 'espeak']")
712 self.send_command_test_helper(
713 exec_command,
714 expected_command=None,
715 extra_rpc_args={},
716 mock_api_response={})
718 def test_read_status(self):
719 '''Test read_status command'''
720 def exec_command():
721 self.fb.read_status()
722 self.send_command_test_helper(
723 exec_command,
724 expected_command={
725 'kind': 'read_status',
726 'args': {},
727 },
728 extra_rpc_args={},
729 mock_api_response={})
731 def test_read_sensor(self):
732 '''Test read_sensor command'''
733 def exec_command():
734 self.fb.read_sensor('Tool Verification')
735 self.send_command_test_helper(
736 exec_command,
737 expected_command={
738 'kind': 'read_pin',
739 'args': {
740 'pin_mode': 0,
741 'label': '---',
742 'pin_number': {
743 'kind': 'named_pin',
744 'args': {'pin_type': 'Sensor', 'pin_id': 123},
745 },
746 },
747 },
748 extra_rpc_args={},
749 mock_api_response=[{'id': 123, 'label': 'Tool Verification', 'mode': 0}])
751 def test_read_sensor_not_found(self):
752 '''Test read_sensor command: sensor not found'''
753 def exec_command():
754 self.fb.read_sensor('Temperature')
755 self.send_command_test_helper(
756 exec_command,
757 expected_command=None,
758 extra_rpc_args={},
759 mock_api_response=[{'label': 'Tool Verification'}])
760 self.assertEqual(self.fb.state.error, "ERROR: 'Temperature' not in sensors: ['Tool Verification'].")
762 def test_assertion(self):
763 '''Test assertion command'''
764 def exec_command():
765 self.fb.assertion('return true', 'abort')
766 self.send_command_test_helper(
767 exec_command,
768 expected_command={
769 'kind': 'assertion',
770 'args': {
771 'assertion_type': 'abort',
772 'lua': 'return true',
773 '_then': {'kind': 'nothing', 'args': {}},
774 }
775 },
776 extra_rpc_args={},
777 mock_api_response={})
779 def test_assertion_with_recovery_sequence(self):
780 '''Test assertion command with recovery sequence'''
781 def exec_command():
782 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
783 self.send_command_test_helper(
784 exec_command,
785 expected_command={
786 'kind': 'assertion',
787 'args': {
788 'assertion_type': 'abort',
789 'lua': 'return true',
790 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
791 }
792 },
793 extra_rpc_args={},
794 mock_api_response=[{'id': 123, 'name': 'Recovery Sequence'}])
796 def test_assertion_recovery_sequence_not_found(self):
797 '''Test assertion command: recovery sequence not found'''
798 def exec_command():
799 self.fb.assertion('return true', 'abort', 'Recovery Sequence')
800 self.send_command_test_helper(
801 exec_command,
802 expected_command=None,
803 extra_rpc_args={},
804 mock_api_response=[])
805 self.assertEqual(self.fb.state.error, "ERROR: 'Recovery Sequence' not in sequences: [].")
807 def test_assertion_invalid_assertion_type(self):
808 '''Test assertion command: invalid assertion type'''
809 def exec_command():
810 with self.assertRaises(ValueError) as cm:
811 self.fb.assertion('return true', 'nope')
812 self.assertEqual(
813 cm.exception.args[0],
814 "Invalid assertion_type: nope not in ['abort', 'recover', 'abort_recover', 'continue']")
815 self.send_command_test_helper(
816 exec_command,
817 expected_command=None,
818 extra_rpc_args={},
819 mock_api_response={})
821 def test_wait(self):
822 '''Test wait command'''
823 def exec_command():
824 self.fb.wait(123)
825 self.send_command_test_helper(
826 exec_command,
827 expected_command={
828 'kind': 'wait',
829 'args': {'milliseconds': 123},
830 },
831 extra_rpc_args={},
832 mock_api_response={})
834 def test_unlock(self):
835 '''Test unlock command'''
836 def exec_command():
837 self.fb.unlock()
838 self.send_command_test_helper(
839 exec_command,
840 expected_command={
841 'kind': 'emergency_unlock',
842 'args': {},
843 },
844 extra_rpc_args={'priority': 9000},
845 mock_api_response={})
847 def test_e_stop(self):
848 '''Test e_stop command'''
849 def exec_command():
850 self.fb.e_stop()
851 self.send_command_test_helper(
852 exec_command,
853 expected_command={
854 'kind': 'emergency_lock',
855 'args': {},
856 },
857 extra_rpc_args={'priority': 9000},
858 mock_api_response={})
860 def test_find_home(self):
861 '''Test find_home command'''
862 def exec_command():
863 self.fb.find_home()
864 self.send_command_test_helper(
865 exec_command,
866 expected_command={
867 'kind': 'find_home',
868 'args': {'axis': 'all', 'speed': 100},
869 },
870 extra_rpc_args={},
871 mock_api_response={})
873 def test_find_home_speed_error(self):
874 '''Test find_home command: speed error'''
875 def exec_command():
876 self.fb.find_home('all', 0)
877 self.send_command_test_helper(
878 exec_command,
879 expected_command=None,
880 extra_rpc_args={},
881 mock_api_response={})
882 self.assertEqual(self.fb.state.error, 'ERROR: Speed constrained to 1-100.')
884 def test_find_home_invalid_axis(self):
885 '''Test find_home command: invalid axis'''
886 def exec_command():
887 with self.assertRaises(ValueError) as cm:
888 self.fb.find_home('nope')
889 self.assertEqual(
890 cm.exception.args[0],
891 "Invalid axis: nope not in ['x', 'y', 'z', 'all']")
892 self.send_command_test_helper(
893 exec_command,
894 expected_command=None,
895 extra_rpc_args={},
896 mock_api_response={})
898 def test_set_home(self):
899 '''Test set_home command'''
900 def exec_command():
901 self.fb.set_home()
902 self.send_command_test_helper(
903 exec_command,
904 expected_command={
905 'kind': 'zero',
906 'args': {'axis': 'all'},
907 },
908 extra_rpc_args={},
909 mock_api_response={})
911 def test_toggle_peripheral(self):
912 '''Test toggle_peripheral command'''
913 def exec_command():
914 self.fb.toggle_peripheral('New Peripheral')
915 self.send_command_test_helper(
916 exec_command,
917 expected_command={
918 'kind': 'toggle_pin',
919 'args': {
920 'pin_number': {
921 'kind': 'named_pin',
922 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
923 },
924 },
925 },
926 extra_rpc_args={},
927 mock_api_response=[{'label': 'New Peripheral', 'id': 123}])
929 def test_toggle_peripheral_not_found(self):
930 '''Test toggle_peripheral command: peripheral not found'''
931 def exec_command():
932 self.fb.toggle_peripheral('New Peripheral')
933 self.send_command_test_helper(
934 exec_command,
935 expected_command=None,
936 extra_rpc_args={},
937 mock_api_response=[])
938 self.assertEqual(self.fb.state.error, 'ERROR: \'New Peripheral\' not in peripherals: [].')
940 def test_on_digital(self):
941 '''Test on command: digital'''
942 def exec_command():
943 self.fb.on(13)
944 self.send_command_test_helper(
945 exec_command,
946 expected_command={
947 'kind': 'write_pin',
948 'args': {
949 'pin_value': 1,
950 'pin_mode': 0,
951 'pin_number': 13,
952 },
953 },
954 extra_rpc_args={},
955 mock_api_response={})
957 def test_off(self):
958 '''Test off command'''
959 def exec_command():
960 self.fb.off(13)
961 self.send_command_test_helper(
962 exec_command,
963 expected_command={
964 'kind': 'write_pin',
965 'args': {
966 'pin_value': 0,
967 'pin_mode': 0,
968 'pin_number': 13,
969 },
970 },
971 extra_rpc_args={},
972 mock_api_response={})
974 def test_move(self):
975 '''Test move command'''
976 def exec_command():
977 self.fb.move(1, 2, 3)
978 self.send_command_test_helper(
979 exec_command,
980 expected_command={
981 'kind': 'move',
982 'args': {},
983 'body': [
984 {'kind': 'axis_overwrite', 'args': {
985 'axis': 'x',
986 'axis_operand': {'kind': 'numeric', 'args': {'number': 1}}}},
987 {'kind': 'axis_overwrite', 'args': {
988 'axis': 'y',
989 'axis_operand': {'kind': 'numeric', 'args': {'number': 2}}}},
990 {'kind': 'axis_overwrite', 'args': {
991 'axis': 'z',
992 'axis_operand': {'kind': 'numeric', 'args': {'number': 3}}}},
993 ],
994 },
995 extra_rpc_args={},
996 mock_api_response={})
998 def test_reboot(self):
999 '''Test reboot command'''
1000 def exec_command():
1001 self.fb.reboot()
1002 self.send_command_test_helper(
1003 exec_command,
1004 expected_command={
1005 'kind': 'reboot',
1006 'args': {'package': 'farmbot_os'},
1007 },
1008 extra_rpc_args={},
1009 mock_api_response={})
1011 def test_shutdown(self):
1012 '''Test shutdown command'''
1013 def exec_command():
1014 self.fb.shutdown()
1015 self.send_command_test_helper(
1016 exec_command,
1017 expected_command={
1018 'kind': 'power_off',
1019 'args': {},
1020 },
1021 extra_rpc_args={},
1022 mock_api_response={})
1024 def test_find_axis_length(self):
1025 '''Test find_axis_length command'''
1026 def exec_command():
1027 self.fb.find_axis_length()
1028 self.send_command_test_helper(
1029 exec_command,
1030 expected_command={
1031 'kind': 'calibrate',
1032 'args': {'axis': 'all'},
1033 },
1034 extra_rpc_args={},
1035 mock_api_response={})
1037 def test_control_peripheral(self):
1038 '''Test control_peripheral command'''
1039 def exec_command():
1040 self.fb.control_peripheral('New Peripheral', 1)
1041 self.send_command_test_helper(
1042 exec_command,
1043 expected_command={
1044 'kind': 'write_pin',
1045 'args': {
1046 'pin_value': 1,
1047 'pin_mode': 0,
1048 'pin_number': {
1049 'kind': 'named_pin',
1050 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1051 },
1052 },
1053 },
1054 extra_rpc_args={},
1055 mock_api_response=[{'label': 'New Peripheral', 'mode': 0, 'id': 123}])
1057 def test_control_peripheral_not_found(self):
1058 '''Test control_peripheral command: peripheral not found'''
1059 def exec_command():
1060 self.fb.control_peripheral('New Peripheral', 1)
1061 self.send_command_test_helper(
1062 exec_command,
1063 expected_command=None,
1064 extra_rpc_args={},
1065 mock_api_response=[{'label': 'Pump'}, {'label': 'Lights'}])
1066 self.assertEqual(self.fb.state.error, "ERROR: 'New Peripheral' not in peripherals: ['Pump', 'Lights'].")
1068 def test_measure_soil_height(self):
1069 '''Test measure_soil_height command'''
1070 def exec_command():
1071 self.fb.measure_soil_height()
1072 self.send_command_test_helper(
1073 exec_command,
1074 expected_command={
1075 'kind': 'execute_script',
1076 'args': {'label': 'Measure Soil Height'},
1077 },
1078 extra_rpc_args={},
1079 mock_api_response={})
1081 def test_detect_weeds(self):
1082 '''Test detect_weeds command'''
1083 def exec_command():
1084 self.fb.detect_weeds()
1085 self.send_command_test_helper(
1086 exec_command,
1087 expected_command={
1088 'kind': 'execute_script',
1089 'args': {'label': 'plant-detection'},
1090 },
1091 extra_rpc_args={},
1092 mock_api_response={})
1094 def test_calibrate_camera(self):
1095 '''Test calibrate_camera command'''
1096 def exec_command():
1097 self.fb.calibrate_camera()
1098 self.send_command_test_helper(
1099 exec_command,
1100 expected_command={
1101 'kind': 'execute_script',
1102 'args': {'label': 'camera-calibration'},
1103 },
1104 extra_rpc_args={},
1105 mock_api_response={})
1107 def test_sequence(self):
1108 '''Test sequence command'''
1109 def exec_command():
1110 self.fb.sequence('My Sequence')
1111 self.send_command_test_helper(
1112 exec_command,
1113 expected_command={
1114 'kind': 'execute',
1115 'args': {'sequence_id': 123},
1116 },
1117 extra_rpc_args={},
1118 mock_api_response=[{'name': 'My Sequence', 'id': 123}])
1120 def test_sequence_not_found(self):
1121 '''Test sequence command: sequence not found'''
1122 def exec_command():
1123 self.fb.sequence('My Sequence')
1124 self.send_command_test_helper(
1125 exec_command,
1126 expected_command=None,
1127 extra_rpc_args={},
1128 mock_api_response=[{'name': 'Water'}])
1129 self.assertEqual(self.fb.state.error, "ERROR: 'My Sequence' not in sequences: ['Water'].")
1131 def test_take_photo(self):
1132 '''Test take_photo command'''
1133 def exec_command():
1134 self.fb.take_photo()
1135 self.send_command_test_helper(
1136 exec_command,
1137 expected_command={
1138 'kind': 'take_photo',
1139 'args': {},
1140 },
1141 extra_rpc_args={},
1142 mock_api_response={})
1144 def test_control_servo(self):
1145 '''Test control_servo command'''
1146 def exec_command():
1147 self.fb.control_servo(4, 100)
1148 self.send_command_test_helper(
1149 exec_command,
1150 expected_command={
1151 'kind': 'set_servo_angle',
1152 'args': {
1153 'pin_number': 4,
1154 'pin_value': 100,
1155 },
1156 },
1157 extra_rpc_args={},
1158 mock_api_response={'mode': 0})
1160 def test_control_servo_error(self):
1161 '''Test control_servo command: error'''
1162 def exec_command():
1163 self.fb.control_servo(4, 200)
1164 self.send_command_test_helper(
1165 exec_command,
1166 expected_command=None,
1167 extra_rpc_args={},
1168 mock_api_response={'mode': 0})
1170 def test_get_xyz(self):
1171 '''Test get_xyz command'''
1172 def exec_command():
1173 self.fb.state.last_messages['status'] = {
1174 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1175 }
1176 position = self.fb.get_xyz()
1177 self.assertEqual(position, {'x': 1, 'y': 2, 'z': 3})
1178 self.send_command_test_helper(
1179 exec_command,
1180 expected_command={
1181 'kind': 'read_status',
1182 'args': {},
1183 },
1184 extra_rpc_args={},
1185 mock_api_response={})
1187 def test_get_xyz_no_status(self):
1188 '''Test get_xyz command: no status'''
1189 def exec_command():
1190 self.fb.state.last_messages['status'] = None
1191 position = self.fb.get_xyz()
1192 self.assertIsNone(position)
1193 self.send_command_test_helper(
1194 exec_command,
1195 expected_command={
1196 'kind': 'read_status',
1197 'args': {},
1198 },
1199 extra_rpc_args={},
1200 mock_api_response={})
1202 def test_check_position(self):
1203 '''Test check_position command: at position'''
1204 def exec_command():
1205 self.fb.state.last_messages['status'] = {
1206 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1207 }
1208 at_position = self.fb.check_position(1, 2, 3, 0)
1209 self.assertTrue(at_position)
1210 self.send_command_test_helper(
1211 exec_command,
1212 expected_command={
1213 'kind': 'read_status',
1214 'args': {},
1215 },
1216 extra_rpc_args={},
1217 mock_api_response={})
1219 def test_check_position_false(self):
1220 '''Test check_position command: not at position'''
1221 def exec_command():
1222 self.fb.state.last_messages['status'] = {
1223 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}},
1224 }
1225 at_position = self.fb.check_position(0, 0, 0, 2)
1226 self.assertFalse(at_position)
1227 self.send_command_test_helper(
1228 exec_command,
1229 expected_command={
1230 'kind': 'read_status',
1231 'args': {},
1232 },
1233 extra_rpc_args={},
1234 mock_api_response={})
1236 def test_check_position_no_status(self):
1237 '''Test check_position command: no status'''
1238 def exec_command():
1239 self.fb.state.last_messages['status'] = None
1240 at_position = self.fb.check_position(0, 0, 0, 2)
1241 self.assertFalse(at_position)
1242 self.send_command_test_helper(
1243 exec_command,
1244 expected_command={
1245 'kind': 'read_status',
1246 'args': {},
1247 },
1248 extra_rpc_args={},
1249 mock_api_response={})
1251 def test_mount_tool(self):
1252 '''Test mount_tool command'''
1253 def exec_command():
1254 self.fb.mount_tool('Weeder')
1255 self.send_command_test_helper(
1256 exec_command,
1257 expected_command={
1258 'kind': 'lua',
1259 'args': {'lua': 'mount_tool("Weeder")'},
1260 },
1261 extra_rpc_args={},
1262 mock_api_response={})
1264 def test_dismount_tool(self):
1265 '''Test dismount_tool command'''
1266 def exec_command():
1267 self.fb.dismount_tool()
1268 self.send_command_test_helper(
1269 exec_command,
1270 expected_command={
1271 'kind': 'lua',
1272 'args': {'lua': 'dismount_tool()'},
1273 },
1274 extra_rpc_args={},
1275 mock_api_response={})
1277 def test_water(self):
1278 '''Test water command'''
1279 def exec_command():
1280 self.fb.water(123)
1281 self.send_command_test_helper(
1282 exec_command,
1283 expected_command={
1284 'kind': 'lua',
1285 'args': {'lua': '''plant = api({
1286 method = "GET",
1287 url = "/api/points/123"
1288 })
1289 water(plant)'''},
1290 },
1291 extra_rpc_args={},
1292 mock_api_response={})
1294 def test_dispense(self):
1295 '''Test dispense command'''
1296 def exec_command():
1297 self.fb.dispense(100, 'Weeder', 4)
1298 self.send_command_test_helper(
1299 exec_command,
1300 expected_command={
1301 'kind': 'lua',
1302 'args': {
1303 'lua': 'dispense(100, {tool_name = "Weeder", pin = 4})',
1304 },
1305 },
1306 extra_rpc_args={},
1307 mock_api_response={})
1309 @patch('requests.request')
1310 def helper_get_seed_tray_cell(self, *args, **kwargs):
1311 '''Test helper for get_seed_tray_cell command'''
1312 mock_request = args[0]
1313 tray_data = kwargs['tray_data']
1314 cell = kwargs['cell']
1315 expected_xyz = kwargs['expected_xyz']
1316 mock_response = Mock()
1317 mock_api_response = [
1318 {
1319 'id': 123,
1320 'name': 'Seed Tray',
1321 'pointer_type': '', # not an actual data field
1322 },
1323 {
1324 'pointer_type': 'ToolSlot',
1325 'pullout_direction': 1,
1326 'x': 0,
1327 'y': 0,
1328 'z': 0,
1329 'tool_id': 123,
1330 'name': '', # not an actual data field
1331 **tray_data,
1332 },
1333 ]
1334 mock_response.json.return_value = mock_api_response
1335 mock_response.status_code = 200
1336 mock_response.text = 'text'
1337 mock_request.return_value = mock_response
1338 cell = self.fb.get_seed_tray_cell('Seed Tray', cell)
1339 mock_request.assert_has_calls([
1340 call(
1341 'GET',
1342 'https://my.farm.bot/api/tools',
1343 headers={
1344 'authorization': 'encoded_token_value',
1345 'content-type': 'application/json',
1346 },
1347 json=None,
1348 ),
1349 call().json(),
1350 call(
1351 'GET',
1352 'https://my.farm.bot/api/points',
1353 headers={
1354 'authorization': 'encoded_token_value',
1355 'content-type': 'application/json',
1356 },
1357 json=None,
1358 ),
1359 call().json(),
1360 ])
1361 self.assertEqual(cell, expected_xyz, kwargs)
1363 def test_get_seed_tray_cell(self):
1364 '''Test get_seed_tray_cell'''
1365 test_cases = [
1366 {
1367 'tray_data': {'pullout_direction': 1},
1368 'cell': 'a1',
1369 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1370 },
1371 {
1372 'tray_data': {'pullout_direction': 1},
1373 'cell': 'b2',
1374 'expected_xyz': {'x': -11.25, 'y': -6.25, 'z': 0},
1375 },
1376 {
1377 'tray_data': {'pullout_direction': 1},
1378 'cell': 'd4',
1379 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1380 },
1381 {
1382 'tray_data': {'pullout_direction': 2},
1383 'cell': 'a1',
1384 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0},
1385 },
1386 {
1387 'tray_data': {'pullout_direction': 2},
1388 'cell': 'b2',
1389 'expected_xyz': {'x': -23.75, 'y': 6.25, 'z': 0},
1390 },
1391 {
1392 'tray_data': {'pullout_direction': 2},
1393 'cell': 'd4',
1394 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0},
1395 },
1396 {
1397 'tray_data': {'pullout_direction': 2, 'x': 100, 'y': 200, 'z': -100},
1398 'cell': 'd4',
1399 'expected_xyz': {'x': 101.25, 'y': 181.25, 'z': -100},
1400 },
1401 ]
1402 for test_case in test_cases:
1403 self.helper_get_seed_tray_cell(**test_case)
1405 @patch('requests.request')
1406 def helper_get_seed_tray_cell_error(self, *args, **kwargs):
1407 '''Test helper for get_seed_tray_cell command errors'''
1408 mock_request = args[0]
1409 tray_data = kwargs['tray_data']
1410 cell = kwargs['cell']
1411 error = kwargs['error']
1412 mock_response = Mock()
1413 mock_api_response = [
1414 {
1415 'id': 123,
1416 'name': 'Seed Tray',
1417 'pointer_type': '', # not an actual data field
1418 },
1419 {
1420 'pointer_type': 'ToolSlot',
1421 'pullout_direction': 1,
1422 'x': 0,
1423 'y': 0,
1424 'z': 0,
1425 'tool_id': 123,
1426 'name': '', # not an actual data field
1427 **tray_data,
1428 },
1429 ]
1430 mock_response.json.return_value = mock_api_response
1431 mock_response.status_code = 200
1432 mock_response.text = 'text'
1433 mock_request.return_value = mock_response
1434 with self.assertRaises(ValueError) as cm:
1435 self.fb.get_seed_tray_cell('Seed Tray', cell)
1436 self.assertEqual(cm.exception.args[0], error)
1437 mock_request.assert_has_calls([
1438 call(
1439 'GET',
1440 'https://my.farm.bot/api/tools',
1441 headers={
1442 'authorization': 'encoded_token_value',
1443 'content-type': 'application/json',
1444 },
1445 json=None,
1446 ),
1447 call().json(),
1448 call(
1449 'GET',
1450 'https://my.farm.bot/api/points',
1451 headers={
1452 'authorization': 'encoded_token_value',
1453 'content-type': 'application/json',
1454 },
1455 json=None,
1456 ),
1457 call().json(),
1458 ])
1460 def test_get_seed_tray_cell_invalid_cell_name(self):
1461 '''Test get_seed_tray_cell: invalid cell name'''
1462 self.helper_get_seed_tray_cell_error(
1463 tray_data={},
1464 cell='e4',
1465 error='Seed Tray Cell must be one of **A1** through **D4**',
1466 )
1468 def test_get_seed_tray_cell_invalid_pullout_direction(self):
1469 '''Test get_seed_tray_cell: invalid pullout direction'''
1470 self.helper_get_seed_tray_cell_error(
1471 tray_data={'pullout_direction': 0},
1472 cell='d4',
1473 error='Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`',
1474 )
1476 @patch('requests.request')
1477 def test_get_seed_tray_cell_no_tray(self, mock_request):
1478 '''Test get_seed_tray_cell: no seed tray'''
1479 mock_response = Mock()
1480 mock_api_response = []
1481 mock_response.json.return_value = mock_api_response
1482 mock_response.status_code = 200
1483 mock_response.text = 'text'
1484 mock_request.return_value = mock_response
1485 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1486 mock_request.assert_has_calls([
1487 call(
1488 'GET',
1489 'https://my.farm.bot/api/tools',
1490 headers={
1491 'authorization': 'encoded_token_value',
1492 'content-type': 'application/json',
1493 },
1494 json=None,
1495 ),
1496 call().json(),
1497 ])
1498 self.assertIsNone(result)
1500 @patch('requests.request')
1501 def test_get_seed_tray_cell_not_mounted(self, mock_request):
1502 '''Test get_seed_tray_cell: seed tray not mounted'''
1503 mock_response = Mock()
1504 mock_api_response = [{
1505 'id': 123,
1506 'name': 'Seed Tray',
1507 'pointer_type': '', # not an actual data field,
1508 }]
1509 mock_response.json.return_value = mock_api_response
1510 mock_response.status_code = 200
1511 mock_response.text = 'text'
1512 mock_request.return_value = mock_response
1513 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1')
1514 mock_request.assert_has_calls([
1515 call(
1516 'GET',
1517 'https://my.farm.bot/api/tools',
1518 headers={
1519 'authorization': 'encoded_token_value',
1520 'content-type': 'application/json',
1521 },
1522 json=None,
1523 ),
1524 call().json(),
1525 ])
1526 self.assertIsNone(result)
1528 def test_get_job_one(self):
1529 '''Test get_job command: get one job'''
1530 def exec_command():
1531 self.fb.state.last_messages['status'] = {
1532 'jobs': {
1533 'job name': {'status': 'working'},
1534 },
1535 }
1536 job = self.fb.get_job('job name')
1537 self.assertEqual(job, {'status': 'working'})
1538 self.send_command_test_helper(
1539 exec_command,
1540 expected_command={
1541 'kind': 'read_status',
1542 'args': {},
1543 },
1544 extra_rpc_args={},
1545 mock_api_response={})
1547 def test_get_job_all(self):
1548 '''Test get_job command: get all jobs'''
1549 def exec_command():
1550 self.fb.state.last_messages['status'] = {
1551 'jobs': {
1552 'job name': {'status': 'working'},
1553 },
1554 }
1555 jobs = self.fb.get_job()
1556 self.assertEqual(jobs, {'job name': {'status': 'working'}})
1557 self.send_command_test_helper(
1558 exec_command,
1559 expected_command={
1560 'kind': 'read_status',
1561 'args': {},
1562 },
1563 extra_rpc_args={},
1564 mock_api_response={})
1566 def test_get_job_no_status(self):
1567 '''Test get_job command: no status'''
1568 def exec_command():
1569 self.fb.state.last_messages['status'] = None
1570 job = self.fb.get_job('job name')
1571 self.assertIsNone(job)
1572 self.send_command_test_helper(
1573 exec_command,
1574 expected_command={
1575 'kind': 'read_status',
1576 'args': {},
1577 },
1578 extra_rpc_args={},
1579 mock_api_response={})
1581 def test_set_job(self):
1582 '''Test set_job command'''
1583 def exec_command():
1584 self.fb.set_job('job name', 'working', 50)
1585 self.send_command_test_helper(
1586 exec_command,
1587 expected_command={
1588 'kind': 'lua',
1589 'args': {'lua': '''local job_name = "job name"
1590 set_job(job_name)
1592 -- Update the job's status and percent:
1593 set_job(job_name, {
1594 status = "working",
1595 percent = 50
1596 })'''},
1597 },
1598 extra_rpc_args={},
1599 mock_api_response={})
1601 def test_complete_job(self):
1602 '''Test complete_job command'''
1603 def exec_command():
1604 self.fb.complete_job('job name')
1605 self.send_command_test_helper(
1606 exec_command,
1607 expected_command={
1608 'kind': 'lua',
1609 'args': {'lua': 'complete_job("job name")'},
1610 },
1611 extra_rpc_args={},
1612 mock_api_response={})
1614 def test_lua(self):
1615 '''Test lua command'''
1616 def exec_command():
1617 self.fb.lua('return true')
1618 self.send_command_test_helper(
1619 exec_command,
1620 expected_command={
1621 'kind': 'lua',
1622 'args': {'lua': 'return true'},
1623 },
1624 extra_rpc_args={},
1625 mock_api_response={})
1627 def test_if_statement(self):
1628 '''Test if_statement command'''
1629 def exec_command():
1630 self.fb.if_statement('pin10', 'is', 0)
1631 self.send_command_test_helper(
1632 exec_command,
1633 expected_command={
1634 'kind': '_if',
1635 'args': {
1636 'lhs': 'pin10',
1637 'op': 'is',
1638 'rhs': 0,
1639 '_then': {'kind': 'nothing', 'args': {}},
1640 '_else': {'kind': 'nothing', 'args': {}},
1641 }
1642 },
1643 extra_rpc_args={},
1644 mock_api_response=[])
1646 def test_if_statement_with_named_pin(self):
1647 '''Test if_statement command with named pin'''
1648 def exec_command():
1649 self.fb.if_statement('Lights', 'is', 0, named_pin_type='Peripheral')
1650 self.send_command_test_helper(
1651 exec_command,
1652 expected_command={
1653 'kind': '_if',
1654 'args': {
1655 'lhs': {
1656 'kind': 'named_pin',
1657 'args': {'pin_type': 'Peripheral', 'pin_id': 123},
1658 },
1659 'op': 'is',
1660 'rhs': 0,
1661 '_then': {'kind': 'nothing', 'args': {}},
1662 '_else': {'kind': 'nothing', 'args': {}},
1663 }
1664 },
1665 extra_rpc_args={},
1666 mock_api_response=[{'id': 123, 'label': 'Lights', 'mode': 0}])
1668 def test_if_statement_with_named_pin_not_found(self):
1669 '''Test if_statement command: named pin not found'''
1670 def exec_command():
1671 self.fb.if_statement('Lights', 'is', 0, named_pin_type='Peripheral')
1672 self.send_command_test_helper(
1673 exec_command,
1674 expected_command=None,
1675 extra_rpc_args={},
1676 mock_api_response=[{'label': 'Pump'}])
1677 self.assertEqual(self.fb.state.error, "ERROR: 'Lights' not in peripherals: ['Pump'].")
1679 def test_if_statement_with_sequences(self):
1680 '''Test if_statement command with sequences'''
1681 def exec_command():
1682 self.fb.if_statement('pin10', '<', 0, 'Watering Sequence', 'Drying Sequence')
1683 self.send_command_test_helper(
1684 exec_command,
1685 expected_command={
1686 'kind': '_if',
1687 'args': {
1688 'lhs': 'pin10',
1689 'op': '<',
1690 'rhs': 0,
1691 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}},
1692 '_else': {'kind': 'execute', 'args': {'sequence_id': 456}},
1693 }
1694 },
1695 extra_rpc_args={},
1696 mock_api_response=[
1697 {'id': 123, 'name': 'Watering Sequence'},
1698 {'id': 456, 'name': 'Drying Sequence'},
1699 ])
1701 def test_if_statement_with_sequence_not_found(self):
1702 '''Test if_statement command: sequence not found'''
1703 def exec_command():
1704 self.fb.if_statement('pin10', '<', 0, 'Watering Sequence', 'Drying Sequence')
1705 self.send_command_test_helper(
1706 exec_command,
1707 expected_command=None,
1708 extra_rpc_args={},
1709 mock_api_response=[])
1710 self.assertEqual(self.fb.state.error, "ERROR: 'Watering Sequence' not in sequences: [].")
1712 def test_if_statement_invalid_operator(self):
1713 '''Test if_statement command: invalid operator'''
1714 def exec_command():
1715 with self.assertRaises(ValueError) as cm:
1716 self.fb.if_statement('pin10', 'nope', 0)
1717 self.assertEqual(
1718 cm.exception.args[0],
1719 "Invalid operator: nope not in ['<', '>', 'is', 'not', 'is_undefined']")
1720 self.send_command_test_helper(
1721 exec_command,
1722 expected_command=None,
1723 extra_rpc_args={},
1724 mock_api_response=[])
1726 def test_if_statement_invalid_variable(self):
1727 '''Test if_statement command: invalid variable'''
1728 variables = ["x", "y", "z", *[f"pin{str(i)}" for i in range(70)]]
1729 def exec_command():
1730 with self.assertRaises(ValueError) as cm:
1731 self.fb.if_statement('nope', '<', 0)
1732 self.assertEqual(
1733 cm.exception.args[0],
1734 f"Invalid variable: nope not in {variables}")
1735 self.send_command_test_helper(
1736 exec_command,
1737 expected_command=None,
1738 extra_rpc_args={},
1739 mock_api_response=[])
1741 def test_if_statement_invalid_named_pin_type(self):
1742 '''Test if_statement command: invalid named pin type'''
1743 def exec_command():
1744 with self.assertRaises(ValueError) as cm:
1745 self.fb.if_statement('pin10', '<', 0, named_pin_type='nope')
1746 self.assertEqual(
1747 cm.exception.args[0],
1748 "Invalid named_pin_type: nope not in ['Peripheral', 'Sensor']")
1749 self.send_command_test_helper(
1750 exec_command,
1751 expected_command=None,
1752 extra_rpc_args={},
1753 mock_api_response=[])
1755 @staticmethod
1756 def helper_get_print_strings(mock_print):
1757 '''Test helper to get print call strings.'''
1758 return [string[1][0] for string in mock_print.mock_calls if len(string[1]) > 0]
1760 @patch('builtins.print')
1761 def test_print_status(self, mock_print):
1762 '''Test print_status.'''
1763 self.fb.set_verbosity(0)
1764 self.fb.state.print_status(description="testing")
1765 mock_print.assert_not_called()
1766 self.fb.set_verbosity(1)
1767 self.fb.state.print_status(description="testing")
1768 call_strings = self.helper_get_print_strings(mock_print)
1769 self.assertIn('testing', call_strings)
1770 mock_print.reset_mock()
1771 self.fb.set_verbosity(2)
1772 self.fb.state.print_status(endpoint_json=["testing"])
1773 call_strings = self.helper_get_print_strings(mock_print)
1774 call_strings = [s.split('(')[0].strip('`') for s in call_strings]
1775 self.assertIn('[\n "testing"\n]', call_strings)
1776 self.assertIn('test_print_status', call_strings)