Coverage for tests/tests_main.py: 100%

720 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-08-31 14:00 -0700

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 

23 

24class TestFarmbot(unittest.TestCase): 

25 '''Farmbot tests''' 

26 

27 def setUp(self): 

28 '''Set up method called before each test case''' 

29 self.fb = Farmbot() 

30 self.fb.set_token(MOCK_TOKEN) 

31 self.fb.set_verbosity(0) 

32 self.fb.state.test_env = True 

33 self.fb.state.broker_listen_duration = 0 

34 

35 @patch('requests.post') 

36 def test_get_token_default_server(self, mock_post): 

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

38 mock_response = Mock() 

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

40 mock_response.json.return_value = expected_token 

41 mock_response.status_code = 200 

42 mock_response.text = 'text' 

43 mock_post.return_value = mock_response 

44 self.fb.set_token(None) 

45 # Call with default server 

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

47 mock_post.assert_called_once_with( 

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

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

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

51 'password': 'test_pass_123'}} 

52 ) 

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

54 

55 @patch('requests.post') 

56 def test_get_token_custom_server(self, mock_post): 

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

58 mock_response = Mock() 

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

60 mock_response.json.return_value = expected_token 

61 mock_response.status_code = 200 

62 mock_response.text = 'text' 

63 mock_post.return_value = mock_response 

64 self.fb.set_token(None) 

65 # Call with custom server 

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

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

68 mock_post.assert_called_once_with( 

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

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

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

72 'password': 'test_pass_123'}} 

73 ) 

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

75 

76 @patch('requests.post') 

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

78 '''Test helper for get_token errors''' 

79 mock_post = args[0] 

80 status_code = kwargs['status_code'] 

81 error_msg = kwargs['error_msg'] 

82 mock_response = Mock() 

83 mock_response.status_code = status_code 

84 mock_post.return_value = mock_response 

85 self.fb.set_token(None) 

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

87 mock_post.assert_called_once_with( 

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

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

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

91 'password': 'test_pass_123'}} 

92 ) 

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

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

95 

96 def test_get_token_bad_email(self): 

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

98 self.helper_get_token_errors( 

99 status_code=422, 

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

101 ) 

102 

103 def test_get_token_bad_server(self): 

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

105 self.helper_get_token_errors( 

106 status_code=404, 

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

108 ) 

109 

110 def test_get_token_other_error(self): 

111 '''get_token: other error''' 

112 self.helper_get_token_errors( 

113 status_code=500, 

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

115 ) 

116 

117 @patch('requests.post') 

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

119 '''Test helper for get_token exceptions''' 

120 mock_post = args[0] 

121 exception = kwargs['exception'] 

122 error_msg = kwargs['error_msg'] 

123 mock_post.side_effect = exception 

124 self.fb.set_token(None) 

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

126 mock_post.assert_called_once_with( 

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

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

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

130 'password': 'test_pass_123'}} 

131 ) 

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

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

134 

135 def test_get_token_server_not_found(self): 

136 '''get_token: server not found''' 

137 self.helper_get_token_exceptions( 

138 exception=requests.exceptions.ConnectionError, 

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

140 ) 

141 

142 def test_get_token_timeout(self): 

143 '''get_token: timeout''' 

144 self.helper_get_token_exceptions( 

145 exception=requests.exceptions.Timeout, 

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

147 ) 

148 

149 def test_get_token_problem(self): 

150 '''get_token: problem''' 

151 self.helper_get_token_exceptions( 

152 exception=requests.exceptions.RequestException, 

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

154 ) 

155 

156 def test_get_token_other_exception(self): 

157 '''get_token: other exception''' 

158 self.helper_get_token_exceptions( 

159 exception=Exception('other'), 

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

161 ) 

162 

163 @patch('requests.request') 

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

165 '''Test helper for api_get errors''' 

166 mock_request = args[0] 

167 status_code = kwargs['status_code'] 

168 error_msg = kwargs['error_msg'] 

169 mock_response = Mock() 

170 mock_response.status_code = status_code 

171 mock_response.reason = 'reason' 

172 mock_response.text = 'text' 

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

174 mock_request.return_value = mock_response 

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

176 mock_request.assert_called_once_with( 

177 'GET', 

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

179 headers={ 

180 'authorization': 'encoded_token_value', 

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

182 }, 

183 json=None, 

184 ) 

185 self.assertEqual(response, error_msg) 

186 

187 def test_api_get_errors(self): 

188 '''Test api_get errors''' 

189 self.helper_api_get_error( 

190 status_code=404, 

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

192 ) 

193 self.helper_api_get_error( 

194 status_code=500, 

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

196 ) 

197 self.helper_api_get_error( 

198 status_code=600, 

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

200 ) 

201 

202 @patch('requests.request') 

203 def test_api_string_error_response_handling(self, mock_request): 

204 '''Test API string response errors''' 

205 mock_response = Mock() 

206 mock_response.status_code = 404 

207 mock_response.reason = 'reason' 

208 mock_response.text = 'error string' 

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

210 mock_request.return_value = mock_response 

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

212 mock_request.assert_called_once_with( 

213 'GET', 

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

215 headers={ 

216 'authorization': 'encoded_token_value', 

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

218 }, 

219 json=None, 

220 ) 

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

222 

223 @patch('requests.request') 

224 def test_api_string_error_response_handling_html(self, mock_request): 

225 '''Test API html string response errors''' 

226 mock_response = Mock() 

227 mock_response.status_code = 404 

228 mock_response.reason = 'reason' 

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

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

231 mock_request.return_value = mock_response 

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

233 mock_request.assert_called_once_with( 

234 'GET', 

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

236 headers={ 

237 'authorization': 'encoded_token_value', 

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

239 }, 

240 json=None, 

241 ) 

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

243 

244 @patch('requests.request') 

245 def test_api_get_endpoint_only(self, mock_request): 

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

247 mock_response = Mock() 

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

249 mock_response.json.return_value = expected_response 

250 mock_response.status_code = 200 

251 mock_response.text = 'text' 

252 mock_request.return_value = mock_response 

253 # Call with endpoint only 

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

255 mock_request.assert_called_once_with( 

256 'GET', 

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

258 headers={ 

259 'authorization': 'encoded_token_value', 

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

261 }, 

262 json=None, 

263 ) 

264 self.assertEqual(response, expected_response) 

265 

266 @patch('requests.request') 

267 def test_api_get_with_id(self, mock_request): 

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

269 mock_response = Mock() 

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

271 mock_response.json.return_value = expected_response 

272 mock_response.status_code = 200 

273 mock_response.text = 'text' 

274 mock_request.return_value = mock_response 

275 # Call with specific ID 

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

277 mock_request.assert_called_once_with( 

278 'GET', 

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

280 headers={ 

281 'authorization': 'encoded_token_value', 

282 'content-type': 'application/json', 

283 }, 

284 json=None, 

285 ) 

286 self.assertEqual(response, expected_response) 

287 

288 @patch('requests.request') 

289 def test_check_token_api_request(self, mock_request): 

290 '''Test check_token: API request''' 

291 self.fb.set_token(None) 

292 with self.assertRaises(ValueError) as cm: 

293 self.fb.api_get('points') 

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

295 mock_request.assert_not_called() 

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

297 

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

299 @patch('requests.request') 

300 def test_check_token_broker(self, mock_request, mock_mqtt): 

301 '''Test check_token: broker''' 

302 mock_client = Mock() 

303 mock_mqtt.return_value = mock_client 

304 self.fb.set_token(None) 

305 with self.assertRaises(ValueError) as cm: 

306 self.fb.on(123) 

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

308 with self.assertRaises(ValueError) as cm: 

309 self.fb.read_sensor(123) 

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

311 with self.assertRaises(ValueError) as cm: 

312 self.fb.get_xyz() 

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

314 with self.assertRaises(ValueError) as cm: 

315 self.fb.read_status() 

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

317 mock_request.assert_not_called() 

318 mock_client.publish.assert_not_called() 

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

320 

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

322 def test_publish_disabled(self, mock_mqtt): 

323 '''Test publish disabled''' 

324 mock_client = Mock() 

325 mock_mqtt.return_value = mock_client 

326 self.fb.state.dry_run = True 

327 self.fb.on(123) 

328 mock_client.publish.assert_not_called() 

329 

330 @patch('requests.request') 

331 def test_api_patch(self, mock_request): 

332 '''test api_patch function''' 

333 mock_response = Mock() 

334 mock_response.status_code = 200 

335 mock_response.text = 'text' 

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

337 mock_request.return_value = mock_response 

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

339 mock_request.assert_has_calls([call( 

340 'PATCH', 

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

342 headers={ 

343 'authorization': 'encoded_token_value', 

344 'content-type': 'application/json', 

345 }, 

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

347 ), 

348 call().json(), 

349 ]) 

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

351 

352 @patch('requests.request') 

353 def test_api_post(self, mock_request): 

354 '''test api_post function''' 

355 mock_response = Mock() 

356 mock_response.status_code = 200 

357 mock_response.text = 'text' 

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

359 mock_request.return_value = mock_response 

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

361 mock_request.assert_has_calls([call( 

362 'POST', 

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

364 headers={ 

365 'authorization': 'encoded_token_value', 

366 'content-type': 'application/json', 

367 }, 

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

369 ), 

370 call().json(), 

371 ]) 

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

373 

374 @patch('requests.request') 

375 def test_api_delete(self, mock_request): 

376 '''test api_delete function''' 

377 mock_response = Mock() 

378 mock_response.status_code = 200 

379 mock_response.text = 'text' 

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

381 mock_request.return_value = mock_response 

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

383 mock_request.assert_called_once_with( 

384 'DELETE', 

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

386 headers={ 

387 'authorization': 'encoded_token_value', 

388 'content-type': 'application/json', 

389 }, 

390 json=None, 

391 ) 

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

393 

394 @patch('requests.request') 

395 def test_api_delete_requests_disabled(self, mock_request): 

396 '''test api_delete function: requests disabled''' 

397 self.fb.state.dry_run = True 

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

399 mock_request.assert_not_called() 

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

401 

402 @patch('requests.request') 

403 def test_group_one(self, mock_request): 

404 '''test group function: get one group''' 

405 mock_response = Mock() 

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

407 mock_response.status_code = 200 

408 mock_response.text = 'text' 

409 mock_request.return_value = mock_response 

410 group_info = self.fb.group(12345) 

411 mock_request.assert_called_once_with( 

412 'GET', 

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

414 headers={ 

415 'authorization': 'encoded_token_value', 

416 'content-type': 'application/json', 

417 }, 

418 json=None, 

419 ) 

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

421 

422 @patch('requests.request') 

423 def test_group_all(self, mock_request): 

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

425 mock_response = Mock() 

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

427 mock_response.status_code = 200 

428 mock_response.text = 'text' 

429 mock_request.return_value = mock_response 

430 group_info = self.fb.group() 

431 mock_request.assert_called_once_with( 

432 'GET', 

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

434 headers={ 

435 'authorization': 'encoded_token_value', 

436 'content-type': 'application/json', 

437 }, 

438 json=None, 

439 ) 

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

441 

442 @patch('requests.request') 

443 def test_curve_one(self, mock_request): 

444 '''test curve function: get one curve''' 

445 mock_response = Mock() 

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

447 mock_response.status_code = 200 

448 mock_response.text = 'text' 

449 mock_request.return_value = mock_response 

450 curve_info = self.fb.curve(12345) 

451 mock_request.assert_called_once_with( 

452 'GET', 

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

454 headers={ 

455 'authorization': 'encoded_token_value', 

456 'content-type': 'application/json', 

457 }, 

458 json=None, 

459 ) 

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

461 

462 @patch('requests.request') 

463 def test_curve_all(self, mock_request): 

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

465 mock_response = Mock() 

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

467 mock_response.status_code = 200 

468 mock_response.text = 'text' 

469 mock_request.return_value = mock_response 

470 curve_info = self.fb.curve() 

471 mock_request.assert_called_once_with( 

472 'GET', 

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

474 headers={ 

475 'authorization': 'encoded_token_value', 

476 'content-type': 'application/json', 

477 }, 

478 json=None, 

479 ) 

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

481 

482 @patch('requests.request') 

483 def test_safe_z(self, mock_request): 

484 '''test safe_z function''' 

485 mock_response = Mock() 

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

487 mock_response.status_code = 200 

488 mock_response.text = 'text' 

489 mock_request.return_value = mock_response 

490 safe_height = self.fb.safe_z() 

491 mock_request.assert_called_once_with( 

492 'GET', 

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

494 headers={ 

495 'authorization': 'encoded_token_value', 

496 'content-type': 'application/json', 

497 }, 

498 json=None, 

499 ) 

500 self.assertEqual(safe_height, 100) 

501 

502 @patch('requests.request') 

503 def test_garden_size(self, mock_request): 

504 '''test garden_size function''' 

505 mock_response = Mock() 

506 mock_response.json.return_value = { 

507 'movement_axis_nr_steps_x': 1000, 

508 'movement_axis_nr_steps_y': 2000, 

509 'movement_axis_nr_steps_z': 40000, 

510 'movement_step_per_mm_x': 5, 

511 'movement_step_per_mm_y': 5, 

512 'movement_step_per_mm_z': 25, 

513 } 

514 mock_response.status_code = 200 

515 mock_response.text = 'text' 

516 mock_request.return_value = mock_response 

517 garden_size = self.fb.garden_size() 

518 mock_request.assert_called_once_with( 

519 'GET', 

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

521 headers={ 

522 'authorization': 'encoded_token_value', 

523 'content-type': 'application/json', 

524 }, 

525 json=None, 

526 ) 

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

528 

529 @patch('requests.request') 

530 def test_log(self, mock_request): 

531 '''test log function''' 

532 mock_response = Mock() 

533 mock_response.status_code = 200 

534 mock_response.text = 'text' 

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

536 mock_request.return_value = mock_response 

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

538 mock_request.assert_called_once_with( 

539 'POST', 

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

541 headers={ 

542 'authorization': 'encoded_token_value', 

543 'content-type': 'application/json', 

544 }, 

545 json={ 

546 'message': 'test message', 

547 'type': 'info', 

548 'channels': ['toast'], 

549 }, 

550 ) 

551 

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

553 def test_connect_broker(self, mock_mqtt): 

554 '''Test test_connect_broker command''' 

555 mock_client = Mock() 

556 mock_mqtt.return_value = mock_client 

557 self.fb.connect_broker() 

558 mock_client.username_pw_set.assert_called_once_with( 

559 username='device_0', 

560 password='encoded_token_value') 

561 mock_client.connect.assert_called_once_with( 

562 'mqtt_url', 

563 port=1883, 

564 keepalive=60) 

565 mock_client.loop_start.assert_called() 

566 

567 def test_disconnect_broker(self): 

568 '''Test disconnect_broker command''' 

569 mock_client = Mock() 

570 self.fb.broker.client = mock_client 

571 self.fb.disconnect_broker() 

572 mock_client.loop_stop.assert_called_once() 

573 mock_client.disconnect.assert_called_once() 

574 

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

576 def test_listen(self, mock_mqtt): 

577 '''Test listen command''' 

578 mock_client = Mock() 

579 mock_mqtt.return_value = mock_client 

580 self.fb.listen() 

581 

582 class MockMessage: 

583 '''Mock message class''' 

584 topic = 'topic' 

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

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

587 mock_client.username_pw_set.assert_called_once_with( 

588 username='device_0', 

589 password='encoded_token_value') 

590 mock_client.connect.assert_called_once_with( 

591 'mqtt_url', 

592 port=1883, 

593 keepalive=60) 

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

595 mock_client.loop_start.assert_called() 

596 mock_client.loop_stop.assert_called() 

597 

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

599 def test_listen_clear_last(self, mock_mqtt): 

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

601 mock_client = Mock() 

602 mock_mqtt.return_value = mock_client 

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

604 self.fb.state.test_env = False 

605 self.fb.listen() 

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

607 

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

609 def test_publish_apply_label(self, mock_mqtt): 

610 '''Test publish command: set uuid''' 

611 mock_client = Mock() 

612 mock_mqtt.return_value = mock_client 

613 self.fb.state.test_env = False 

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

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

616 

617 @patch('requests.request') 

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

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

620 '''Helper for testing command execution''' 

621 execute_command = args[0] 

622 mock_mqtt = args[1] 

623 mock_request = args[2] 

624 expected_command = kwargs.get('expected_command') 

625 extra_rpc_args = kwargs.get('extra_rpc_args') 

626 mock_api_response = kwargs.get('mock_api_response') 

627 error = kwargs.get('error') 

628 mock_client = Mock() 

629 mock_mqtt.return_value = mock_client 

630 mock_response = Mock() 

631 mock_response.json.return_value = mock_api_response 

632 mock_response.status_code = 200 

633 mock_response.text = 'text' 

634 mock_request.return_value = mock_response 

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

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

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

638 } 

639 execute_command() 

640 if expected_command is None: 

641 mock_client.publish.assert_not_called() 

642 return 

643 expected_payload = { 

644 'kind': 'rpc_request', 

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

646 'body': [expected_command], 

647 } 

648 mock_client.username_pw_set.assert_called_once_with( 

649 username='device_0', 

650 password='encoded_token_value') 

651 mock_client.connect.assert_called_once_with( 

652 'mqtt_url', 

653 port=1883, 

654 keepalive=60) 

655 mock_client.loop_start.assert_called() 

656 mock_client.publish.assert_called_once_with( 

657 'bot/device_0/from_clients', 

658 payload=json.dumps(expected_payload)) 

659 if not error: 

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

661 

662 def test_message(self): 

663 '''Test message command''' 

664 def exec_command(): 

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

666 self.send_command_test_helper( 

667 exec_command, 

668 expected_command={ 

669 'kind': 'send_message', 

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

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

672 }, 

673 extra_rpc_args={}, 

674 mock_api_response={}) 

675 

676 def test_debug(self): 

677 '''Test debug command''' 

678 def exec_command(): 

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

680 self.send_command_test_helper( 

681 exec_command, 

682 expected_command={ 

683 'kind': 'send_message', 

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

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

686 }, 

687 extra_rpc_args={}, 

688 mock_api_response={}) 

689 

690 def test_toast(self): 

691 '''Test toast command''' 

692 def exec_command(): 

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

694 self.send_command_test_helper( 

695 exec_command, 

696 expected_command={ 

697 'kind': 'send_message', 

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

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

700 }, 

701 extra_rpc_args={}, 

702 mock_api_response={}) 

703 

704 def test_invalid_message_type(self): 

705 '''Test message_type validation''' 

706 def exec_command(): 

707 with self.assertRaises(ValueError) as cm: 

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

709 self.assertEqual( 

710 cm.exception.args[0], 

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

712 self.send_command_test_helper( 

713 exec_command, 

714 expected_command=None, 

715 extra_rpc_args={}, 

716 mock_api_response={}) 

717 

718 def test_invalid_message_channel(self): 

719 '''Test message channel validation''' 

720 def exec_command(): 

721 with self.assertRaises(ValueError) as cm: 

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

723 self.assertEqual( 

724 cm.exception.args[0], 

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

726 self.send_command_test_helper( 

727 exec_command, 

728 expected_command=None, 

729 extra_rpc_args={}, 

730 mock_api_response={}) 

731 

732 def test_read_status(self): 

733 '''Test read_status command''' 

734 def exec_command(): 

735 self.fb.read_status() 

736 self.send_command_test_helper( 

737 exec_command, 

738 expected_command={ 

739 'kind': 'read_status', 

740 'args': {}, 

741 }, 

742 extra_rpc_args={}, 

743 mock_api_response={}) 

744 

745 def test_read_sensor(self): 

746 '''Test read_sensor command''' 

747 def exec_command(): 

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

749 self.send_command_test_helper( 

750 exec_command, 

751 expected_command={ 

752 'kind': 'read_pin', 

753 'args': { 

754 'pin_mode': 0, 

755 'label': '---', 

756 'pin_number': { 

757 'kind': 'named_pin', 

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

759 }, 

760 }, 

761 }, 

762 extra_rpc_args={}, 

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

764 

765 def test_read_sensor_not_found(self): 

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

767 def exec_command(): 

768 self.fb.read_sensor('Temperature') 

769 self.send_command_test_helper( 

770 exec_command, 

771 expected_command=None, 

772 extra_rpc_args={}, 

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

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

775 

776 def test_assertion(self): 

777 '''Test assertion command''' 

778 def exec_command(): 

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

780 self.send_command_test_helper( 

781 exec_command, 

782 expected_command={ 

783 'kind': 'assertion', 

784 'args': { 

785 'assertion_type': 'abort', 

786 'lua': 'return true', 

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

788 } 

789 }, 

790 extra_rpc_args={}, 

791 mock_api_response={}) 

792 

793 def test_assertion_with_recovery_sequence(self): 

794 '''Test assertion command with recovery sequence''' 

795 def exec_command(): 

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

797 self.send_command_test_helper( 

798 exec_command, 

799 expected_command={ 

800 'kind': 'assertion', 

801 'args': { 

802 'assertion_type': 'abort', 

803 'lua': 'return true', 

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

805 } 

806 }, 

807 extra_rpc_args={}, 

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

809 

810 def test_assertion_recovery_sequence_not_found(self): 

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

812 def exec_command(): 

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

814 self.send_command_test_helper( 

815 exec_command, 

816 expected_command=None, 

817 extra_rpc_args={}, 

818 mock_api_response=[]) 

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

820 

821 def test_assertion_invalid_assertion_type(self): 

822 '''Test assertion command: invalid assertion type''' 

823 def exec_command(): 

824 with self.assertRaises(ValueError) as cm: 

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

826 self.assertEqual( 

827 cm.exception.args[0], 

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

829 self.send_command_test_helper( 

830 exec_command, 

831 expected_command=None, 

832 extra_rpc_args={}, 

833 mock_api_response={}) 

834 

835 def test_wait(self): 

836 '''Test wait command''' 

837 def exec_command(): 

838 self.fb.wait(123) 

839 self.send_command_test_helper( 

840 exec_command, 

841 expected_command={ 

842 'kind': 'wait', 

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

844 }, 

845 extra_rpc_args={}, 

846 mock_api_response={}) 

847 

848 def test_unlock(self): 

849 '''Test unlock command''' 

850 def exec_command(): 

851 self.fb.unlock() 

852 self.send_command_test_helper( 

853 exec_command, 

854 expected_command={ 

855 'kind': 'emergency_unlock', 

856 'args': {}, 

857 }, 

858 extra_rpc_args={'priority': 9000}, 

859 mock_api_response={}) 

860 

861 def test_e_stop(self): 

862 '''Test e_stop command''' 

863 def exec_command(): 

864 self.fb.e_stop() 

865 self.send_command_test_helper( 

866 exec_command, 

867 expected_command={ 

868 'kind': 'emergency_lock', 

869 'args': {}, 

870 }, 

871 extra_rpc_args={'priority': 9000}, 

872 mock_api_response={}) 

873 

874 def test_find_home(self): 

875 '''Test find_home command''' 

876 def exec_command(): 

877 self.fb.find_home() 

878 self.send_command_test_helper( 

879 exec_command, 

880 expected_command={ 

881 'kind': 'find_home', 

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

883 }, 

884 extra_rpc_args={}, 

885 mock_api_response={}) 

886 

887 def test_find_home_speed_error(self): 

888 '''Test find_home command: speed error''' 

889 def exec_command(): 

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

891 self.send_command_test_helper( 

892 exec_command, 

893 expected_command=None, 

894 extra_rpc_args={}, 

895 mock_api_response={}) 

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

897 

898 def test_find_home_invalid_axis(self): 

899 '''Test find_home command: invalid axis''' 

900 def exec_command(): 

901 with self.assertRaises(ValueError) as cm: 

902 self.fb.find_home('nope') 

903 self.assertEqual( 

904 cm.exception.args[0], 

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

906 self.send_command_test_helper( 

907 exec_command, 

908 expected_command=None, 

909 extra_rpc_args={}, 

910 mock_api_response={}) 

911 

912 def test_set_home(self): 

913 '''Test set_home command''' 

914 def exec_command(): 

915 self.fb.set_home() 

916 self.send_command_test_helper( 

917 exec_command, 

918 expected_command={ 

919 'kind': 'zero', 

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

921 }, 

922 extra_rpc_args={}, 

923 mock_api_response={}) 

924 

925 def test_toggle_peripheral(self): 

926 '''Test toggle_peripheral command''' 

927 def exec_command(): 

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

929 self.send_command_test_helper( 

930 exec_command, 

931 expected_command={ 

932 'kind': 'toggle_pin', 

933 'args': { 

934 'pin_number': { 

935 'kind': 'named_pin', 

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

937 }, 

938 }, 

939 }, 

940 extra_rpc_args={}, 

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

942 

943 def test_toggle_peripheral_not_found(self): 

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

945 def exec_command(): 

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

947 self.send_command_test_helper( 

948 exec_command, 

949 expected_command=None, 

950 extra_rpc_args={}, 

951 mock_api_response=[]) 

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

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 mock_response = Mock() 

1331 mock_api_response = [ 

1332 { 

1333 'id': 123, 

1334 'name': 'Seed Tray', 

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

1336 }, 

1337 { 

1338 'pointer_type': 'ToolSlot', 

1339 'pullout_direction': 1, 

1340 'x': 0, 

1341 'y': 0, 

1342 'z': 0, 

1343 'tool_id': 123, 

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

1345 **tray_data, 

1346 }, 

1347 ] 

1348 mock_response.json.return_value = mock_api_response 

1349 mock_response.status_code = 200 

1350 mock_response.text = 'text' 

1351 mock_request.return_value = mock_response 

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

1353 mock_request.assert_has_calls([ 

1354 call( 

1355 'GET', 

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

1357 headers={ 

1358 'authorization': 'encoded_token_value', 

1359 'content-type': 'application/json', 

1360 }, 

1361 json=None, 

1362 ), 

1363 call().json(), 

1364 call( 

1365 'GET', 

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

1367 headers={ 

1368 'authorization': 'encoded_token_value', 

1369 'content-type': 'application/json', 

1370 }, 

1371 json=None, 

1372 ), 

1373 call().json(), 

1374 ]) 

1375 self.assertEqual(cell, expected_xyz, kwargs) 

1376 

1377 def test_get_seed_tray_cell(self): 

1378 '''Test get_seed_tray_cell''' 

1379 test_cases = [ 

1380 { 

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

1382 'cell': 'a1', 

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

1384 }, 

1385 { 

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

1387 'cell': 'b2', 

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

1389 }, 

1390 { 

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

1392 'cell': 'd4', 

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

1394 }, 

1395 { 

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

1397 'cell': 'a1', 

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

1399 }, 

1400 { 

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

1402 'cell': 'b2', 

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

1404 }, 

1405 { 

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

1407 'cell': 'd4', 

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

1409 }, 

1410 { 

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

1412 'cell': 'd4', 

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

1414 }, 

1415 ] 

1416 for test_case in test_cases: 

1417 self.helper_get_seed_tray_cell(**test_case) 

1418 

1419 @patch('requests.request') 

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

1421 '''Test helper for get_seed_tray_cell command errors''' 

1422 mock_request = args[0] 

1423 tray_data = kwargs['tray_data'] 

1424 cell = kwargs['cell'] 

1425 error = kwargs['error'] 

1426 mock_response = Mock() 

1427 mock_api_response = [ 

1428 { 

1429 'id': 123, 

1430 'name': 'Seed Tray', 

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

1432 }, 

1433 { 

1434 'pointer_type': 'ToolSlot', 

1435 'pullout_direction': 1, 

1436 'x': 0, 

1437 'y': 0, 

1438 'z': 0, 

1439 'tool_id': 123, 

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

1441 **tray_data, 

1442 }, 

1443 ] 

1444 mock_response.json.return_value = mock_api_response 

1445 mock_response.status_code = 200 

1446 mock_response.text = 'text' 

1447 mock_request.return_value = mock_response 

1448 with self.assertRaises(ValueError) as cm: 

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

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

1451 mock_request.assert_has_calls([ 

1452 call( 

1453 'GET', 

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

1455 headers={ 

1456 'authorization': 'encoded_token_value', 

1457 'content-type': 'application/json', 

1458 }, 

1459 json=None, 

1460 ), 

1461 call().json(), 

1462 call( 

1463 'GET', 

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

1465 headers={ 

1466 'authorization': 'encoded_token_value', 

1467 'content-type': 'application/json', 

1468 }, 

1469 json=None, 

1470 ), 

1471 call().json(), 

1472 ]) 

1473 

1474 def test_get_seed_tray_cell_invalid_cell_name(self): 

1475 '''Test get_seed_tray_cell: invalid cell name''' 

1476 self.helper_get_seed_tray_cell_error( 

1477 tray_data={}, 

1478 cell='e4', 

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

1480 ) 

1481 

1482 def test_get_seed_tray_cell_invalid_pullout_direction(self): 

1483 '''Test get_seed_tray_cell: invalid pullout direction''' 

1484 self.helper_get_seed_tray_cell_error( 

1485 tray_data={'pullout_direction': 0}, 

1486 cell='d4', 

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

1488 ) 

1489 

1490 @patch('requests.request') 

1491 def test_get_seed_tray_cell_no_tray(self, mock_request): 

1492 '''Test get_seed_tray_cell: no seed tray''' 

1493 mock_response = Mock() 

1494 mock_api_response = [] 

1495 mock_response.json.return_value = mock_api_response 

1496 mock_response.status_code = 200 

1497 mock_response.text = 'text' 

1498 mock_request.return_value = mock_response 

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

1500 mock_request.assert_has_calls([ 

1501 call( 

1502 'GET', 

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

1504 headers={ 

1505 'authorization': 'encoded_token_value', 

1506 'content-type': 'application/json', 

1507 }, 

1508 json=None, 

1509 ), 

1510 call().json(), 

1511 ]) 

1512 self.assertIsNone(result) 

1513 

1514 @patch('requests.request') 

1515 def test_get_seed_tray_cell_not_mounted(self, mock_request): 

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

1517 mock_response = Mock() 

1518 mock_api_response = [{ 

1519 'id': 123, 

1520 'name': 'Seed Tray', 

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

1522 }] 

1523 mock_response.json.return_value = mock_api_response 

1524 mock_response.status_code = 200 

1525 mock_response.text = 'text' 

1526 mock_request.return_value = mock_response 

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

1528 mock_request.assert_has_calls([ 

1529 call( 

1530 'GET', 

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

1532 headers={ 

1533 'authorization': 'encoded_token_value', 

1534 'content-type': 'application/json', 

1535 }, 

1536 json=None, 

1537 ), 

1538 call().json(), 

1539 ]) 

1540 self.assertIsNone(result) 

1541 

1542 def test_get_job_one(self): 

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

1544 def exec_command(): 

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

1546 'jobs': { 

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

1548 }, 

1549 } 

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

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

1552 self.send_command_test_helper( 

1553 exec_command, 

1554 expected_command={ 

1555 'kind': 'read_status', 

1556 'args': {}, 

1557 }, 

1558 extra_rpc_args={}, 

1559 mock_api_response={}) 

1560 

1561 def test_get_job_all(self): 

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

1563 def exec_command(): 

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

1565 'jobs': { 

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

1567 }, 

1568 } 

1569 jobs = self.fb.get_job() 

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

1571 self.send_command_test_helper( 

1572 exec_command, 

1573 expected_command={ 

1574 'kind': 'read_status', 

1575 'args': {}, 

1576 }, 

1577 extra_rpc_args={}, 

1578 mock_api_response={}) 

1579 

1580 def test_get_job_no_status(self): 

1581 '''Test get_job command: no status''' 

1582 def exec_command(): 

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

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

1585 self.assertIsNone(job) 

1586 self.send_command_test_helper( 

1587 exec_command, 

1588 expected_command={ 

1589 'kind': 'read_status', 

1590 'args': {}, 

1591 }, 

1592 extra_rpc_args={}, 

1593 mock_api_response={}) 

1594 

1595 def test_set_job(self): 

1596 '''Test set_job command''' 

1597 def exec_command(): 

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

1599 self.send_command_test_helper( 

1600 exec_command, 

1601 expected_command={ 

1602 'kind': 'lua', 

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

1604 set_job(job_name) 

1605 

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

1607 set_job(job_name, { 

1608 status = "working", 

1609 percent = 50 

1610 })'''}, 

1611 }, 

1612 extra_rpc_args={}, 

1613 mock_api_response={}) 

1614 

1615 def test_complete_job(self): 

1616 '''Test complete_job command''' 

1617 def exec_command(): 

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

1619 self.send_command_test_helper( 

1620 exec_command, 

1621 expected_command={ 

1622 'kind': 'lua', 

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

1624 }, 

1625 extra_rpc_args={}, 

1626 mock_api_response={}) 

1627 

1628 def test_lua(self): 

1629 '''Test lua command''' 

1630 def exec_command(): 

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

1632 self.send_command_test_helper( 

1633 exec_command, 

1634 expected_command={ 

1635 'kind': 'lua', 

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

1637 }, 

1638 extra_rpc_args={}, 

1639 mock_api_response={}) 

1640 

1641 def test_if_statement(self): 

1642 '''Test if_statement command''' 

1643 def exec_command(): 

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

1645 self.send_command_test_helper( 

1646 exec_command, 

1647 expected_command={ 

1648 'kind': '_if', 

1649 'args': { 

1650 'lhs': 'pin10', 

1651 'op': 'is', 

1652 'rhs': 0, 

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

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

1655 } 

1656 }, 

1657 extra_rpc_args={}, 

1658 mock_api_response=[]) 

1659 

1660 def test_if_statement_with_named_pin(self): 

1661 '''Test if_statement command with named pin''' 

1662 def exec_command(): 

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

1664 self.send_command_test_helper( 

1665 exec_command, 

1666 expected_command={ 

1667 'kind': '_if', 

1668 'args': { 

1669 'lhs': { 

1670 'kind': 'named_pin', 

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

1672 }, 

1673 'op': 'is', 

1674 'rhs': 0, 

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

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

1677 } 

1678 }, 

1679 extra_rpc_args={}, 

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

1681 

1682 def test_if_statement_with_named_pin_not_found(self): 

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

1684 def exec_command(): 

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

1686 self.send_command_test_helper( 

1687 exec_command, 

1688 expected_command=None, 

1689 extra_rpc_args={}, 

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

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

1692 

1693 def test_if_statement_with_sequences(self): 

1694 '''Test if_statement command with sequences''' 

1695 def exec_command(): 

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

1697 self.send_command_test_helper( 

1698 exec_command, 

1699 expected_command={ 

1700 'kind': '_if', 

1701 'args': { 

1702 'lhs': 'pin10', 

1703 'op': '<', 

1704 'rhs': 0, 

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

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

1707 } 

1708 }, 

1709 extra_rpc_args={}, 

1710 mock_api_response=[ 

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

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

1713 ]) 

1714 

1715 def test_if_statement_with_sequence_not_found(self): 

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

1717 def exec_command(): 

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

1719 self.send_command_test_helper( 

1720 exec_command, 

1721 expected_command=None, 

1722 extra_rpc_args={}, 

1723 mock_api_response=[]) 

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

1725 

1726 def test_if_statement_invalid_operator(self): 

1727 '''Test if_statement command: invalid operator''' 

1728 def exec_command(): 

1729 with self.assertRaises(ValueError) as cm: 

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

1731 self.assertEqual( 

1732 cm.exception.args[0], 

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

1734 self.send_command_test_helper( 

1735 exec_command, 

1736 expected_command=None, 

1737 extra_rpc_args={}, 

1738 mock_api_response=[]) 

1739 

1740 def test_if_statement_invalid_variable(self): 

1741 '''Test if_statement command: invalid variable''' 

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

1743 def exec_command(): 

1744 with self.assertRaises(ValueError) as cm: 

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

1746 self.assertEqual( 

1747 cm.exception.args[0], 

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

1749 self.send_command_test_helper( 

1750 exec_command, 

1751 expected_command=None, 

1752 extra_rpc_args={}, 

1753 mock_api_response=[]) 

1754 

1755 def test_if_statement_invalid_named_pin_type(self): 

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

1757 def exec_command(): 

1758 with self.assertRaises(ValueError) as cm: 

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

1760 self.assertEqual( 

1761 cm.exception.args[0], 

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

1763 self.send_command_test_helper( 

1764 exec_command, 

1765 expected_command=None, 

1766 extra_rpc_args={}, 

1767 mock_api_response=[]) 

1768 

1769 def test_rpc_error(self): 

1770 '''Test rpc error handling''' 

1771 def exec_command(): 

1772 self.fb.wait(100) 

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

1774 self.send_command_test_helper( 

1775 exec_command, 

1776 error=True, 

1777 expected_command={ 

1778 'kind': 'wait', 

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

1780 extra_rpc_args={}, 

1781 mock_api_response=[]) 

1782 

1783 def test_rpc_response_timeout(self): 

1784 '''Test rpc response timeout handling''' 

1785 def exec_command(): 

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

1787 self.fb.wait(100) 

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

1789 self.send_command_test_helper( 

1790 exec_command, 

1791 expected_command={ 

1792 'kind': 'wait', 

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

1794 extra_rpc_args={}, 

1795 mock_api_response=[]) 

1796 

1797 @staticmethod 

1798 def helper_get_print_strings(mock_print): 

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

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

1801 

1802 @patch('builtins.print') 

1803 def test_print_status(self, mock_print): 

1804 '''Test print_status.''' 

1805 self.fb.set_verbosity(0) 

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

1807 mock_print.assert_not_called() 

1808 self.fb.set_verbosity(1) 

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

1810 call_strings = self.helper_get_print_strings(mock_print) 

1811 self.assertIn('testing', call_strings) 

1812 mock_print.reset_mock() 

1813 self.fb.set_verbosity(2) 

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

1815 call_strings = self.helper_get_print_strings(mock_print) 

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

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

1818 self.assertIn('test_print_status', call_strings)