Coverage for tests/tests_main.py: 100%

731 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-09-01 11:52 -0700

1''' 

2Farmbot class unit tests. 

3''' 

4 

5import json 

6import unittest 

7from unittest.mock import Mock, patch, call 

8import requests 

9 

10from farmbot_sidecar_starter_pack import Farmbot 

11 

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} 

22 

23TOKEN_REQUEST_KWARGS = { 

24 'headers': {'content-type': 'application/json'}, 

25 'timeout': 0, 

26} 

27 

28REQUEST_KWARGS_WITH_PAYLOAD = { 

29 'headers': { 

30 'authorization': 'encoded_token_value', 

31 'content-type': 'application/json' 

32 }, 

33 'timeout': 0, 

34} 

35 

36REQUEST_KWARGS = { 

37 **REQUEST_KWARGS_WITH_PAYLOAD, 

38 'json': None, 

39} 

40 

41class TestFarmbot(unittest.TestCase): 

42 '''Farmbot tests''' 

43 

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 

52 @patch('requests.post') 

53 def test_get_token_default_server(self, mock_post): 

54 '''POSITIVE TEST: function called with email, password, and default server''' 

55 mock_response = Mock() 

56 expected_token = {'token': 'abc123'} 

57 mock_response.json.return_value = expected_token 

58 mock_response.status_code = 200 

59 mock_response.text = 'text' 

60 mock_post.return_value = mock_response 

61 self.fb.set_token(None) 

62 # Call with default server 

63 self.fb.get_token('test_email@gmail.com', 'test_pass_123') 

64 mock_post.assert_called_once_with( 

65 'https://my.farm.bot/api/tokens', 

66 **TOKEN_REQUEST_KWARGS, 

67 json={'user': {'email': 'test_email@gmail.com', 

68 'password': 'test_pass_123'}}, 

69 ) 

70 self.assertEqual(self.fb.state.token, expected_token) 

71 

72 @patch('requests.post') 

73 def test_get_token_custom_server(self, mock_post): 

74 '''POSITIVE TEST: function called with email, password, and custom server''' 

75 mock_response = Mock() 

76 expected_token = {'token': 'abc123'} 

77 mock_response.json.return_value = expected_token 

78 mock_response.status_code = 200 

79 mock_response.text = 'text' 

80 mock_post.return_value = mock_response 

81 self.fb.set_token(None) 

82 # Call with custom server 

83 self.fb.get_token('test_email@gmail.com', 'test_pass_123', 

84 'https://staging.farm.bot') 

85 mock_post.assert_called_once_with( 

86 'https://staging.farm.bot/api/tokens', 

87 **TOKEN_REQUEST_KWARGS, 

88 json={'user': {'email': 'test_email@gmail.com', 

89 'password': 'test_pass_123'}}, 

90 ) 

91 self.assertEqual(self.fb.state.token, expected_token) 

92 

93 @patch('requests.post') 

94 def helper_get_token_errors(self, *args, **kwargs): 

95 '''Test helper for get_token errors''' 

96 mock_post = args[0] 

97 status_code = kwargs['status_code'] 

98 error_msg = kwargs['error_msg'] 

99 mock_response = Mock() 

100 mock_response.status_code = status_code 

101 mock_post.return_value = mock_response 

102 self.fb.set_token(None) 

103 self.fb.get_token('email@gmail.com', 'test_pass_123') 

104 mock_post.assert_called_once_with( 

105 'https://my.farm.bot/api/tokens', 

106 **TOKEN_REQUEST_KWARGS, 

107 json={'user': {'email': 'email@gmail.com', 

108 'password': 'test_pass_123'}}, 

109 ) 

110 self.assertEqual(self.fb.state.error, error_msg) 

111 self.assertIsNone(self.fb.state.token) 

112 

113 def test_get_token_bad_email(self): 

114 '''NEGATIVE TEST: function called with incorrect email''' 

115 self.helper_get_token_errors( 

116 status_code=422, 

117 error_msg='HTTP ERROR: Incorrect email address or password.', 

118 ) 

119 

120 def test_get_token_bad_server(self): 

121 '''NEGATIVE TEST: function called with incorrect server''' 

122 self.helper_get_token_errors( 

123 status_code=404, 

124 error_msg='HTTP ERROR: The server address does not exist.', 

125 ) 

126 

127 def test_get_token_other_error(self): 

128 '''get_token: other error''' 

129 self.helper_get_token_errors( 

130 status_code=500, 

131 error_msg='HTTP ERROR: Unexpected status code 500', 

132 ) 

133 

134 @patch('requests.post') 

135 def helper_get_token_exceptions(self, *args, **kwargs): 

136 '''Test helper for get_token exceptions''' 

137 mock_post = args[0] 

138 exception = kwargs['exception'] 

139 error_msg = kwargs['error_msg'] 

140 mock_post.side_effect = exception 

141 self.fb.set_token(None) 

142 self.fb.get_token('email@gmail.com', 'test_pass_123') 

143 mock_post.assert_called_once_with( 

144 'https://my.farm.bot/api/tokens', 

145 **TOKEN_REQUEST_KWARGS, 

146 json={'user': {'email': 'email@gmail.com', 

147 'password': 'test_pass_123'}}, 

148 ) 

149 self.assertEqual(self.fb.state.error, error_msg) 

150 self.assertIsNone(self.fb.state.token) 

151 

152 def test_get_token_server_not_found(self): 

153 '''get_token: server not found''' 

154 self.helper_get_token_exceptions( 

155 exception=requests.exceptions.ConnectionError, 

156 error_msg='DNS ERROR: The server address does not exist.', 

157 ) 

158 

159 def test_get_token_timeout(self): 

160 '''get_token: timeout''' 

161 self.helper_get_token_exceptions( 

162 exception=requests.exceptions.Timeout, 

163 error_msg='DNS ERROR: The request timed out.', 

164 ) 

165 

166 def test_get_token_problem(self): 

167 '''get_token: problem''' 

168 self.helper_get_token_exceptions( 

169 exception=requests.exceptions.RequestException, 

170 error_msg='DNS ERROR: There was a problem with the request.', 

171 ) 

172 

173 def test_get_token_other_exception(self): 

174 '''get_token: other exception''' 

175 self.helper_get_token_exceptions( 

176 exception=Exception('other'), 

177 error_msg='DNS ERROR: An unexpected error occurred: other', 

178 ) 

179 

180 @patch('requests.request') 

181 def helper_api_get_error(self, *args, **kwargs): 

182 '''Test helper for api_get errors''' 

183 mock_request = args[0] 

184 status_code = kwargs['status_code'] 

185 error_msg = kwargs['error_msg'] 

186 mock_response = Mock() 

187 mock_response.status_code = status_code 

188 mock_response.reason = 'reason' 

189 mock_response.text = 'text' 

190 mock_response.json.return_value = {'error': 'error'} 

191 mock_request.return_value = mock_response 

192 response = self.fb.api_get('device') 

193 mock_request.assert_called_once_with( 

194 'GET', 

195 'https://my.farm.bot/api/device', 

196 **REQUEST_KWARGS, 

197 ) 

198 self.assertEqual(response, error_msg) 

199 

200 def test_api_get_errors(self): 

201 '''Test api_get errors''' 

202 self.helper_api_get_error( 

203 status_code=404, 

204 error_msg='CLIENT ERROR 404: The specified endpoint does not exist. ({\n "error": "error"\n})', 

205 ) 

206 self.helper_api_get_error( 

207 status_code=500, 

208 error_msg='SERVER ERROR 500: text ({\n "error": "error"\n})', 

209 ) 

210 self.helper_api_get_error( 

211 status_code=600, 

212 error_msg='UNEXPECTED ERROR 600: text ({\n "error": "error"\n})', 

213 ) 

214 

215 @patch('requests.request') 

216 def test_api_string_error_response_handling(self, mock_request): 

217 '''Test API string response errors''' 

218 mock_response = Mock() 

219 mock_response.status_code = 404 

220 mock_response.reason = 'reason' 

221 mock_response.text = 'error string' 

222 mock_response.json.side_effect = requests.exceptions.JSONDecodeError('', '', 0) 

223 mock_request.return_value = mock_response 

224 response = self.fb.api_get('device') 

225 mock_request.assert_called_once_with( 

226 'GET', 

227 'https://my.farm.bot/api/device', 

228 **REQUEST_KWARGS, 

229 ) 

230 self.assertEqual(response, 'CLIENT ERROR 404: The specified endpoint does not exist. (error string)') 

231 

232 @patch('requests.request') 

233 def test_api_string_error_response_handling_html(self, mock_request): 

234 '''Test API html string response errors''' 

235 mock_response = Mock() 

236 mock_response.status_code = 404 

237 mock_response.reason = 'reason' 

238 mock_response.text = '<html><h1>error0</h1><h2>error1</h2></html>' 

239 mock_response.json.side_effect = requests.exceptions.JSONDecodeError('', '', 0) 

240 mock_request.return_value = mock_response 

241 response = self.fb.api_get('device') 

242 mock_request.assert_called_once_with( 

243 'GET', 

244 'https://my.farm.bot/api/device', 

245 **REQUEST_KWARGS, 

246 ) 

247 self.assertEqual(response, 'CLIENT ERROR 404: The specified endpoint does not exist. (error0 error1)') 

248 

249 @patch('requests.request') 

250 def test_api_get_endpoint_only(self, mock_request): 

251 '''POSITIVE TEST: function called with endpoint only''' 

252 mock_response = Mock() 

253 expected_response = {'device': 'info'} 

254 mock_response.json.return_value = expected_response 

255 mock_response.status_code = 200 

256 mock_response.text = 'text' 

257 mock_request.return_value = mock_response 

258 # Call with endpoint only 

259 response = self.fb.api_get('device') 

260 mock_request.assert_called_once_with( 

261 'GET', 

262 'https://my.farm.bot/api/device', 

263 **REQUEST_KWARGS, 

264 ) 

265 self.assertEqual(response, expected_response) 

266 

267 @patch('requests.request') 

268 def test_api_get_with_id(self, mock_request): 

269 '''POSITIVE TEST: function called with valid ID''' 

270 mock_response = Mock() 

271 expected_response = {'peripheral': 'info'} 

272 mock_response.json.return_value = expected_response 

273 mock_response.status_code = 200 

274 mock_response.text = 'text' 

275 mock_request.return_value = mock_response 

276 # Call with specific ID 

277 response = self.fb.api_get('peripherals', '12345') 

278 mock_request.assert_called_once_with( 

279 'GET', 

280 'https://my.farm.bot/api/peripherals/12345', 

281 **REQUEST_KWARGS, 

282 ) 

283 self.assertEqual(response, expected_response) 

284 

285 @patch('requests.request') 

286 def test_check_token_api_request(self, mock_request): 

287 '''Test check_token: API request''' 

288 self.fb.set_token(None) 

289 with self.assertRaises(ValueError) as cm: 

290 self.fb.api_get('points') 

291 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR) 

292 mock_request.assert_not_called() 

293 self.assertEqual(self.fb.state.error, self.fb.state.NO_TOKEN_ERROR) 

294 

295 @patch('paho.mqtt.client.Client') 

296 @patch('requests.request') 

297 def test_check_token_broker(self, mock_request, mock_mqtt): 

298 '''Test check_token: broker''' 

299 mock_client = Mock() 

300 mock_mqtt.return_value = mock_client 

301 self.fb.set_token(None) 

302 with self.assertRaises(ValueError) as cm: 

303 self.fb.on(123) 

304 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR) 

305 with self.assertRaises(ValueError) as cm: 

306 self.fb.read_sensor(123) 

307 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR) 

308 with self.assertRaises(ValueError) as cm: 

309 self.fb.get_xyz() 

310 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR) 

311 with self.assertRaises(ValueError) as cm: 

312 self.fb.read_status() 

313 self.assertEqual(cm.exception.args[0], self.fb.state.NO_TOKEN_ERROR) 

314 mock_request.assert_not_called() 

315 mock_client.publish.assert_not_called() 

316 self.assertEqual(self.fb.state.error, self.fb.state.NO_TOKEN_ERROR) 

317 

318 @patch('paho.mqtt.client.Client') 

319 def test_publish_disabled(self, mock_mqtt): 

320 '''Test publish disabled''' 

321 mock_client = Mock() 

322 mock_mqtt.return_value = mock_client 

323 self.fb.state.dry_run = True 

324 self.fb.on(123) 

325 mock_client.publish.assert_not_called() 

326 

327 @patch('requests.request') 

328 def test_api_patch(self, mock_request): 

329 '''test api_patch function''' 

330 mock_response = Mock() 

331 mock_response.status_code = 200 

332 mock_response.text = 'text' 

333 mock_response.json.return_value = {'name': 'new name'} 

334 mock_request.return_value = mock_response 

335 device_info = self.fb.api_patch('device', {'name': 'new name'}) 

336 mock_request.assert_has_calls([call( 

337 'PATCH', 

338 'https://my.farm.bot/api/device', 

339 **REQUEST_KWARGS_WITH_PAYLOAD, 

340 json={'name': 'new name'}, 

341 ), 

342 call().json(), 

343 ]) 

344 self.assertEqual(device_info, {'name': 'new name'}) 

345 

346 @patch('requests.request') 

347 def test_api_post(self, mock_request): 

348 '''test api_post function''' 

349 mock_response = Mock() 

350 mock_response.status_code = 200 

351 mock_response.text = 'text' 

352 mock_response.json.return_value = {'name': 'new name'} 

353 mock_request.return_value = mock_response 

354 point = self.fb.api_post('points', {'name': 'new name'}) 

355 mock_request.assert_has_calls([call( 

356 'POST', 

357 'https://my.farm.bot/api/points', 

358 **REQUEST_KWARGS_WITH_PAYLOAD, 

359 json={'name': 'new name'}, 

360 ), 

361 call().json(), 

362 ]) 

363 self.assertEqual(point, {'name': 'new name'}) 

364 

365 @patch('requests.request') 

366 def test_api_delete(self, mock_request): 

367 '''test api_delete function''' 

368 mock_response = Mock() 

369 mock_response.status_code = 200 

370 mock_response.text = 'text' 

371 mock_response.json.return_value = {'name': 'deleted'} 

372 mock_request.return_value = mock_response 

373 result = self.fb.api_delete('points', 12345) 

374 mock_request.assert_called_once_with( 

375 'DELETE', 

376 'https://my.farm.bot/api/points/12345', 

377 **REQUEST_KWARGS, 

378 ) 

379 self.assertEqual(result, {'name': 'deleted'}) 

380 

381 @patch('requests.request') 

382 def test_api_delete_requests_disabled(self, mock_request): 

383 '''test api_delete function: requests disabled''' 

384 self.fb.state.dry_run = True 

385 result = self.fb.api_delete('points', 12345) 

386 mock_request.assert_not_called() 

387 self.assertEqual(result, {"edit_requests_disabled": True}) 

388 

389 @patch('requests.request') 

390 def test_group_one(self, mock_request): 

391 '''test group function: get one group''' 

392 mock_response = Mock() 

393 mock_response.json.return_value = {'name': 'Group 0'} 

394 mock_response.status_code = 200 

395 mock_response.text = 'text' 

396 mock_request.return_value = mock_response 

397 group_info = self.fb.group(12345) 

398 mock_request.assert_called_once_with( 

399 'GET', 

400 'https://my.farm.bot/api/point_groups/12345', 

401 **REQUEST_KWARGS, 

402 ) 

403 self.assertEqual(group_info, {'name': 'Group 0'}) 

404 

405 @patch('requests.request') 

406 def test_group_all(self, mock_request): 

407 '''test group function: get all groups''' 

408 mock_response = Mock() 

409 mock_response.json.return_value = [{'name': 'Group 0'}] 

410 mock_response.status_code = 200 

411 mock_response.text = 'text' 

412 mock_request.return_value = mock_response 

413 group_info = self.fb.group() 

414 mock_request.assert_called_once_with( 

415 'GET', 

416 'https://my.farm.bot/api/point_groups', 

417 **REQUEST_KWARGS, 

418 ) 

419 self.assertEqual(group_info, [{'name': 'Group 0'}]) 

420 

421 @patch('requests.request') 

422 def test_curve_one(self, mock_request): 

423 '''test curve function: get one curve''' 

424 mock_response = Mock() 

425 mock_response.json.return_value = {'name': 'Curve 0'} 

426 mock_response.status_code = 200 

427 mock_response.text = 'text' 

428 mock_request.return_value = mock_response 

429 curve_info = self.fb.curve(12345) 

430 mock_request.assert_called_once_with( 

431 'GET', 

432 'https://my.farm.bot/api/curves/12345', 

433 **REQUEST_KWARGS, 

434 ) 

435 self.assertEqual(curve_info, {'name': 'Curve 0'}) 

436 

437 @patch('requests.request') 

438 def test_curve_all(self, mock_request): 

439 '''test curve function: get all curves''' 

440 mock_response = Mock() 

441 mock_response.json.return_value = [{'name': 'Curve 0'}] 

442 mock_response.status_code = 200 

443 mock_response.text = 'text' 

444 mock_request.return_value = mock_response 

445 curve_info = self.fb.curve() 

446 mock_request.assert_called_once_with( 

447 'GET', 

448 'https://my.farm.bot/api/curves', 

449 **REQUEST_KWARGS, 

450 ) 

451 self.assertEqual(curve_info, [{'name': 'Curve 0'}]) 

452 

453 @patch('requests.request') 

454 def test_safe_z(self, mock_request): 

455 '''test safe_z function''' 

456 mock_response = Mock() 

457 mock_response.json.return_value = {'safe_height': 100} 

458 mock_response.status_code = 200 

459 mock_response.text = 'text' 

460 mock_request.return_value = mock_response 

461 safe_height = self.fb.safe_z() 

462 mock_request.assert_called_once_with( 

463 'GET', 

464 'https://my.farm.bot/api/fbos_config', 

465 **REQUEST_KWARGS, 

466 ) 

467 self.assertEqual(safe_height, 100) 

468 

469 @patch('requests.request') 

470 def test_garden_size(self, mock_request): 

471 '''test garden_size function''' 

472 mock_response = Mock() 

473 mock_response.json.return_value = { 

474 'movement_axis_nr_steps_x': 1000, 

475 'movement_axis_nr_steps_y': 2000, 

476 'movement_axis_nr_steps_z': 40000, 

477 'movement_step_per_mm_x': 5, 

478 'movement_step_per_mm_y': 5, 

479 'movement_step_per_mm_z': 25, 

480 } 

481 mock_response.status_code = 200 

482 mock_response.text = 'text' 

483 mock_request.return_value = mock_response 

484 garden_size = self.fb.garden_size() 

485 mock_request.assert_called_once_with( 

486 'GET', 

487 'https://my.farm.bot/api/firmware_config', 

488 **REQUEST_KWARGS, 

489 ) 

490 self.assertEqual(garden_size, {'x': 200, 'y': 400, 'z': 1600}) 

491 

492 @patch('requests.request') 

493 def test_log(self, mock_request): 

494 '''test log function''' 

495 mock_response = Mock() 

496 mock_response.status_code = 200 

497 mock_response.text = 'text' 

498 mock_response.json.return_value = {'message': 'test message'} 

499 mock_request.return_value = mock_response 

500 self.fb.log('test message', 'info', 'toast') 

501 mock_request.assert_called_once_with( 

502 'POST', 

503 'https://my.farm.bot/api/logs', 

504 **REQUEST_KWARGS_WITH_PAYLOAD, 

505 json={ 

506 'message': 'test message', 

507 'type': 'info', 

508 'channels': ['toast'], 

509 }, 

510 ) 

511 

512 @patch('paho.mqtt.client.Client') 

513 def test_connect_broker(self, mock_mqtt): 

514 '''Test test_connect_broker command''' 

515 mock_client = Mock() 

516 mock_mqtt.return_value = mock_client 

517 self.fb.connect_broker() 

518 mock_client.username_pw_set.assert_called_once_with( 

519 username='device_0', 

520 password='encoded_token_value') 

521 mock_client.connect.assert_called_once_with( 

522 'mqtt_url', 

523 port=1883, 

524 keepalive=60) 

525 mock_client.loop_start.assert_called() 

526 

527 def test_disconnect_broker(self): 

528 '''Test disconnect_broker command''' 

529 mock_client = Mock() 

530 self.fb.broker.client = mock_client 

531 self.fb.disconnect_broker() 

532 mock_client.loop_stop.assert_called_once() 

533 mock_client.disconnect.assert_called_once() 

534 

535 @patch('paho.mqtt.client.Client') 

536 def test_listen(self, mock_mqtt): 

537 '''Test listen command''' 

538 mock_client = Mock() 

539 mock_mqtt.return_value = mock_client 

540 self.fb.listen() 

541 

542 class MockMessage: 

543 '''Mock message class''' 

544 topic = 'topic' 

545 payload = '{"message": "test message"}' 

546 mock_client.on_message('', '', MockMessage()) 

547 mock_client.username_pw_set.assert_called_once_with( 

548 username='device_0', 

549 password='encoded_token_value') 

550 mock_client.connect.assert_called_once_with( 

551 'mqtt_url', 

552 port=1883, 

553 keepalive=60) 

554 mock_client.subscribe.assert_called_once_with('bot/device_0/#') 

555 mock_client.loop_start.assert_called() 

556 mock_client.loop_stop.assert_called() 

557 

558 @patch('paho.mqtt.client.Client') 

559 def test_listen_clear_last(self, mock_mqtt): 

560 '''Test listen command: clear last message''' 

561 mock_client = Mock() 

562 mock_mqtt.return_value = mock_client 

563 self.fb.state.last_messages = {'#': "message"} 

564 self.fb.state.test_env = False 

565 self.fb.listen() 

566 self.assertIsNone(self.fb.state.last_messages['#']) 

567 

568 @patch('paho.mqtt.client.Client') 

569 def test_publish_apply_label(self, mock_mqtt): 

570 '''Test publish command: set uuid''' 

571 mock_client = Mock() 

572 mock_mqtt.return_value = mock_client 

573 self.fb.state.test_env = False 

574 self.fb.publish({'kind': 'sync', 'args': {}}) 

575 self.assertNotIn(self.fb.state.last_published.get('args', {}).get('label'), ['test', '', None]) 

576 

577 @patch('requests.request') 

578 @patch('paho.mqtt.client.Client') 

579 def send_command_test_helper(self, *args, **kwargs): 

580 '''Helper for testing command execution''' 

581 execute_command = args[0] 

582 mock_mqtt = args[1] 

583 mock_request = args[2] 

584 expected_command = kwargs.get('expected_command') 

585 extra_rpc_args = kwargs.get('extra_rpc_args') 

586 mock_api_response = kwargs.get('mock_api_response') 

587 error = kwargs.get('error') 

588 mock_client = Mock() 

589 mock_mqtt.return_value = mock_client 

590 mock_response = Mock() 

591 mock_response.json.return_value = mock_api_response 

592 mock_response.status_code = 200 

593 mock_response.text = 'text' 

594 mock_request.return_value = mock_response 

595 self.fb.state.last_messages['from_device'] = { 

596 'kind': 'rpc_error' if error else 'rpc_ok', 

597 'args': {'label': 'test'}, 

598 } 

599 execute_command() 

600 if expected_command is None: 

601 mock_client.publish.assert_not_called() 

602 return 

603 expected_payload = { 

604 'kind': 'rpc_request', 

605 'args': {'label': 'test', **extra_rpc_args}, 

606 'body': [expected_command], 

607 } 

608 mock_client.username_pw_set.assert_called_once_with( 

609 username='device_0', 

610 password='encoded_token_value') 

611 mock_client.connect.assert_called_once_with( 

612 'mqtt_url', 

613 port=1883, 

614 keepalive=60) 

615 mock_client.loop_start.assert_called() 

616 mock_client.publish.assert_called_once_with( 

617 'bot/device_0/from_clients', 

618 payload=json.dumps(expected_payload)) 

619 if not error: 

620 self.assertNotEqual(self.fb.state.error, 'RPC error response received.') 

621 

622 def test_message(self): 

623 '''Test message command''' 

624 def exec_command(): 

625 self.fb.message('test message', 'info') 

626 self.send_command_test_helper( 

627 exec_command, 

628 expected_command={ 

629 'kind': 'send_message', 

630 'args': {'message': 'test message', 'message_type': 'info'}, 

631 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}], 

632 }, 

633 extra_rpc_args={}, 

634 mock_api_response={}) 

635 

636 def test_debug(self): 

637 '''Test debug command''' 

638 def exec_command(): 

639 self.fb.debug('test message') 

640 self.send_command_test_helper( 

641 exec_command, 

642 expected_command={ 

643 'kind': 'send_message', 

644 'args': {'message': 'test message', 'message_type': 'debug'}, 

645 'body': [{'kind': 'channel', 'args': {'channel_name': 'ticker'}}], 

646 }, 

647 extra_rpc_args={}, 

648 mock_api_response={}) 

649 

650 def test_toast(self): 

651 '''Test toast command''' 

652 def exec_command(): 

653 self.fb.toast('test message') 

654 self.send_command_test_helper( 

655 exec_command, 

656 expected_command={ 

657 'kind': 'send_message', 

658 'args': {'message': 'test message', 'message_type': 'info'}, 

659 'body': [{'kind': 'channel', 'args': {'channel_name': 'toast'}}], 

660 }, 

661 extra_rpc_args={}, 

662 mock_api_response={}) 

663 

664 def test_invalid_message_type(self): 

665 '''Test message_type validation''' 

666 def exec_command(): 

667 with self.assertRaises(ValueError) as cm: 

668 self.fb.message('test', message_type='nope') 

669 self.assertEqual( 

670 cm.exception.args[0], 

671 "Invalid message type: `nope` not in ['assertion', 'busy', 'debug', 'error', 'fun', 'info', 'success', 'warn']") 

672 self.send_command_test_helper( 

673 exec_command, 

674 expected_command=None, 

675 extra_rpc_args={}, 

676 mock_api_response={}) 

677 

678 def test_invalid_message_channel(self): 

679 '''Test message channel validation''' 

680 def exec_command(): 

681 with self.assertRaises(ValueError) as cm: 

682 self.fb.message('test', channel='nope') 

683 self.assertEqual( 

684 cm.exception.args[0], 

685 "Invalid channel: nope not in ['ticker', 'toast', 'email', 'espeak']") 

686 self.send_command_test_helper( 

687 exec_command, 

688 expected_command=None, 

689 extra_rpc_args={}, 

690 mock_api_response={}) 

691 

692 def test_read_status(self): 

693 '''Test read_status command''' 

694 def exec_command(): 

695 self.fb.read_status() 

696 self.send_command_test_helper( 

697 exec_command, 

698 expected_command={ 

699 'kind': 'read_status', 

700 'args': {}, 

701 }, 

702 extra_rpc_args={}, 

703 mock_api_response={}) 

704 

705 def test_read_sensor(self): 

706 '''Test read_sensor command''' 

707 def exec_command(): 

708 self.fb.read_sensor('Tool Verification') 

709 self.send_command_test_helper( 

710 exec_command, 

711 expected_command={ 

712 'kind': 'read_pin', 

713 'args': { 

714 'pin_mode': 0, 

715 'label': '---', 

716 'pin_number': { 

717 'kind': 'named_pin', 

718 'args': {'pin_type': 'Sensor', 'pin_id': 123}, 

719 }, 

720 }, 

721 }, 

722 extra_rpc_args={}, 

723 mock_api_response=[{'id': 123, 'label': 'Tool Verification', 'mode': 0}]) 

724 

725 def test_read_sensor_not_found(self): 

726 '''Test read_sensor command: sensor not found''' 

727 def exec_command(): 

728 self.fb.read_sensor('Temperature') 

729 self.send_command_test_helper( 

730 exec_command, 

731 expected_command=None, 

732 extra_rpc_args={}, 

733 mock_api_response=[{'label': 'Tool Verification'}]) 

734 self.assertEqual(self.fb.state.error, "ERROR: 'Temperature' not in sensors: ['Tool Verification'].") 

735 

736 def test_assertion(self): 

737 '''Test assertion command''' 

738 def exec_command(): 

739 self.fb.assertion('return true', 'abort') 

740 self.send_command_test_helper( 

741 exec_command, 

742 expected_command={ 

743 'kind': 'assertion', 

744 'args': { 

745 'assertion_type': 'abort', 

746 'lua': 'return true', 

747 '_then': {'kind': 'nothing', 'args': {}}, 

748 } 

749 }, 

750 extra_rpc_args={}, 

751 mock_api_response={}) 

752 

753 def test_assertion_with_recovery_sequence(self): 

754 '''Test assertion command with recovery sequence''' 

755 def exec_command(): 

756 self.fb.assertion('return true', 'abort', 'Recovery Sequence') 

757 self.send_command_test_helper( 

758 exec_command, 

759 expected_command={ 

760 'kind': 'assertion', 

761 'args': { 

762 'assertion_type': 'abort', 

763 'lua': 'return true', 

764 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}}, 

765 } 

766 }, 

767 extra_rpc_args={}, 

768 mock_api_response=[{'id': 123, 'name': 'Recovery Sequence'}]) 

769 

770 def test_assertion_recovery_sequence_not_found(self): 

771 '''Test assertion command: recovery sequence not found''' 

772 def exec_command(): 

773 self.fb.assertion('return true', 'abort', 'Recovery Sequence') 

774 self.send_command_test_helper( 

775 exec_command, 

776 expected_command=None, 

777 extra_rpc_args={}, 

778 mock_api_response=[]) 

779 self.assertEqual(self.fb.state.error, "ERROR: 'Recovery Sequence' not in sequences: [].") 

780 

781 def test_assertion_invalid_assertion_type(self): 

782 '''Test assertion command: invalid assertion type''' 

783 def exec_command(): 

784 with self.assertRaises(ValueError) as cm: 

785 self.fb.assertion('return true', 'nope') 

786 self.assertEqual( 

787 cm.exception.args[0], 

788 "Invalid assertion_type: nope not in ['abort', 'recover', 'abort_recover', 'continue']") 

789 self.send_command_test_helper( 

790 exec_command, 

791 expected_command=None, 

792 extra_rpc_args={}, 

793 mock_api_response={}) 

794 

795 def test_wait(self): 

796 '''Test wait command''' 

797 def exec_command(): 

798 self.fb.wait(123) 

799 self.send_command_test_helper( 

800 exec_command, 

801 expected_command={ 

802 'kind': 'wait', 

803 'args': {'milliseconds': 123}, 

804 }, 

805 extra_rpc_args={}, 

806 mock_api_response={}) 

807 

808 def test_unlock(self): 

809 '''Test unlock command''' 

810 def exec_command(): 

811 self.fb.unlock() 

812 self.send_command_test_helper( 

813 exec_command, 

814 expected_command={ 

815 'kind': 'emergency_unlock', 

816 'args': {}, 

817 }, 

818 extra_rpc_args={'priority': 9000}, 

819 mock_api_response={}) 

820 

821 def test_e_stop(self): 

822 '''Test e_stop command''' 

823 def exec_command(): 

824 self.fb.e_stop() 

825 self.send_command_test_helper( 

826 exec_command, 

827 expected_command={ 

828 'kind': 'emergency_lock', 

829 'args': {}, 

830 }, 

831 extra_rpc_args={'priority': 9000}, 

832 mock_api_response={}) 

833 

834 def test_find_home(self): 

835 '''Test find_home command''' 

836 def exec_command(): 

837 self.fb.find_home() 

838 self.send_command_test_helper( 

839 exec_command, 

840 expected_command={ 

841 'kind': 'find_home', 

842 'args': {'axis': 'all', 'speed': 100}, 

843 }, 

844 extra_rpc_args={}, 

845 mock_api_response={}) 

846 

847 def test_find_home_speed_error(self): 

848 '''Test find_home command: speed error''' 

849 def exec_command(): 

850 self.fb.find_home('all', 0) 

851 self.send_command_test_helper( 

852 exec_command, 

853 expected_command=None, 

854 extra_rpc_args={}, 

855 mock_api_response={}) 

856 self.assertEqual(self.fb.state.error, 'ERROR: Speed constrained to 1-100.') 

857 

858 def test_find_home_invalid_axis(self): 

859 '''Test find_home command: invalid axis''' 

860 def exec_command(): 

861 with self.assertRaises(ValueError) as cm: 

862 self.fb.find_home('nope') 

863 self.assertEqual( 

864 cm.exception.args[0], 

865 "Invalid axis: nope not in ['x', 'y', 'z', 'all']") 

866 self.send_command_test_helper( 

867 exec_command, 

868 expected_command=None, 

869 extra_rpc_args={}, 

870 mock_api_response={}) 

871 

872 def test_set_home(self): 

873 '''Test set_home command''' 

874 def exec_command(): 

875 self.fb.set_home() 

876 self.send_command_test_helper( 

877 exec_command, 

878 expected_command={ 

879 'kind': 'zero', 

880 'args': {'axis': 'all'}, 

881 }, 

882 extra_rpc_args={}, 

883 mock_api_response={}) 

884 

885 def test_toggle_peripheral(self): 

886 '''Test toggle_peripheral command''' 

887 def exec_command(): 

888 self.fb.toggle_peripheral('New Peripheral') 

889 self.send_command_test_helper( 

890 exec_command, 

891 expected_command={ 

892 'kind': 'toggle_pin', 

893 'args': { 

894 'pin_number': { 

895 'kind': 'named_pin', 

896 'args': {'pin_type': 'Peripheral', 'pin_id': 123}, 

897 }, 

898 }, 

899 }, 

900 extra_rpc_args={}, 

901 mock_api_response=[{'label': 'New Peripheral', 'id': 123}]) 

902 

903 def test_toggle_peripheral_not_found(self): 

904 '''Test toggle_peripheral command: peripheral not found''' 

905 def exec_command(): 

906 self.fb.toggle_peripheral('New Peripheral') 

907 self.send_command_test_helper( 

908 exec_command, 

909 expected_command=None, 

910 extra_rpc_args={}, 

911 mock_api_response=[]) 

912 self.assertEqual(self.fb.state.error, 'ERROR: \'New Peripheral\' not in peripherals: [].') 

913 

914 def test_on_digital(self): 

915 '''Test on command: digital''' 

916 def exec_command(): 

917 self.fb.on(13) 

918 self.send_command_test_helper( 

919 exec_command, 

920 expected_command={ 

921 'kind': 'write_pin', 

922 'args': { 

923 'pin_value': 1, 

924 'pin_mode': 0, 

925 'pin_number': 13, 

926 }, 

927 }, 

928 extra_rpc_args={}, 

929 mock_api_response={}) 

930 

931 def test_off(self): 

932 '''Test off command''' 

933 def exec_command(): 

934 self.fb.off(13) 

935 self.send_command_test_helper( 

936 exec_command, 

937 expected_command={ 

938 'kind': 'write_pin', 

939 'args': { 

940 'pin_value': 0, 

941 'pin_mode': 0, 

942 'pin_number': 13, 

943 }, 

944 }, 

945 extra_rpc_args={}, 

946 mock_api_response={}) 

947 

948 def test_move(self): 

949 '''Test move command''' 

950 def exec_command(): 

951 self.fb.move(1, 2, 3) 

952 self.send_command_test_helper( 

953 exec_command, 

954 expected_command={ 

955 'kind': 'move', 

956 'args': {}, 

957 'body': [ 

958 {'kind': 'axis_overwrite', 'args': { 

959 'axis': 'x', 

960 'axis_operand': {'kind': 'numeric', 'args': {'number': 1}}}}, 

961 {'kind': 'axis_overwrite', 'args': { 

962 'axis': 'y', 

963 'axis_operand': {'kind': 'numeric', 'args': {'number': 2}}}}, 

964 {'kind': 'axis_overwrite', 'args': { 

965 'axis': 'z', 

966 'axis_operand': {'kind': 'numeric', 'args': {'number': 3}}}}, 

967 ], 

968 }, 

969 extra_rpc_args={}, 

970 mock_api_response={}) 

971 

972 def test_reboot(self): 

973 '''Test reboot command''' 

974 def exec_command(): 

975 self.fb.reboot() 

976 self.send_command_test_helper( 

977 exec_command, 

978 expected_command={ 

979 'kind': 'reboot', 

980 'args': {'package': 'farmbot_os'}, 

981 }, 

982 extra_rpc_args={}, 

983 mock_api_response={}) 

984 

985 def test_shutdown(self): 

986 '''Test shutdown command''' 

987 def exec_command(): 

988 self.fb.shutdown() 

989 self.send_command_test_helper( 

990 exec_command, 

991 expected_command={ 

992 'kind': 'power_off', 

993 'args': {}, 

994 }, 

995 extra_rpc_args={}, 

996 mock_api_response={}) 

997 

998 def test_find_axis_length(self): 

999 '''Test find_axis_length command''' 

1000 def exec_command(): 

1001 self.fb.find_axis_length() 

1002 self.send_command_test_helper( 

1003 exec_command, 

1004 expected_command={ 

1005 'kind': 'calibrate', 

1006 'args': {'axis': 'all'}, 

1007 }, 

1008 extra_rpc_args={}, 

1009 mock_api_response={}) 

1010 

1011 def test_control_peripheral(self): 

1012 '''Test control_peripheral command''' 

1013 def exec_command(): 

1014 self.fb.control_peripheral('New Peripheral', 1) 

1015 self.send_command_test_helper( 

1016 exec_command, 

1017 expected_command={ 

1018 'kind': 'write_pin', 

1019 'args': { 

1020 'pin_value': 1, 

1021 'pin_mode': 0, 

1022 'pin_number': { 

1023 'kind': 'named_pin', 

1024 'args': {'pin_type': 'Peripheral', 'pin_id': 123}, 

1025 }, 

1026 }, 

1027 }, 

1028 extra_rpc_args={}, 

1029 mock_api_response=[{'label': 'New Peripheral', 'mode': 0, 'id': 123}]) 

1030 

1031 def test_control_peripheral_not_found(self): 

1032 '''Test control_peripheral command: peripheral not found''' 

1033 def exec_command(): 

1034 self.fb.control_peripheral('New Peripheral', 1) 

1035 self.send_command_test_helper( 

1036 exec_command, 

1037 expected_command=None, 

1038 extra_rpc_args={}, 

1039 mock_api_response=[{'label': 'Pump'}, {'label': 'Lights'}]) 

1040 self.assertEqual(self.fb.state.error, "ERROR: 'New Peripheral' not in peripherals: ['Pump', 'Lights'].") 

1041 

1042 def test_measure_soil_height(self): 

1043 '''Test measure_soil_height command''' 

1044 def exec_command(): 

1045 self.fb.measure_soil_height() 

1046 self.send_command_test_helper( 

1047 exec_command, 

1048 expected_command={ 

1049 'kind': 'execute_script', 

1050 'args': {'label': 'Measure Soil Height'}, 

1051 }, 

1052 extra_rpc_args={}, 

1053 mock_api_response={}) 

1054 

1055 def test_detect_weeds(self): 

1056 '''Test detect_weeds command''' 

1057 def exec_command(): 

1058 self.fb.detect_weeds() 

1059 self.send_command_test_helper( 

1060 exec_command, 

1061 expected_command={ 

1062 'kind': 'execute_script', 

1063 'args': {'label': 'plant-detection'}, 

1064 }, 

1065 extra_rpc_args={}, 

1066 mock_api_response={}) 

1067 

1068 def test_calibrate_camera(self): 

1069 '''Test calibrate_camera command''' 

1070 def exec_command(): 

1071 self.fb.calibrate_camera() 

1072 self.send_command_test_helper( 

1073 exec_command, 

1074 expected_command={ 

1075 'kind': 'execute_script', 

1076 'args': {'label': 'camera-calibration'}, 

1077 }, 

1078 extra_rpc_args={}, 

1079 mock_api_response={}) 

1080 

1081 def test_sequence(self): 

1082 '''Test sequence command''' 

1083 def exec_command(): 

1084 self.fb.sequence('My Sequence') 

1085 self.send_command_test_helper( 

1086 exec_command, 

1087 expected_command={ 

1088 'kind': 'execute', 

1089 'args': {'sequence_id': 123}, 

1090 }, 

1091 extra_rpc_args={}, 

1092 mock_api_response=[{'name': 'My Sequence', 'id': 123}]) 

1093 

1094 def test_sequence_not_found(self): 

1095 '''Test sequence command: sequence not found''' 

1096 def exec_command(): 

1097 self.fb.sequence('My Sequence') 

1098 self.send_command_test_helper( 

1099 exec_command, 

1100 expected_command=None, 

1101 extra_rpc_args={}, 

1102 mock_api_response=[{'name': 'Water'}]) 

1103 self.assertEqual(self.fb.state.error, "ERROR: 'My Sequence' not in sequences: ['Water'].") 

1104 

1105 def test_take_photo(self): 

1106 '''Test take_photo command''' 

1107 def exec_command(): 

1108 self.fb.take_photo() 

1109 self.send_command_test_helper( 

1110 exec_command, 

1111 expected_command={ 

1112 'kind': 'take_photo', 

1113 'args': {}, 

1114 }, 

1115 extra_rpc_args={}, 

1116 mock_api_response={}) 

1117 

1118 def test_control_servo(self): 

1119 '''Test control_servo command''' 

1120 def exec_command(): 

1121 self.fb.control_servo(4, 100) 

1122 self.send_command_test_helper( 

1123 exec_command, 

1124 expected_command={ 

1125 'kind': 'set_servo_angle', 

1126 'args': { 

1127 'pin_number': 4, 

1128 'pin_value': 100, 

1129 }, 

1130 }, 

1131 extra_rpc_args={}, 

1132 mock_api_response={'mode': 0}) 

1133 

1134 def test_control_servo_error(self): 

1135 '''Test control_servo command: error''' 

1136 def exec_command(): 

1137 self.fb.control_servo(4, 200) 

1138 self.send_command_test_helper( 

1139 exec_command, 

1140 expected_command=None, 

1141 extra_rpc_args={}, 

1142 mock_api_response={'mode': 0}) 

1143 

1144 def test_get_xyz(self): 

1145 '''Test get_xyz command''' 

1146 def exec_command(): 

1147 self.fb.state.last_messages['status'] = { 

1148 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}}, 

1149 } 

1150 position = self.fb.get_xyz() 

1151 self.assertEqual(position, {'x': 1, 'y': 2, 'z': 3}) 

1152 self.send_command_test_helper( 

1153 exec_command, 

1154 expected_command={ 

1155 'kind': 'read_status', 

1156 'args': {}, 

1157 }, 

1158 extra_rpc_args={}, 

1159 mock_api_response={}) 

1160 

1161 def test_get_xyz_no_status(self): 

1162 '''Test get_xyz command: no status''' 

1163 def exec_command(): 

1164 self.fb.state.last_messages['status'] = None 

1165 position = self.fb.get_xyz() 

1166 self.assertIsNone(position) 

1167 self.send_command_test_helper( 

1168 exec_command, 

1169 expected_command={ 

1170 'kind': 'read_status', 

1171 'args': {}, 

1172 }, 

1173 extra_rpc_args={}, 

1174 mock_api_response={}) 

1175 

1176 def test_check_position(self): 

1177 '''Test check_position command: at position''' 

1178 def exec_command(): 

1179 self.fb.state.last_messages['status'] = { 

1180 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}}, 

1181 } 

1182 at_position = self.fb.check_position(1, 2, 3, 0) 

1183 self.assertTrue(at_position) 

1184 self.send_command_test_helper( 

1185 exec_command, 

1186 expected_command={ 

1187 'kind': 'read_status', 

1188 'args': {}, 

1189 }, 

1190 extra_rpc_args={}, 

1191 mock_api_response={}) 

1192 

1193 def test_check_position_false(self): 

1194 '''Test check_position command: not at position''' 

1195 def exec_command(): 

1196 self.fb.state.last_messages['status'] = { 

1197 'location_data': {'position': {'x': 1, 'y': 2, 'z': 3}}, 

1198 } 

1199 at_position = self.fb.check_position(0, 0, 0, 2) 

1200 self.assertFalse(at_position) 

1201 self.send_command_test_helper( 

1202 exec_command, 

1203 expected_command={ 

1204 'kind': 'read_status', 

1205 'args': {}, 

1206 }, 

1207 extra_rpc_args={}, 

1208 mock_api_response={}) 

1209 

1210 def test_check_position_no_status(self): 

1211 '''Test check_position command: no status''' 

1212 def exec_command(): 

1213 self.fb.state.last_messages['status'] = None 

1214 at_position = self.fb.check_position(0, 0, 0, 2) 

1215 self.assertFalse(at_position) 

1216 self.send_command_test_helper( 

1217 exec_command, 

1218 expected_command={ 

1219 'kind': 'read_status', 

1220 'args': {}, 

1221 }, 

1222 extra_rpc_args={}, 

1223 mock_api_response={}) 

1224 

1225 def test_mount_tool(self): 

1226 '''Test mount_tool command''' 

1227 def exec_command(): 

1228 self.fb.mount_tool('Weeder') 

1229 self.send_command_test_helper( 

1230 exec_command, 

1231 expected_command={ 

1232 'kind': 'lua', 

1233 'args': {'lua': 'mount_tool("Weeder")'}, 

1234 }, 

1235 extra_rpc_args={}, 

1236 mock_api_response={}) 

1237 

1238 def test_dismount_tool(self): 

1239 '''Test dismount_tool command''' 

1240 def exec_command(): 

1241 self.fb.dismount_tool() 

1242 self.send_command_test_helper( 

1243 exec_command, 

1244 expected_command={ 

1245 'kind': 'lua', 

1246 'args': {'lua': 'dismount_tool()'}, 

1247 }, 

1248 extra_rpc_args={}, 

1249 mock_api_response={}) 

1250 

1251 def test_water(self): 

1252 '''Test water command''' 

1253 def exec_command(): 

1254 self.fb.water(123) 

1255 self.send_command_test_helper( 

1256 exec_command, 

1257 expected_command={ 

1258 'kind': 'lua', 

1259 'args': {'lua': '''plant = api({ 

1260 method = "GET", 

1261 url = "/api/points/123" 

1262 }) 

1263 water(plant)'''}, 

1264 }, 

1265 extra_rpc_args={}, 

1266 mock_api_response={}) 

1267 

1268 def test_dispense(self): 

1269 '''Test dispense command''' 

1270 def exec_command(): 

1271 self.fb.dispense(100, 'Weeder', 4) 

1272 self.send_command_test_helper( 

1273 exec_command, 

1274 expected_command={ 

1275 'kind': 'lua', 

1276 'args': { 

1277 'lua': 'dispense(100, {tool_name = "Weeder", pin = 4})', 

1278 }, 

1279 }, 

1280 extra_rpc_args={}, 

1281 mock_api_response={}) 

1282 

1283 @patch('requests.request') 

1284 def helper_get_seed_tray_cell(self, *args, **kwargs): 

1285 '''Test helper for get_seed_tray_cell command''' 

1286 mock_request = args[0] 

1287 tray_data = kwargs['tray_data'] 

1288 cell = kwargs['cell'] 

1289 expected_xyz = kwargs['expected_xyz'] 

1290 mock_response = Mock() 

1291 mock_api_response = [ 

1292 { 

1293 'id': 123, 

1294 'name': 'Seed Tray', 

1295 'pointer_type': '', # not an actual data field 

1296 }, 

1297 { 

1298 'pointer_type': 'ToolSlot', 

1299 'pullout_direction': 1, 

1300 'x': 0, 

1301 'y': 0, 

1302 'z': 0, 

1303 'tool_id': 123, 

1304 'name': '', # not an actual data field 

1305 **tray_data, 

1306 }, 

1307 ] 

1308 mock_response.json.return_value = mock_api_response 

1309 mock_response.status_code = 200 

1310 mock_response.text = 'text' 

1311 mock_request.return_value = mock_response 

1312 cell = self.fb.get_seed_tray_cell('Seed Tray', cell) 

1313 mock_request.assert_has_calls([ 

1314 call( 

1315 'GET', 

1316 'https://my.farm.bot/api/tools', 

1317 **REQUEST_KWARGS, 

1318 ), 

1319 call().json(), 

1320 call( 

1321 'GET', 

1322 'https://my.farm.bot/api/points', 

1323 **REQUEST_KWARGS, 

1324 ), 

1325 call().json(), 

1326 ]) 

1327 self.assertEqual(cell, expected_xyz, kwargs) 

1328 

1329 def test_get_seed_tray_cell(self): 

1330 '''Test get_seed_tray_cell''' 

1331 test_cases = [ 

1332 { 

1333 'tray_data': {'pullout_direction': 1}, 

1334 'cell': 'a1', 

1335 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0}, 

1336 }, 

1337 { 

1338 'tray_data': {'pullout_direction': 1}, 

1339 'cell': 'b2', 

1340 'expected_xyz': {'x': -11.25, 'y': -6.25, 'z': 0}, 

1341 }, 

1342 { 

1343 'tray_data': {'pullout_direction': 1}, 

1344 'cell': 'd4', 

1345 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0}, 

1346 }, 

1347 { 

1348 'tray_data': {'pullout_direction': 2}, 

1349 'cell': 'a1', 

1350 'expected_xyz': {'x': -36.25, 'y': 18.75, 'z': 0}, 

1351 }, 

1352 { 

1353 'tray_data': {'pullout_direction': 2}, 

1354 'cell': 'b2', 

1355 'expected_xyz': {'x': -23.75, 'y': 6.25, 'z': 0}, 

1356 }, 

1357 { 

1358 'tray_data': {'pullout_direction': 2}, 

1359 'cell': 'd4', 

1360 'expected_xyz': {'x': 1.25, 'y': -18.75, 'z': 0}, 

1361 }, 

1362 { 

1363 'tray_data': {'pullout_direction': 2, 'x': 100, 'y': 200, 'z': -100}, 

1364 'cell': 'd4', 

1365 'expected_xyz': {'x': 101.25, 'y': 181.25, 'z': -100}, 

1366 }, 

1367 ] 

1368 for test_case in test_cases: 

1369 self.helper_get_seed_tray_cell(**test_case) 

1370 

1371 @patch('requests.request') 

1372 def helper_get_seed_tray_cell_error(self, *args, **kwargs): 

1373 '''Test helper for get_seed_tray_cell command errors''' 

1374 mock_request = args[0] 

1375 tray_data = kwargs['tray_data'] 

1376 cell = kwargs['cell'] 

1377 error = kwargs['error'] 

1378 mock_response = Mock() 

1379 mock_api_response = [ 

1380 { 

1381 'id': 123, 

1382 'name': 'Seed Tray', 

1383 'pointer_type': '', # not an actual data field 

1384 }, 

1385 { 

1386 'pointer_type': 'ToolSlot', 

1387 'pullout_direction': 1, 

1388 'x': 0, 

1389 'y': 0, 

1390 'z': 0, 

1391 'tool_id': 123, 

1392 'name': '', # not an actual data field 

1393 **tray_data, 

1394 }, 

1395 ] 

1396 mock_response.json.return_value = mock_api_response 

1397 mock_response.status_code = 200 

1398 mock_response.text = 'text' 

1399 mock_request.return_value = mock_response 

1400 with self.assertRaises(ValueError) as cm: 

1401 self.fb.get_seed_tray_cell('Seed Tray', cell) 

1402 self.assertEqual(cm.exception.args[0], error) 

1403 mock_request.assert_has_calls([ 

1404 call( 

1405 'GET', 

1406 'https://my.farm.bot/api/tools', 

1407 **REQUEST_KWARGS, 

1408 ), 

1409 call().json(), 

1410 call( 

1411 'GET', 

1412 'https://my.farm.bot/api/points', 

1413 **REQUEST_KWARGS, 

1414 ), 

1415 call().json(), 

1416 ]) 

1417 

1418 def test_get_seed_tray_cell_invalid_cell_name(self): 

1419 '''Test get_seed_tray_cell: invalid cell name''' 

1420 self.helper_get_seed_tray_cell_error( 

1421 tray_data={}, 

1422 cell='e4', 

1423 error='Seed Tray Cell must be one of **A1** through **D4**', 

1424 ) 

1425 

1426 def test_get_seed_tray_cell_invalid_pullout_direction(self): 

1427 '''Test get_seed_tray_cell: invalid pullout direction''' 

1428 self.helper_get_seed_tray_cell_error( 

1429 tray_data={'pullout_direction': 0}, 

1430 cell='d4', 

1431 error='Seed Tray **SLOT DIRECTION** must be `Positive X` or `Negative X`', 

1432 ) 

1433 

1434 @patch('requests.request') 

1435 def test_get_seed_tray_cell_no_tray(self, mock_request): 

1436 '''Test get_seed_tray_cell: no seed tray''' 

1437 mock_response = Mock() 

1438 mock_api_response = [] 

1439 mock_response.json.return_value = mock_api_response 

1440 mock_response.status_code = 200 

1441 mock_response.text = 'text' 

1442 mock_request.return_value = mock_response 

1443 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1') 

1444 mock_request.assert_has_calls([ 

1445 call( 

1446 'GET', 

1447 'https://my.farm.bot/api/tools', 

1448 **REQUEST_KWARGS, 

1449 ), 

1450 call().json(), 

1451 ]) 

1452 self.assertIsNone(result) 

1453 

1454 @patch('requests.request') 

1455 def test_get_seed_tray_cell_not_mounted(self, mock_request): 

1456 '''Test get_seed_tray_cell: seed tray not mounted''' 

1457 mock_response = Mock() 

1458 mock_api_response = [{ 

1459 'id': 123, 

1460 'name': 'Seed Tray', 

1461 'pointer_type': '', # not an actual data field, 

1462 }] 

1463 mock_response.json.return_value = mock_api_response 

1464 mock_response.status_code = 200 

1465 mock_response.text = 'text' 

1466 mock_request.return_value = mock_response 

1467 result = self.fb.get_seed_tray_cell('Seed Tray', 'a1') 

1468 mock_request.assert_has_calls([ 

1469 call( 

1470 'GET', 

1471 'https://my.farm.bot/api/tools', 

1472 **REQUEST_KWARGS, 

1473 ), 

1474 call().json(), 

1475 ]) 

1476 self.assertIsNone(result) 

1477 

1478 def test_get_job_one(self): 

1479 '''Test get_job command: get one job''' 

1480 def exec_command(): 

1481 self.fb.state.last_messages['status'] = { 

1482 'jobs': { 

1483 'job name': {'status': 'working'}, 

1484 }, 

1485 } 

1486 job = self.fb.get_job('job name') 

1487 self.assertEqual(job, {'status': 'working'}) 

1488 self.send_command_test_helper( 

1489 exec_command, 

1490 expected_command={ 

1491 'kind': 'read_status', 

1492 'args': {}, 

1493 }, 

1494 extra_rpc_args={}, 

1495 mock_api_response={}) 

1496 

1497 def test_get_job_all(self): 

1498 '''Test get_job command: get all jobs''' 

1499 def exec_command(): 

1500 self.fb.state.last_messages['status'] = { 

1501 'jobs': { 

1502 'job name': {'status': 'working'}, 

1503 }, 

1504 } 

1505 jobs = self.fb.get_job() 

1506 self.assertEqual(jobs, {'job name': {'status': 'working'}}) 

1507 self.send_command_test_helper( 

1508 exec_command, 

1509 expected_command={ 

1510 'kind': 'read_status', 

1511 'args': {}, 

1512 }, 

1513 extra_rpc_args={}, 

1514 mock_api_response={}) 

1515 

1516 def test_get_job_no_status(self): 

1517 '''Test get_job command: no status''' 

1518 def exec_command(): 

1519 self.fb.state.last_messages['status'] = None 

1520 job = self.fb.get_job('job name') 

1521 self.assertIsNone(job) 

1522 self.send_command_test_helper( 

1523 exec_command, 

1524 expected_command={ 

1525 'kind': 'read_status', 

1526 'args': {}, 

1527 }, 

1528 extra_rpc_args={}, 

1529 mock_api_response={}) 

1530 

1531 def test_set_job(self): 

1532 '''Test set_job command''' 

1533 def exec_command(): 

1534 self.fb.set_job('job name', 'working', 50) 

1535 self.send_command_test_helper( 

1536 exec_command, 

1537 expected_command={ 

1538 'kind': 'lua', 

1539 'args': {'lua': '''local job_name = "job name" 

1540 set_job(job_name) 

1541 

1542 -- Update the job's status and percent: 

1543 set_job(job_name, { 

1544 status = "working", 

1545 percent = 50 

1546 })'''}, 

1547 }, 

1548 extra_rpc_args={}, 

1549 mock_api_response={}) 

1550 

1551 def test_complete_job(self): 

1552 '''Test complete_job command''' 

1553 def exec_command(): 

1554 self.fb.complete_job('job name') 

1555 self.send_command_test_helper( 

1556 exec_command, 

1557 expected_command={ 

1558 'kind': 'lua', 

1559 'args': {'lua': 'complete_job("job name")'}, 

1560 }, 

1561 extra_rpc_args={}, 

1562 mock_api_response={}) 

1563 

1564 def test_lua(self): 

1565 '''Test lua command''' 

1566 def exec_command(): 

1567 self.fb.lua('return true') 

1568 self.send_command_test_helper( 

1569 exec_command, 

1570 expected_command={ 

1571 'kind': 'lua', 

1572 'args': {'lua': 'return true'}, 

1573 }, 

1574 extra_rpc_args={}, 

1575 mock_api_response={}) 

1576 

1577 def test_if_statement(self): 

1578 '''Test if_statement command''' 

1579 def exec_command(): 

1580 self.fb.if_statement('pin10', 'is', 0) 

1581 self.send_command_test_helper( 

1582 exec_command, 

1583 expected_command={ 

1584 'kind': '_if', 

1585 'args': { 

1586 'lhs': 'pin10', 

1587 'op': 'is', 

1588 'rhs': 0, 

1589 '_then': {'kind': 'nothing', 'args': {}}, 

1590 '_else': {'kind': 'nothing', 'args': {}}, 

1591 } 

1592 }, 

1593 extra_rpc_args={}, 

1594 mock_api_response=[]) 

1595 

1596 def test_if_statement_with_named_pin(self): 

1597 '''Test if_statement command with named pin''' 

1598 def exec_command(): 

1599 self.fb.if_statement('Lights', 'is', 0, named_pin_type='Peripheral') 

1600 self.send_command_test_helper( 

1601 exec_command, 

1602 expected_command={ 

1603 'kind': '_if', 

1604 'args': { 

1605 'lhs': { 

1606 'kind': 'named_pin', 

1607 'args': {'pin_type': 'Peripheral', 'pin_id': 123}, 

1608 }, 

1609 'op': 'is', 

1610 'rhs': 0, 

1611 '_then': {'kind': 'nothing', 'args': {}}, 

1612 '_else': {'kind': 'nothing', 'args': {}}, 

1613 } 

1614 }, 

1615 extra_rpc_args={}, 

1616 mock_api_response=[{'id': 123, 'label': 'Lights', 'mode': 0}]) 

1617 

1618 def test_if_statement_with_named_pin_not_found(self): 

1619 '''Test if_statement command: named pin not found''' 

1620 def exec_command(): 

1621 self.fb.if_statement('Lights', 'is', 0, named_pin_type='Peripheral') 

1622 self.send_command_test_helper( 

1623 exec_command, 

1624 expected_command=None, 

1625 extra_rpc_args={}, 

1626 mock_api_response=[{'label': 'Pump'}]) 

1627 self.assertEqual(self.fb.state.error, "ERROR: 'Lights' not in peripherals: ['Pump'].") 

1628 

1629 def test_if_statement_with_sequences(self): 

1630 '''Test if_statement command with sequences''' 

1631 def exec_command(): 

1632 self.fb.if_statement('pin10', '<', 0, 'Watering Sequence', 'Drying Sequence') 

1633 self.send_command_test_helper( 

1634 exec_command, 

1635 expected_command={ 

1636 'kind': '_if', 

1637 'args': { 

1638 'lhs': 'pin10', 

1639 'op': '<', 

1640 'rhs': 0, 

1641 '_then': {'kind': 'execute', 'args': {'sequence_id': 123}}, 

1642 '_else': {'kind': 'execute', 'args': {'sequence_id': 456}}, 

1643 } 

1644 }, 

1645 extra_rpc_args={}, 

1646 mock_api_response=[ 

1647 {'id': 123, 'name': 'Watering Sequence'}, 

1648 {'id': 456, 'name': 'Drying Sequence'}, 

1649 ]) 

1650 

1651 def test_if_statement_with_sequence_not_found(self): 

1652 '''Test if_statement command: sequence not found''' 

1653 def exec_command(): 

1654 self.fb.if_statement('pin10', '<', 0, 'Watering Sequence', 'Drying Sequence') 

1655 self.send_command_test_helper( 

1656 exec_command, 

1657 expected_command=None, 

1658 extra_rpc_args={}, 

1659 mock_api_response=[]) 

1660 self.assertEqual(self.fb.state.error, "ERROR: 'Watering Sequence' not in sequences: [].") 

1661 

1662 def test_if_statement_invalid_operator(self): 

1663 '''Test if_statement command: invalid operator''' 

1664 def exec_command(): 

1665 with self.assertRaises(ValueError) as cm: 

1666 self.fb.if_statement('pin10', 'nope', 0) 

1667 self.assertEqual( 

1668 cm.exception.args[0], 

1669 "Invalid operator: nope not in ['<', '>', 'is', 'not', 'is_undefined']") 

1670 self.send_command_test_helper( 

1671 exec_command, 

1672 expected_command=None, 

1673 extra_rpc_args={}, 

1674 mock_api_response=[]) 

1675 

1676 def test_if_statement_invalid_variable(self): 

1677 '''Test if_statement command: invalid variable''' 

1678 variables = ["x", "y", "z", *[f"pin{str(i)}" for i in range(70)]] 

1679 def exec_command(): 

1680 with self.assertRaises(ValueError) as cm: 

1681 self.fb.if_statement('nope', '<', 0) 

1682 self.assertEqual( 

1683 cm.exception.args[0], 

1684 f"Invalid variable: nope not in {variables}") 

1685 self.send_command_test_helper( 

1686 exec_command, 

1687 expected_command=None, 

1688 extra_rpc_args={}, 

1689 mock_api_response=[]) 

1690 

1691 def test_if_statement_invalid_named_pin_type(self): 

1692 '''Test if_statement command: invalid named pin type''' 

1693 def exec_command(): 

1694 with self.assertRaises(ValueError) as cm: 

1695 self.fb.if_statement('pin10', '<', 0, named_pin_type='nope') 

1696 self.assertEqual( 

1697 cm.exception.args[0], 

1698 "Invalid named_pin_type: nope not in ['Peripheral', 'Sensor']") 

1699 self.send_command_test_helper( 

1700 exec_command, 

1701 expected_command=None, 

1702 extra_rpc_args={}, 

1703 mock_api_response=[]) 

1704 

1705 def test_rpc_error(self): 

1706 '''Test rpc error handling''' 

1707 def exec_command(): 

1708 self.fb.wait(100) 

1709 self.assertEqual(self.fb.state.error, 'RPC error response received.') 

1710 self.send_command_test_helper( 

1711 exec_command, 

1712 error=True, 

1713 expected_command={ 

1714 'kind': 'wait', 

1715 'args': {'milliseconds': 100}}, 

1716 extra_rpc_args={}, 

1717 mock_api_response=[]) 

1718 

1719 def test_rpc_response_timeout(self): 

1720 '''Test rpc response timeout handling''' 

1721 def exec_command(): 

1722 self.fb.state.last_messages['from_device'] = {'kind': 'rpc_ok', 'args': {'label': 'wrong label'}} 

1723 self.fb.wait(100) 

1724 self.assertEqual(self.fb.state.error, 'Timed out waiting for RPC response.') 

1725 self.send_command_test_helper( 

1726 exec_command, 

1727 expected_command={ 

1728 'kind': 'wait', 

1729 'args': {'milliseconds': 100}}, 

1730 extra_rpc_args={}, 

1731 mock_api_response=[]) 

1732 

1733 def test_set_verbosity(self): 

1734 '''Test set_verbosity.''' 

1735 self.assertEqual(self.fb.state.verbosity, 0) 

1736 self.fb.set_verbosity(1) 

1737 self.assertEqual(self.fb.state.verbosity, 1) 

1738 

1739 def test_set_timeout(self): 

1740 '''Test set_timeout.''' 

1741 self.assertEqual(self.fb.state.timeout['listen'], 0) 

1742 self.fb.set_timeout(15) 

1743 self.assertEqual(self.fb.state.timeout['listen'], 15) 

1744 

1745 @staticmethod 

1746 def helper_get_print_strings(mock_print): 

1747 '''Test helper to get print call strings.''' 

1748 return [string[1][0] for string in mock_print.mock_calls if len(string[1]) > 0] 

1749 

1750 @patch('builtins.print') 

1751 def test_print_status(self, mock_print): 

1752 '''Test print_status.''' 

1753 self.fb.set_verbosity(0) 

1754 self.fb.state.print_status(description="testing") 

1755 mock_print.assert_not_called() 

1756 self.fb.set_verbosity(1) 

1757 self.fb.state.print_status(description="testing") 

1758 call_strings = self.helper_get_print_strings(mock_print) 

1759 self.assertIn('testing', call_strings) 

1760 mock_print.reset_mock() 

1761 self.fb.set_verbosity(2) 

1762 self.fb.state.print_status(endpoint_json=["testing"]) 

1763 call_strings = self.helper_get_print_strings(mock_print) 

1764 call_strings = [s.split('(')[0].strip('`') for s in call_strings] 

1765 self.assertIn('[\n "testing"\n]', call_strings) 

1766 self.assertIn('test_print_status', call_strings)