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