Coverage for tests/tests_main.py: 100%

763 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-09-03 09:25 -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 self.fb.state.clear_cache() 

52 

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) 

72 

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) 

93 

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) 

113 

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 ) 

120 

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 ) 

127 

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 ) 

134 

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) 

152 

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 ) 

159 

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 ) 

166 

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 ) 

173 

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 ) 

180 

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) 

200 

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 ) 

215 

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)') 

232 

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)') 

249 

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) 

267 

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) 

285 

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) 

295 

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) 

318 

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() 

327 

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'}) 

346 

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'}) 

365 

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'}) 

381 

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}) 

389 

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'}) 

405 

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'}]) 

421 

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'}) 

437 

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'}]) 

453 

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) 

469 

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}) 

492 

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 ) 

512 

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() 

527 

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() 

535 

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() 

542 

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() 

558 

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

560 def test_listen_clear_last(self, mock_mqtt): 

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

562 mock_client = Mock() 

563 mock_mqtt.return_value = mock_client 

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

565 self.fb.state.test_env = False 

566 self.fb.listen() 

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

568 

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

570 def test_publish_apply_label(self, mock_mqtt): 

571 '''Test publish command: set uuid''' 

572 mock_client = Mock() 

573 mock_mqtt.return_value = mock_client 

574 self.fb.state.test_env = False 

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

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

577 

578 @patch('requests.request') 

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

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

581 '''Helper for testing command execution''' 

582 execute_command = args[0] 

583 mock_mqtt = args[1] 

584 mock_request = args[2] 

585 expected_command = kwargs.get('expected_command') 

586 extra_rpc_args = kwargs.get('extra_rpc_args') 

587 mock_api_response = kwargs.get('mock_api_response') 

588 error = kwargs.get('error') 

589 mock_client = Mock() 

590 mock_mqtt.return_value = mock_client 

591 mock_response = Mock() 

592 mock_response.json.return_value = mock_api_response 

593 mock_response.status_code = 200 

594 mock_response.text = 'text' 

595 mock_request.return_value = mock_response 

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

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

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

599 } 

600 execute_command() 

601 if expected_command is None: 

602 mock_client.publish.assert_not_called() 

603 return 

604 expected_payload = { 

605 'kind': 'rpc_request', 

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

607 'body': [expected_command], 

608 } 

609 mock_client.username_pw_set.assert_called_once_with( 

610 username='device_0', 

611 password='encoded_token_value') 

612 mock_client.connect.assert_called_once_with( 

613 'mqtt_url', 

614 port=1883, 

615 keepalive=60) 

616 mock_client.loop_start.assert_called() 

617 mock_client.publish.assert_called_once_with( 

618 'bot/device_0/from_clients', 

619 payload=json.dumps(expected_payload)) 

620 if not error: 

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

622 

623 def test_message(self): 

624 '''Test message command''' 

625 def exec_command(): 

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

627 self.send_command_test_helper( 

628 exec_command, 

629 expected_command={ 

630 'kind': 'send_message', 

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

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

633 }, 

634 extra_rpc_args={}, 

635 mock_api_response={}) 

636 

637 def test_debug(self): 

638 '''Test debug command''' 

639 def exec_command(): 

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

641 self.send_command_test_helper( 

642 exec_command, 

643 expected_command={ 

644 'kind': 'send_message', 

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

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

647 }, 

648 extra_rpc_args={}, 

649 mock_api_response={}) 

650 

651 def test_toast(self): 

652 '''Test toast command''' 

653 def exec_command(): 

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

655 self.send_command_test_helper( 

656 exec_command, 

657 expected_command={ 

658 'kind': 'send_message', 

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

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

661 }, 

662 extra_rpc_args={}, 

663 mock_api_response={}) 

664 

665 def test_invalid_message_type(self): 

666 '''Test message_type validation''' 

667 def exec_command(): 

668 with self.assertRaises(ValueError) as cm: 

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

670 self.assertEqual( 

671 cm.exception.args[0], 

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

673 self.send_command_test_helper( 

674 exec_command, 

675 expected_command=None, 

676 extra_rpc_args={}, 

677 mock_api_response={}) 

678 

679 def test_invalid_message_channel(self): 

680 '''Test message channel validation''' 

681 def exec_command(): 

682 with self.assertRaises(ValueError) as cm: 

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

684 self.assertEqual( 

685 cm.exception.args[0], 

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

687 self.send_command_test_helper( 

688 exec_command, 

689 expected_command=None, 

690 extra_rpc_args={}, 

691 mock_api_response={}) 

692 

693 def test_read_status(self): 

694 '''Test read_status command''' 

695 def exec_command(): 

696 self.fb.read_status() 

697 self.send_command_test_helper( 

698 exec_command, 

699 expected_command={ 

700 'kind': 'read_status', 

701 'args': {}, 

702 }, 

703 extra_rpc_args={}, 

704 mock_api_response={}) 

705 

706 def test_read_sensor(self): 

707 '''Test read_sensor command''' 

708 def exec_command(): 

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

710 self.send_command_test_helper( 

711 exec_command, 

712 expected_command={ 

713 'kind': 'read_pin', 

714 'args': { 

715 'pin_mode': 0, 

716 'label': '---', 

717 'pin_number': { 

718 'kind': 'named_pin', 

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

720 }, 

721 }, 

722 }, 

723 extra_rpc_args={}, 

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

725 

726 def test_read_sensor_not_found(self): 

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

728 def exec_command(): 

729 self.fb.read_sensor('Temperature') 

730 self.send_command_test_helper( 

731 exec_command, 

732 expected_command=None, 

733 extra_rpc_args={}, 

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

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

736 

737 def test_assertion(self): 

738 '''Test assertion command''' 

739 def exec_command(): 

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

741 self.send_command_test_helper( 

742 exec_command, 

743 expected_command={ 

744 'kind': 'assertion', 

745 'args': { 

746 'assertion_type': 'abort', 

747 'lua': 'return true', 

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

749 } 

750 }, 

751 extra_rpc_args={}, 

752 mock_api_response={}) 

753 

754 def test_assertion_with_recovery_sequence(self): 

755 '''Test assertion command with recovery sequence''' 

756 def exec_command(): 

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

758 self.send_command_test_helper( 

759 exec_command, 

760 expected_command={ 

761 'kind': 'assertion', 

762 'args': { 

763 'assertion_type': 'abort', 

764 'lua': 'return true', 

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

766 } 

767 }, 

768 extra_rpc_args={}, 

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

770 

771 def test_assertion_recovery_sequence_not_found(self): 

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

773 def exec_command(): 

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

775 self.send_command_test_helper( 

776 exec_command, 

777 expected_command=None, 

778 extra_rpc_args={}, 

779 mock_api_response=[]) 

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

781 

782 def test_assertion_invalid_assertion_type(self): 

783 '''Test assertion command: invalid assertion type''' 

784 def exec_command(): 

785 with self.assertRaises(ValueError) as cm: 

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

787 self.assertEqual( 

788 cm.exception.args[0], 

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

790 self.send_command_test_helper( 

791 exec_command, 

792 expected_command=None, 

793 extra_rpc_args={}, 

794 mock_api_response={}) 

795 

796 def test_wait(self): 

797 '''Test wait command''' 

798 def exec_command(): 

799 self.fb.wait(123) 

800 self.send_command_test_helper( 

801 exec_command, 

802 expected_command={ 

803 'kind': 'wait', 

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

805 }, 

806 extra_rpc_args={}, 

807 mock_api_response={}) 

808 

809 def test_unlock(self): 

810 '''Test unlock command''' 

811 def exec_command(): 

812 self.fb.unlock() 

813 self.send_command_test_helper( 

814 exec_command, 

815 expected_command={ 

816 'kind': 'emergency_unlock', 

817 'args': {}, 

818 }, 

819 extra_rpc_args={'priority': 9000}, 

820 mock_api_response={}) 

821 

822 def test_e_stop(self): 

823 '''Test e_stop command''' 

824 def exec_command(): 

825 self.fb.e_stop() 

826 self.send_command_test_helper( 

827 exec_command, 

828 expected_command={ 

829 'kind': 'emergency_lock', 

830 'args': {}, 

831 }, 

832 extra_rpc_args={'priority': 9000}, 

833 mock_api_response={}) 

834 

835 def test_find_home(self): 

836 '''Test find_home command''' 

837 def exec_command(): 

838 self.fb.find_home() 

839 self.send_command_test_helper( 

840 exec_command, 

841 expected_command={ 

842 'kind': 'find_home', 

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

844 }, 

845 extra_rpc_args={}, 

846 mock_api_response={}) 

847 

848 def test_find_home_speed_error(self): 

849 '''Test find_home command: speed error''' 

850 def exec_command(): 

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

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: Speed constrained to 1-100.') 

858 

859 def test_find_home_invalid_axis(self): 

860 '''Test find_home command: invalid axis''' 

861 def exec_command(): 

862 with self.assertRaises(ValueError) as cm: 

863 self.fb.find_home('nope') 

864 self.assertEqual( 

865 cm.exception.args[0], 

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

867 self.send_command_test_helper( 

868 exec_command, 

869 expected_command=None, 

870 extra_rpc_args={}, 

871 mock_api_response={}) 

872 

873 def test_set_home(self): 

874 '''Test set_home command''' 

875 def exec_command(): 

876 self.fb.set_home() 

877 self.send_command_test_helper( 

878 exec_command, 

879 expected_command={ 

880 'kind': 'zero', 

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

882 }, 

883 extra_rpc_args={}, 

884 mock_api_response={}) 

885 

886 def test_toggle_peripheral(self): 

887 '''Test toggle_peripheral command''' 

888 def exec_command(): 

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

890 self.send_command_test_helper( 

891 exec_command, 

892 expected_command={ 

893 'kind': 'toggle_pin', 

894 'args': { 

895 'pin_number': { 

896 'kind': 'named_pin', 

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

898 }, 

899 }, 

900 }, 

901 extra_rpc_args={}, 

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

903 

904 def test_toggle_peripheral_not_found(self): 

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

906 def exec_command(): 

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

908 self.send_command_test_helper( 

909 exec_command, 

910 expected_command=None, 

911 extra_rpc_args={}, 

912 mock_api_response=[]) 

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

914 

915 @patch('requests.request') 

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

917 def test_toggle_peripheral_use_cache(self, mock_mqtt, mock_request): 

918 '''Test toggle_peripheral command: use cache''' 

919 mock_client = Mock() 

920 mock_mqtt.return_value = mock_client 

921 mock_response = Mock() 

922 mock_response.json.return_value = [ 

923 {'label': 'Peripheral 4', 'id': 123}, 

924 {'label': 'Peripheral 5', 'id': 456} 

925 ] 

926 mock_response.status_code = 200 

927 mock_response.text = 'text' 

928 mock_request.return_value = mock_response 

929 # save cache 

930 self.fb.toggle_peripheral('Peripheral 4') 

931 mock_request.assert_called() 

932 mock_client.publish.assert_called() 

933 mock_request.reset_mock() 

934 mock_client.reset_mock() 

935 # use cache 

936 self.fb.toggle_peripheral('Peripheral 5') 

937 mock_request.assert_not_called() 

938 mock_client.publish.assert_called() 

939 mock_request.reset_mock() 

940 mock_client.reset_mock() 

941 # clear cache 

942 self.fb.toggle_peripheral('Peripheral 6') 

943 mock_request.assert_not_called() 

944 mock_client.publish.assert_not_called() 

945 mock_request.reset_mock() 

946 mock_client.reset_mock() 

947 # save cache 

948 self.fb.toggle_peripheral('Peripheral 4') 

949 mock_request.assert_called() 

950 mock_client.publish.assert_called() 

951 mock_request.reset_mock() 

952 mock_client.reset_mock() 

953 

954 def test_on_digital(self): 

955 '''Test on command: digital''' 

956 def exec_command(): 

957 self.fb.on(13) 

958 self.send_command_test_helper( 

959 exec_command, 

960 expected_command={ 

961 'kind': 'write_pin', 

962 'args': { 

963 'pin_value': 1, 

964 'pin_mode': 0, 

965 'pin_number': 13, 

966 }, 

967 }, 

968 extra_rpc_args={}, 

969 mock_api_response={}) 

970 

971 def test_off(self): 

972 '''Test off command''' 

973 def exec_command(): 

974 self.fb.off(13) 

975 self.send_command_test_helper( 

976 exec_command, 

977 expected_command={ 

978 'kind': 'write_pin', 

979 'args': { 

980 'pin_value': 0, 

981 'pin_mode': 0, 

982 'pin_number': 13, 

983 }, 

984 }, 

985 extra_rpc_args={}, 

986 mock_api_response={}) 

987 

988 def test_move(self): 

989 '''Test move command''' 

990 def exec_command(): 

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

992 self.send_command_test_helper( 

993 exec_command, 

994 expected_command={ 

995 'kind': 'move', 

996 'args': {}, 

997 'body': [ 

998 {'kind': 'axis_overwrite', 'args': { 

999 'axis': 'x', 

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

1001 {'kind': 'axis_overwrite', 'args': { 

1002 'axis': 'y', 

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

1004 {'kind': 'axis_overwrite', 'args': { 

1005 'axis': 'z', 

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

1007 ], 

1008 }, 

1009 extra_rpc_args={}, 

1010 mock_api_response={}) 

1011 

1012 def test_reboot(self): 

1013 '''Test reboot command''' 

1014 def exec_command(): 

1015 self.fb.reboot() 

1016 self.send_command_test_helper( 

1017 exec_command, 

1018 expected_command={ 

1019 'kind': 'reboot', 

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

1021 }, 

1022 extra_rpc_args={}, 

1023 mock_api_response={}) 

1024 

1025 def test_shutdown(self): 

1026 '''Test shutdown command''' 

1027 def exec_command(): 

1028 self.fb.shutdown() 

1029 self.send_command_test_helper( 

1030 exec_command, 

1031 expected_command={ 

1032 'kind': 'power_off', 

1033 'args': {}, 

1034 }, 

1035 extra_rpc_args={}, 

1036 mock_api_response={}) 

1037 

1038 def test_find_axis_length(self): 

1039 '''Test find_axis_length command''' 

1040 def exec_command(): 

1041 self.fb.find_axis_length() 

1042 self.send_command_test_helper( 

1043 exec_command, 

1044 expected_command={ 

1045 'kind': 'calibrate', 

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

1047 }, 

1048 extra_rpc_args={}, 

1049 mock_api_response={}) 

1050 

1051 def test_control_peripheral(self): 

1052 '''Test control_peripheral command''' 

1053 def exec_command(): 

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

1055 self.send_command_test_helper( 

1056 exec_command, 

1057 expected_command={ 

1058 'kind': 'write_pin', 

1059 'args': { 

1060 'pin_value': 1, 

1061 'pin_mode': 0, 

1062 'pin_number': { 

1063 'kind': 'named_pin', 

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

1065 }, 

1066 }, 

1067 }, 

1068 extra_rpc_args={}, 

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

1070 

1071 def test_control_peripheral_not_found(self): 

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

1073 def exec_command(): 

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

1075 self.send_command_test_helper( 

1076 exec_command, 

1077 expected_command=None, 

1078 extra_rpc_args={}, 

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

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

1081 

1082 def test_measure_soil_height(self): 

1083 '''Test measure_soil_height command''' 

1084 def exec_command(): 

1085 self.fb.measure_soil_height() 

1086 self.send_command_test_helper( 

1087 exec_command, 

1088 expected_command={ 

1089 'kind': 'execute_script', 

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

1091 }, 

1092 extra_rpc_args={}, 

1093 mock_api_response={}) 

1094 

1095 def test_detect_weeds(self): 

1096 '''Test detect_weeds command''' 

1097 def exec_command(): 

1098 self.fb.detect_weeds() 

1099 self.send_command_test_helper( 

1100 exec_command, 

1101 expected_command={ 

1102 'kind': 'execute_script', 

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

1104 }, 

1105 extra_rpc_args={}, 

1106 mock_api_response={}) 

1107 

1108 def test_calibrate_camera(self): 

1109 '''Test calibrate_camera command''' 

1110 def exec_command(): 

1111 self.fb.calibrate_camera() 

1112 self.send_command_test_helper( 

1113 exec_command, 

1114 expected_command={ 

1115 'kind': 'execute_script', 

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

1117 }, 

1118 extra_rpc_args={}, 

1119 mock_api_response={}) 

1120 

1121 def test_sequence(self): 

1122 '''Test sequence command''' 

1123 def exec_command(): 

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

1125 self.send_command_test_helper( 

1126 exec_command, 

1127 expected_command={ 

1128 'kind': 'execute', 

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

1130 }, 

1131 extra_rpc_args={}, 

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

1133 

1134 def test_sequence_not_found(self): 

1135 '''Test sequence command: sequence not found''' 

1136 def exec_command(): 

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

1138 self.send_command_test_helper( 

1139 exec_command, 

1140 expected_command=None, 

1141 extra_rpc_args={}, 

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

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

1144 

1145 def test_take_photo(self): 

1146 '''Test take_photo command''' 

1147 def exec_command(): 

1148 self.fb.take_photo() 

1149 self.send_command_test_helper( 

1150 exec_command, 

1151 expected_command={ 

1152 'kind': 'take_photo', 

1153 'args': {}, 

1154 }, 

1155 extra_rpc_args={}, 

1156 mock_api_response={}) 

1157 

1158 def test_control_servo(self): 

1159 '''Test control_servo command''' 

1160 def exec_command(): 

1161 self.fb.control_servo(4, 100) 

1162 self.send_command_test_helper( 

1163 exec_command, 

1164 expected_command={ 

1165 'kind': 'set_servo_angle', 

1166 'args': { 

1167 'pin_number': 4, 

1168 'pin_value': 100, 

1169 }, 

1170 }, 

1171 extra_rpc_args={}, 

1172 mock_api_response={'mode': 0}) 

1173 

1174 def test_control_servo_error(self): 

1175 '''Test control_servo command: error''' 

1176 def exec_command(): 

1177 self.fb.control_servo(4, 200) 

1178 self.send_command_test_helper( 

1179 exec_command, 

1180 expected_command=None, 

1181 extra_rpc_args={}, 

1182 mock_api_response={'mode': 0}) 

1183 

1184 def test_get_xyz(self): 

1185 '''Test get_xyz command''' 

1186 def exec_command(): 

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

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

1189 } 

1190 position = self.fb.get_xyz() 

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

1192 self.send_command_test_helper( 

1193 exec_command, 

1194 expected_command={ 

1195 'kind': 'read_status', 

1196 'args': {}, 

1197 }, 

1198 extra_rpc_args={}, 

1199 mock_api_response={}) 

1200 

1201 def test_get_xyz_no_status(self): 

1202 '''Test get_xyz command: no status''' 

1203 def exec_command(): 

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

1205 position = self.fb.get_xyz() 

1206 self.assertIsNone(position) 

1207 self.send_command_test_helper( 

1208 exec_command, 

1209 expected_command={ 

1210 'kind': 'read_status', 

1211 'args': {}, 

1212 }, 

1213 extra_rpc_args={}, 

1214 mock_api_response={}) 

1215 

1216 def test_check_position(self): 

1217 '''Test check_position command: at position''' 

1218 def exec_command(): 

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

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

1221 } 

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

1223 self.assertTrue(at_position) 

1224 self.send_command_test_helper( 

1225 exec_command, 

1226 expected_command={ 

1227 'kind': 'read_status', 

1228 'args': {}, 

1229 }, 

1230 extra_rpc_args={}, 

1231 mock_api_response={}) 

1232 

1233 def test_check_position_false(self): 

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

1235 def exec_command(): 

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

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

1238 } 

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

1240 self.assertFalse(at_position) 

1241 self.send_command_test_helper( 

1242 exec_command, 

1243 expected_command={ 

1244 'kind': 'read_status', 

1245 'args': {}, 

1246 }, 

1247 extra_rpc_args={}, 

1248 mock_api_response={}) 

1249 

1250 def test_check_position_no_status(self): 

1251 '''Test check_position command: no status''' 

1252 def exec_command(): 

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

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

1255 self.assertFalse(at_position) 

1256 self.send_command_test_helper( 

1257 exec_command, 

1258 expected_command={ 

1259 'kind': 'read_status', 

1260 'args': {}, 

1261 }, 

1262 extra_rpc_args={}, 

1263 mock_api_response={}) 

1264 

1265 def test_mount_tool(self): 

1266 '''Test mount_tool command''' 

1267 def exec_command(): 

1268 self.fb.mount_tool('Weeder') 

1269 self.send_command_test_helper( 

1270 exec_command, 

1271 expected_command={ 

1272 'kind': 'lua', 

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

1274 }, 

1275 extra_rpc_args={}, 

1276 mock_api_response={}) 

1277 

1278 def test_dismount_tool(self): 

1279 '''Test dismount_tool command''' 

1280 def exec_command(): 

1281 self.fb.dismount_tool() 

1282 self.send_command_test_helper( 

1283 exec_command, 

1284 expected_command={ 

1285 'kind': 'lua', 

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

1287 }, 

1288 extra_rpc_args={}, 

1289 mock_api_response={}) 

1290 

1291 def test_water(self): 

1292 '''Test water command''' 

1293 def exec_command(): 

1294 self.fb.water(123) 

1295 self.send_command_test_helper( 

1296 exec_command, 

1297 expected_command={ 

1298 'kind': 'lua', 

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

1300 method = "GET", 

1301 url = "/api/points/123" 

1302 }) 

1303 water(plant)'''}, 

1304 }, 

1305 extra_rpc_args={}, 

1306 mock_api_response={}) 

1307 

1308 def test_dispense(self): 

1309 '''Test dispense command''' 

1310 def exec_command(): 

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

1312 self.send_command_test_helper( 

1313 exec_command, 

1314 expected_command={ 

1315 'kind': 'lua', 

1316 'args': { 

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

1318 }, 

1319 }, 

1320 extra_rpc_args={}, 

1321 mock_api_response={}) 

1322 

1323 @patch('requests.request') 

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

1325 '''Test helper for get_seed_tray_cell command''' 

1326 mock_request = args[0] 

1327 tray_data = kwargs['tray_data'] 

1328 cell = kwargs['cell'] 

1329 expected_xyz = kwargs['expected_xyz'] 

1330 self.fb.state.clear_cache() 

1331 mock_response = Mock() 

1332 mock_api_response = [ 

1333 { 

1334 'id': 123, 

1335 'name': 'Seed Tray', 

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

1337 }, 

1338 { 

1339 'pointer_type': 'ToolSlot', 

1340 'pullout_direction': 1, 

1341 'x': 0, 

1342 'y': 0, 

1343 'z': 0, 

1344 'tool_id': 123, 

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

1346 **tray_data, 

1347 }, 

1348 ] 

1349 mock_response.json.return_value = mock_api_response 

1350 mock_response.status_code = 200 

1351 mock_response.text = 'text' 

1352 mock_request.return_value = mock_response 

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

1354 mock_request.assert_has_calls([ 

1355 call( 

1356 'GET', 

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

1358 **REQUEST_KWARGS, 

1359 ), 

1360 call().json(), 

1361 call( 

1362 'GET', 

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

1364 **REQUEST_KWARGS, 

1365 ), 

1366 call().json(), 

1367 ]) 

1368 self.assertEqual(cell, expected_xyz, kwargs) 

1369 

1370 def test_get_seed_tray_cell(self): 

1371 '''Test get_seed_tray_cell''' 

1372 test_cases = [ 

1373 { 

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

1375 'cell': 'a1', 

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

1377 }, 

1378 { 

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

1380 'cell': 'b2', 

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

1382 }, 

1383 { 

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

1385 'cell': 'd4', 

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

1387 }, 

1388 { 

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

1390 'cell': 'a1', 

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

1392 }, 

1393 { 

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

1395 'cell': 'b2', 

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

1397 }, 

1398 { 

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

1400 'cell': 'd4', 

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

1402 }, 

1403 { 

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

1405 'cell': 'd4', 

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

1407 }, 

1408 ] 

1409 for test_case in test_cases: 

1410 self.helper_get_seed_tray_cell(**test_case) 

1411 

1412 @patch('requests.request') 

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

1414 '''Test helper for get_seed_tray_cell command errors''' 

1415 mock_request = args[0] 

1416 tray_data = kwargs['tray_data'] 

1417 cell = kwargs['cell'] 

1418 error = kwargs['error'] 

1419 mock_response = Mock() 

1420 mock_api_response = [ 

1421 { 

1422 'id': 123, 

1423 'name': 'Seed Tray', 

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

1425 }, 

1426 { 

1427 'pointer_type': 'ToolSlot', 

1428 'pullout_direction': 1, 

1429 'x': 0, 

1430 'y': 0, 

1431 'z': 0, 

1432 'tool_id': 123, 

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

1434 **tray_data, 

1435 }, 

1436 ] 

1437 mock_response.json.return_value = mock_api_response 

1438 mock_response.status_code = 200 

1439 mock_response.text = 'text' 

1440 mock_request.return_value = mock_response 

1441 with self.assertRaises(ValueError) as cm: 

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

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

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 call( 

1452 'GET', 

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

1454 **REQUEST_KWARGS, 

1455 ), 

1456 call().json(), 

1457 ]) 

1458 

1459 def test_get_seed_tray_cell_invalid_cell_name(self): 

1460 '''Test get_seed_tray_cell: invalid cell name''' 

1461 self.helper_get_seed_tray_cell_error( 

1462 tray_data={}, 

1463 cell='e4', 

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

1465 ) 

1466 

1467 def test_get_seed_tray_cell_invalid_pullout_direction(self): 

1468 '''Test get_seed_tray_cell: invalid pullout direction''' 

1469 self.helper_get_seed_tray_cell_error( 

1470 tray_data={'pullout_direction': 0}, 

1471 cell='d4', 

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

1473 ) 

1474 

1475 @patch('requests.request') 

1476 def test_get_seed_tray_cell_no_tray(self, mock_request): 

1477 '''Test get_seed_tray_cell: no seed tray''' 

1478 mock_response = Mock() 

1479 mock_api_response = [] 

1480 mock_response.json.return_value = mock_api_response 

1481 mock_response.status_code = 200 

1482 mock_response.text = 'text' 

1483 mock_request.return_value = mock_response 

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

1485 mock_request.assert_has_calls([ 

1486 call( 

1487 'GET', 

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

1489 **REQUEST_KWARGS, 

1490 ), 

1491 call().json(), 

1492 ]) 

1493 self.assertIsNone(result) 

1494 

1495 @patch('requests.request') 

1496 def test_get_seed_tray_cell_not_mounted(self, mock_request): 

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

1498 mock_response = Mock() 

1499 mock_api_response = [{ 

1500 'id': 123, 

1501 'name': 'Seed Tray', 

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

1503 }] 

1504 mock_response.json.return_value = mock_api_response 

1505 mock_response.status_code = 200 

1506 mock_response.text = 'text' 

1507 mock_request.return_value = mock_response 

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

1509 mock_request.assert_has_calls([ 

1510 call( 

1511 'GET', 

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

1513 **REQUEST_KWARGS, 

1514 ), 

1515 call().json(), 

1516 ]) 

1517 self.assertIsNone(result) 

1518 

1519 def test_get_job_one(self): 

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

1521 def exec_command(): 

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

1523 'jobs': { 

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

1525 }, 

1526 } 

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

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

1529 self.send_command_test_helper( 

1530 exec_command, 

1531 expected_command={ 

1532 'kind': 'read_status', 

1533 'args': {}, 

1534 }, 

1535 extra_rpc_args={}, 

1536 mock_api_response={}) 

1537 

1538 def test_get_job_all(self): 

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

1540 def exec_command(): 

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

1542 'jobs': { 

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

1544 }, 

1545 } 

1546 jobs = self.fb.get_job() 

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

1548 self.send_command_test_helper( 

1549 exec_command, 

1550 expected_command={ 

1551 'kind': 'read_status', 

1552 'args': {}, 

1553 }, 

1554 extra_rpc_args={}, 

1555 mock_api_response={}) 

1556 

1557 def test_get_job_no_status(self): 

1558 '''Test get_job command: no status''' 

1559 def exec_command(): 

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

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

1562 self.assertIsNone(job) 

1563 self.send_command_test_helper( 

1564 exec_command, 

1565 expected_command={ 

1566 'kind': 'read_status', 

1567 'args': {}, 

1568 }, 

1569 extra_rpc_args={}, 

1570 mock_api_response={}) 

1571 

1572 def test_set_job(self): 

1573 '''Test set_job command''' 

1574 def exec_command(): 

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

1576 self.send_command_test_helper( 

1577 exec_command, 

1578 expected_command={ 

1579 'kind': 'lua', 

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

1581 set_job(job_name) 

1582 

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

1584 set_job(job_name, { 

1585 status = "working", 

1586 percent = 50 

1587 })'''}, 

1588 }, 

1589 extra_rpc_args={}, 

1590 mock_api_response={}) 

1591 

1592 def test_complete_job(self): 

1593 '''Test complete_job command''' 

1594 def exec_command(): 

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

1596 self.send_command_test_helper( 

1597 exec_command, 

1598 expected_command={ 

1599 'kind': 'lua', 

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

1601 }, 

1602 extra_rpc_args={}, 

1603 mock_api_response={}) 

1604 

1605 def test_lua(self): 

1606 '''Test lua command''' 

1607 def exec_command(): 

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

1609 self.send_command_test_helper( 

1610 exec_command, 

1611 expected_command={ 

1612 'kind': 'lua', 

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

1614 }, 

1615 extra_rpc_args={}, 

1616 mock_api_response={}) 

1617 

1618 def test_if_statement(self): 

1619 '''Test if_statement command''' 

1620 def exec_command(): 

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

1622 self.send_command_test_helper( 

1623 exec_command, 

1624 expected_command={ 

1625 'kind': '_if', 

1626 'args': { 

1627 'lhs': 'pin10', 

1628 'op': 'is', 

1629 'rhs': 0, 

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

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

1632 } 

1633 }, 

1634 extra_rpc_args={}, 

1635 mock_api_response=[]) 

1636 

1637 def test_if_statement_with_named_pin(self): 

1638 '''Test if_statement command with named pin''' 

1639 def exec_command(): 

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

1641 self.send_command_test_helper( 

1642 exec_command, 

1643 expected_command={ 

1644 'kind': '_if', 

1645 'args': { 

1646 'lhs': { 

1647 'kind': 'named_pin', 

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

1649 }, 

1650 'op': 'is', 

1651 'rhs': 0, 

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

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

1654 } 

1655 }, 

1656 extra_rpc_args={}, 

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

1658 

1659 def test_if_statement_with_named_pin_not_found(self): 

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

1661 def exec_command(): 

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

1663 self.send_command_test_helper( 

1664 exec_command, 

1665 expected_command=None, 

1666 extra_rpc_args={}, 

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

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

1669 

1670 def test_if_statement_with_sequences(self): 

1671 '''Test if_statement command with sequences''' 

1672 def exec_command(): 

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

1674 self.send_command_test_helper( 

1675 exec_command, 

1676 expected_command={ 

1677 'kind': '_if', 

1678 'args': { 

1679 'lhs': 'pin10', 

1680 'op': '<', 

1681 'rhs': 0, 

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

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

1684 } 

1685 }, 

1686 extra_rpc_args={}, 

1687 mock_api_response=[ 

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

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

1690 ]) 

1691 

1692 def test_if_statement_with_sequence_not_found(self): 

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

1694 def exec_command(): 

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

1696 self.send_command_test_helper( 

1697 exec_command, 

1698 expected_command=None, 

1699 extra_rpc_args={}, 

1700 mock_api_response=[]) 

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

1702 

1703 def test_if_statement_invalid_operator(self): 

1704 '''Test if_statement command: invalid operator''' 

1705 def exec_command(): 

1706 with self.assertRaises(ValueError) as cm: 

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

1708 self.assertEqual( 

1709 cm.exception.args[0], 

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

1711 self.send_command_test_helper( 

1712 exec_command, 

1713 expected_command=None, 

1714 extra_rpc_args={}, 

1715 mock_api_response=[]) 

1716 

1717 def test_if_statement_invalid_variable(self): 

1718 '''Test if_statement command: invalid variable''' 

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

1720 def exec_command(): 

1721 with self.assertRaises(ValueError) as cm: 

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

1723 self.assertEqual( 

1724 cm.exception.args[0], 

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

1726 self.send_command_test_helper( 

1727 exec_command, 

1728 expected_command=None, 

1729 extra_rpc_args={}, 

1730 mock_api_response=[]) 

1731 

1732 def test_if_statement_invalid_named_pin_type(self): 

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

1734 def exec_command(): 

1735 with self.assertRaises(ValueError) as cm: 

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

1737 self.assertEqual( 

1738 cm.exception.args[0], 

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

1740 self.send_command_test_helper( 

1741 exec_command, 

1742 expected_command=None, 

1743 extra_rpc_args={}, 

1744 mock_api_response=[]) 

1745 

1746 def test_rpc_error(self): 

1747 '''Test rpc error handling''' 

1748 def exec_command(): 

1749 self.fb.wait(100) 

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

1751 self.send_command_test_helper( 

1752 exec_command, 

1753 error=True, 

1754 expected_command={ 

1755 'kind': 'wait', 

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

1757 extra_rpc_args={}, 

1758 mock_api_response=[]) 

1759 

1760 def test_rpc_response_timeout(self): 

1761 '''Test rpc response timeout handling''' 

1762 def exec_command(): 

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

1764 self.fb.wait(100) 

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

1766 self.send_command_test_helper( 

1767 exec_command, 

1768 expected_command={ 

1769 'kind': 'wait', 

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

1771 extra_rpc_args={}, 

1772 mock_api_response=[]) 

1773 

1774 def test_set_verbosity(self): 

1775 '''Test set_verbosity.''' 

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

1777 self.fb.set_verbosity(1) 

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

1779 

1780 def test_set_timeout(self): 

1781 '''Test set_timeout.''' 

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

1783 self.fb.set_timeout(15) 

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

1785 

1786 @staticmethod 

1787 def helper_get_print_strings(mock_print): 

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

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

1790 

1791 @patch('builtins.print') 

1792 def test_print_status(self, mock_print): 

1793 '''Test print_status.''' 

1794 self.fb.set_verbosity(0) 

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

1796 mock_print.assert_not_called() 

1797 self.fb.set_verbosity(1) 

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

1799 call_strings = self.helper_get_print_strings(mock_print) 

1800 self.assertIn('testing', call_strings) 

1801 mock_print.reset_mock() 

1802 self.fb.set_verbosity(2) 

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

1804 call_strings = self.helper_get_print_strings(mock_print) 

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

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

1807 self.assertIn('test_print_status', call_strings)