Coverage for tests/tests_main.py: 100%

700 statements  

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

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.3 

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

581 mock_client.on_connect('', '', '', '') 

582 

583 class MockMessage: 

584 '''Mock message class''' 

585 topic = 'topic' 

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

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

588 mock_client.username_pw_set.assert_called_once_with( 

589 username='device_0', 

590 password='encoded_token_value') 

591 mock_client.connect.assert_called_once_with( 

592 'mqtt_url', 

593 port=1883, 

594 keepalive=60) 

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

596 mock_client.loop_start.assert_called() 

597 mock_client.loop_stop.assert_called() 

598 mock_client.disconnect.assert_called() 

599 

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

601 def test_listen_clear_last(self, mock_mqtt): 

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

603 mock_client = Mock() 

604 mock_mqtt.return_value = mock_client 

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

606 self.fb.state.test_env = False 

607 self.fb.listen(1) 

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

609 

610 @patch('requests.request') 

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

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

613 '''Helper for testing command execution''' 

614 execute_command = args[0] 

615 mock_mqtt = args[1] 

616 mock_request = args[2] 

617 expected_command = kwargs.get('expected_command') 

618 extra_rpc_args = kwargs.get('extra_rpc_args') 

619 mock_api_response = kwargs.get('mock_api_response') 

620 mock_client = Mock() 

621 mock_mqtt.return_value = mock_client 

622 mock_response = Mock() 

623 mock_response.json.return_value = mock_api_response 

624 mock_response.status_code = 200 

625 mock_response.text = 'text' 

626 mock_request.return_value = mock_response 

627 execute_command() 

628 if expected_command is None: 

629 mock_client.publish.assert_not_called() 

630 return 

631 expected_payload = { 

632 'kind': 'rpc_request', 

633 'args': {'label': '', **extra_rpc_args}, 

634 'body': [expected_command], 

635 } 

636 mock_client.username_pw_set.assert_called_once_with( 

637 username='device_0', 

638 password='encoded_token_value') 

639 mock_client.connect.assert_called_once_with( 

640 'mqtt_url', 

641 port=1883, 

642 keepalive=60) 

643 mock_client.loop_start.assert_called() 

644 mock_client.publish.assert_called_once_with( 

645 'bot/device_0/from_clients', 

646 payload=json.dumps(expected_payload)) 

647 

648 def test_message(self): 

649 '''Test message command''' 

650 def exec_command(): 

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

652 self.send_command_test_helper( 

653 exec_command, 

654 expected_command={ 

655 'kind': 'send_message', 

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

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

658 }, 

659 extra_rpc_args={}, 

660 mock_api_response={}) 

661 

662 def test_debug(self): 

663 '''Test debug command''' 

664 def exec_command(): 

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

666 self.send_command_test_helper( 

667 exec_command, 

668 expected_command={ 

669 'kind': 'send_message', 

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

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

672 }, 

673 extra_rpc_args={}, 

674 mock_api_response={}) 

675 

676 def test_toast(self): 

677 '''Test toast command''' 

678 def exec_command(): 

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

680 self.send_command_test_helper( 

681 exec_command, 

682 expected_command={ 

683 'kind': 'send_message', 

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

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

686 }, 

687 extra_rpc_args={}, 

688 mock_api_response={}) 

689 

690 def test_invalid_message_type(self): 

691 '''Test message_type validation''' 

692 def exec_command(): 

693 with self.assertRaises(ValueError) as cm: 

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

695 self.assertEqual( 

696 cm.exception.args[0], 

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

698 self.send_command_test_helper( 

699 exec_command, 

700 expected_command=None, 

701 extra_rpc_args={}, 

702 mock_api_response={}) 

703 

704 def test_invalid_message_channel(self): 

705 '''Test message channel validation''' 

706 def exec_command(): 

707 with self.assertRaises(ValueError) as cm: 

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

709 self.assertEqual( 

710 cm.exception.args[0], 

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

712 self.send_command_test_helper( 

713 exec_command, 

714 expected_command=None, 

715 extra_rpc_args={}, 

716 mock_api_response={}) 

717 

718 def test_read_status(self): 

719 '''Test read_status command''' 

720 def exec_command(): 

721 self.fb.read_status() 

722 self.send_command_test_helper( 

723 exec_command, 

724 expected_command={ 

725 'kind': 'read_status', 

726 'args': {}, 

727 }, 

728 extra_rpc_args={}, 

729 mock_api_response={}) 

730 

731 def test_read_sensor(self): 

732 '''Test read_sensor command''' 

733 def exec_command(): 

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

735 self.send_command_test_helper( 

736 exec_command, 

737 expected_command={ 

738 'kind': 'read_pin', 

739 'args': { 

740 'pin_mode': 0, 

741 'label': '---', 

742 'pin_number': { 

743 'kind': 'named_pin', 

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

745 }, 

746 }, 

747 }, 

748 extra_rpc_args={}, 

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

750 

751 def test_read_sensor_not_found(self): 

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

753 def exec_command(): 

754 self.fb.read_sensor('Temperature') 

755 self.send_command_test_helper( 

756 exec_command, 

757 expected_command=None, 

758 extra_rpc_args={}, 

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

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

761 

762 def test_assertion(self): 

763 '''Test assertion command''' 

764 def exec_command(): 

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

766 self.send_command_test_helper( 

767 exec_command, 

768 expected_command={ 

769 'kind': 'assertion', 

770 'args': { 

771 'assertion_type': 'abort', 

772 'lua': 'return true', 

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

774 } 

775 }, 

776 extra_rpc_args={}, 

777 mock_api_response={}) 

778 

779 def test_assertion_with_recovery_sequence(self): 

780 '''Test assertion command with recovery sequence''' 

781 def exec_command(): 

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

783 self.send_command_test_helper( 

784 exec_command, 

785 expected_command={ 

786 'kind': 'assertion', 

787 'args': { 

788 'assertion_type': 'abort', 

789 'lua': 'return true', 

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

791 } 

792 }, 

793 extra_rpc_args={}, 

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

795 

796 def test_assertion_recovery_sequence_not_found(self): 

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

798 def exec_command(): 

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

800 self.send_command_test_helper( 

801 exec_command, 

802 expected_command=None, 

803 extra_rpc_args={}, 

804 mock_api_response=[]) 

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

806 

807 def test_assertion_invalid_assertion_type(self): 

808 '''Test assertion command: invalid assertion type''' 

809 def exec_command(): 

810 with self.assertRaises(ValueError) as cm: 

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

812 self.assertEqual( 

813 cm.exception.args[0], 

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

815 self.send_command_test_helper( 

816 exec_command, 

817 expected_command=None, 

818 extra_rpc_args={}, 

819 mock_api_response={}) 

820 

821 def test_wait(self): 

822 '''Test wait command''' 

823 def exec_command(): 

824 self.fb.wait(123) 

825 self.send_command_test_helper( 

826 exec_command, 

827 expected_command={ 

828 'kind': 'wait', 

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

830 }, 

831 extra_rpc_args={}, 

832 mock_api_response={}) 

833 

834 def test_unlock(self): 

835 '''Test unlock command''' 

836 def exec_command(): 

837 self.fb.unlock() 

838 self.send_command_test_helper( 

839 exec_command, 

840 expected_command={ 

841 'kind': 'emergency_unlock', 

842 'args': {}, 

843 }, 

844 extra_rpc_args={'priority': 9000}, 

845 mock_api_response={}) 

846 

847 def test_e_stop(self): 

848 '''Test e_stop command''' 

849 def exec_command(): 

850 self.fb.e_stop() 

851 self.send_command_test_helper( 

852 exec_command, 

853 expected_command={ 

854 'kind': 'emergency_lock', 

855 'args': {}, 

856 }, 

857 extra_rpc_args={'priority': 9000}, 

858 mock_api_response={}) 

859 

860 def test_find_home(self): 

861 '''Test find_home command''' 

862 def exec_command(): 

863 self.fb.find_home() 

864 self.send_command_test_helper( 

865 exec_command, 

866 expected_command={ 

867 'kind': 'find_home', 

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

869 }, 

870 extra_rpc_args={}, 

871 mock_api_response={}) 

872 

873 def test_find_home_speed_error(self): 

874 '''Test find_home command: speed error''' 

875 def exec_command(): 

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

877 self.send_command_test_helper( 

878 exec_command, 

879 expected_command=None, 

880 extra_rpc_args={}, 

881 mock_api_response={}) 

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

883 

884 def test_find_home_invalid_axis(self): 

885 '''Test find_home command: invalid axis''' 

886 def exec_command(): 

887 with self.assertRaises(ValueError) as cm: 

888 self.fb.find_home('nope') 

889 self.assertEqual( 

890 cm.exception.args[0], 

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

892 self.send_command_test_helper( 

893 exec_command, 

894 expected_command=None, 

895 extra_rpc_args={}, 

896 mock_api_response={}) 

897 

898 def test_set_home(self): 

899 '''Test set_home command''' 

900 def exec_command(): 

901 self.fb.set_home() 

902 self.send_command_test_helper( 

903 exec_command, 

904 expected_command={ 

905 'kind': 'zero', 

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

907 }, 

908 extra_rpc_args={}, 

909 mock_api_response={}) 

910 

911 def test_toggle_peripheral(self): 

912 '''Test toggle_peripheral command''' 

913 def exec_command(): 

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

915 self.send_command_test_helper( 

916 exec_command, 

917 expected_command={ 

918 'kind': 'toggle_pin', 

919 'args': { 

920 'pin_number': { 

921 'kind': 'named_pin', 

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

923 }, 

924 }, 

925 }, 

926 extra_rpc_args={}, 

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

928 

929 def test_toggle_peripheral_not_found(self): 

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

931 def exec_command(): 

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

933 self.send_command_test_helper( 

934 exec_command, 

935 expected_command=None, 

936 extra_rpc_args={}, 

937 mock_api_response=[]) 

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

939 

940 def test_on_digital(self): 

941 '''Test on command: digital''' 

942 def exec_command(): 

943 self.fb.on(13) 

944 self.send_command_test_helper( 

945 exec_command, 

946 expected_command={ 

947 'kind': 'write_pin', 

948 'args': { 

949 'pin_value': 1, 

950 'pin_mode': 0, 

951 'pin_number': 13, 

952 }, 

953 }, 

954 extra_rpc_args={}, 

955 mock_api_response={}) 

956 

957 def test_off(self): 

958 '''Test off command''' 

959 def exec_command(): 

960 self.fb.off(13) 

961 self.send_command_test_helper( 

962 exec_command, 

963 expected_command={ 

964 'kind': 'write_pin', 

965 'args': { 

966 'pin_value': 0, 

967 'pin_mode': 0, 

968 'pin_number': 13, 

969 }, 

970 }, 

971 extra_rpc_args={}, 

972 mock_api_response={}) 

973 

974 def test_move(self): 

975 '''Test move command''' 

976 def exec_command(): 

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

978 self.send_command_test_helper( 

979 exec_command, 

980 expected_command={ 

981 'kind': 'move', 

982 'args': {}, 

983 'body': [ 

984 {'kind': 'axis_overwrite', 'args': { 

985 'axis': 'x', 

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

987 {'kind': 'axis_overwrite', 'args': { 

988 'axis': 'y', 

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

990 {'kind': 'axis_overwrite', 'args': { 

991 'axis': 'z', 

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

993 ], 

994 }, 

995 extra_rpc_args={}, 

996 mock_api_response={}) 

997 

998 def test_reboot(self): 

999 '''Test reboot command''' 

1000 def exec_command(): 

1001 self.fb.reboot() 

1002 self.send_command_test_helper( 

1003 exec_command, 

1004 expected_command={ 

1005 'kind': 'reboot', 

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

1007 }, 

1008 extra_rpc_args={}, 

1009 mock_api_response={}) 

1010 

1011 def test_shutdown(self): 

1012 '''Test shutdown command''' 

1013 def exec_command(): 

1014 self.fb.shutdown() 

1015 self.send_command_test_helper( 

1016 exec_command, 

1017 expected_command={ 

1018 'kind': 'power_off', 

1019 'args': {}, 

1020 }, 

1021 extra_rpc_args={}, 

1022 mock_api_response={}) 

1023 

1024 def test_find_axis_length(self): 

1025 '''Test find_axis_length command''' 

1026 def exec_command(): 

1027 self.fb.find_axis_length() 

1028 self.send_command_test_helper( 

1029 exec_command, 

1030 expected_command={ 

1031 'kind': 'calibrate', 

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

1033 }, 

1034 extra_rpc_args={}, 

1035 mock_api_response={}) 

1036 

1037 def test_control_peripheral(self): 

1038 '''Test control_peripheral command''' 

1039 def exec_command(): 

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

1041 self.send_command_test_helper( 

1042 exec_command, 

1043 expected_command={ 

1044 'kind': 'write_pin', 

1045 'args': { 

1046 'pin_value': 1, 

1047 'pin_mode': 0, 

1048 'pin_number': { 

1049 'kind': 'named_pin', 

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

1051 }, 

1052 }, 

1053 }, 

1054 extra_rpc_args={}, 

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

1056 

1057 def test_control_peripheral_not_found(self): 

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

1059 def exec_command(): 

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

1061 self.send_command_test_helper( 

1062 exec_command, 

1063 expected_command=None, 

1064 extra_rpc_args={}, 

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

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

1067 

1068 def test_measure_soil_height(self): 

1069 '''Test measure_soil_height command''' 

1070 def exec_command(): 

1071 self.fb.measure_soil_height() 

1072 self.send_command_test_helper( 

1073 exec_command, 

1074 expected_command={ 

1075 'kind': 'execute_script', 

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

1077 }, 

1078 extra_rpc_args={}, 

1079 mock_api_response={}) 

1080 

1081 def test_detect_weeds(self): 

1082 '''Test detect_weeds command''' 

1083 def exec_command(): 

1084 self.fb.detect_weeds() 

1085 self.send_command_test_helper( 

1086 exec_command, 

1087 expected_command={ 

1088 'kind': 'execute_script', 

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

1090 }, 

1091 extra_rpc_args={}, 

1092 mock_api_response={}) 

1093 

1094 def test_calibrate_camera(self): 

1095 '''Test calibrate_camera command''' 

1096 def exec_command(): 

1097 self.fb.calibrate_camera() 

1098 self.send_command_test_helper( 

1099 exec_command, 

1100 expected_command={ 

1101 'kind': 'execute_script', 

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

1103 }, 

1104 extra_rpc_args={}, 

1105 mock_api_response={}) 

1106 

1107 def test_sequence(self): 

1108 '''Test sequence command''' 

1109 def exec_command(): 

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

1111 self.send_command_test_helper( 

1112 exec_command, 

1113 expected_command={ 

1114 'kind': 'execute', 

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

1116 }, 

1117 extra_rpc_args={}, 

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

1119 

1120 def test_sequence_not_found(self): 

1121 '''Test sequence command: sequence not found''' 

1122 def exec_command(): 

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

1124 self.send_command_test_helper( 

1125 exec_command, 

1126 expected_command=None, 

1127 extra_rpc_args={}, 

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

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

1130 

1131 def test_take_photo(self): 

1132 '''Test take_photo command''' 

1133 def exec_command(): 

1134 self.fb.take_photo() 

1135 self.send_command_test_helper( 

1136 exec_command, 

1137 expected_command={ 

1138 'kind': 'take_photo', 

1139 'args': {}, 

1140 }, 

1141 extra_rpc_args={}, 

1142 mock_api_response={}) 

1143 

1144 def test_control_servo(self): 

1145 '''Test control_servo command''' 

1146 def exec_command(): 

1147 self.fb.control_servo(4, 100) 

1148 self.send_command_test_helper( 

1149 exec_command, 

1150 expected_command={ 

1151 'kind': 'set_servo_angle', 

1152 'args': { 

1153 'pin_number': 4, 

1154 'pin_value': 100, 

1155 }, 

1156 }, 

1157 extra_rpc_args={}, 

1158 mock_api_response={'mode': 0}) 

1159 

1160 def test_control_servo_error(self): 

1161 '''Test control_servo command: error''' 

1162 def exec_command(): 

1163 self.fb.control_servo(4, 200) 

1164 self.send_command_test_helper( 

1165 exec_command, 

1166 expected_command=None, 

1167 extra_rpc_args={}, 

1168 mock_api_response={'mode': 0}) 

1169 

1170 def test_get_xyz(self): 

1171 '''Test get_xyz command''' 

1172 def exec_command(): 

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

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

1175 } 

1176 position = self.fb.get_xyz() 

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

1178 self.send_command_test_helper( 

1179 exec_command, 

1180 expected_command={ 

1181 'kind': 'read_status', 

1182 'args': {}, 

1183 }, 

1184 extra_rpc_args={}, 

1185 mock_api_response={}) 

1186 

1187 def test_get_xyz_no_status(self): 

1188 '''Test get_xyz command: no status''' 

1189 def exec_command(): 

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

1191 position = self.fb.get_xyz() 

1192 self.assertIsNone(position) 

1193 self.send_command_test_helper( 

1194 exec_command, 

1195 expected_command={ 

1196 'kind': 'read_status', 

1197 'args': {}, 

1198 }, 

1199 extra_rpc_args={}, 

1200 mock_api_response={}) 

1201 

1202 def test_check_position(self): 

1203 '''Test check_position command: at position''' 

1204 def exec_command(): 

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

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

1207 } 

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

1209 self.assertTrue(at_position) 

1210 self.send_command_test_helper( 

1211 exec_command, 

1212 expected_command={ 

1213 'kind': 'read_status', 

1214 'args': {}, 

1215 }, 

1216 extra_rpc_args={}, 

1217 mock_api_response={}) 

1218 

1219 def test_check_position_false(self): 

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

1221 def exec_command(): 

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

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

1224 } 

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

1226 self.assertFalse(at_position) 

1227 self.send_command_test_helper( 

1228 exec_command, 

1229 expected_command={ 

1230 'kind': 'read_status', 

1231 'args': {}, 

1232 }, 

1233 extra_rpc_args={}, 

1234 mock_api_response={}) 

1235 

1236 def test_check_position_no_status(self): 

1237 '''Test check_position command: no status''' 

1238 def exec_command(): 

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

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

1241 self.assertFalse(at_position) 

1242 self.send_command_test_helper( 

1243 exec_command, 

1244 expected_command={ 

1245 'kind': 'read_status', 

1246 'args': {}, 

1247 }, 

1248 extra_rpc_args={}, 

1249 mock_api_response={}) 

1250 

1251 def test_mount_tool(self): 

1252 '''Test mount_tool command''' 

1253 def exec_command(): 

1254 self.fb.mount_tool('Weeder') 

1255 self.send_command_test_helper( 

1256 exec_command, 

1257 expected_command={ 

1258 'kind': 'lua', 

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

1260 }, 

1261 extra_rpc_args={}, 

1262 mock_api_response={}) 

1263 

1264 def test_dismount_tool(self): 

1265 '''Test dismount_tool command''' 

1266 def exec_command(): 

1267 self.fb.dismount_tool() 

1268 self.send_command_test_helper( 

1269 exec_command, 

1270 expected_command={ 

1271 'kind': 'lua', 

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

1273 }, 

1274 extra_rpc_args={}, 

1275 mock_api_response={}) 

1276 

1277 def test_water(self): 

1278 '''Test water command''' 

1279 def exec_command(): 

1280 self.fb.water(123) 

1281 self.send_command_test_helper( 

1282 exec_command, 

1283 expected_command={ 

1284 'kind': 'lua', 

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

1286 method = "GET", 

1287 url = "/api/points/123" 

1288 }) 

1289 water(plant)'''}, 

1290 }, 

1291 extra_rpc_args={}, 

1292 mock_api_response={}) 

1293 

1294 def test_dispense(self): 

1295 '''Test dispense command''' 

1296 def exec_command(): 

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

1298 self.send_command_test_helper( 

1299 exec_command, 

1300 expected_command={ 

1301 'kind': 'lua', 

1302 'args': { 

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

1304 }, 

1305 }, 

1306 extra_rpc_args={}, 

1307 mock_api_response={}) 

1308 

1309 @patch('requests.request') 

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

1311 '''Test helper for get_seed_tray_cell command''' 

1312 mock_request = args[0] 

1313 tray_data = kwargs['tray_data'] 

1314 cell = kwargs['cell'] 

1315 expected_xyz = kwargs['expected_xyz'] 

1316 mock_response = Mock() 

1317 mock_api_response = [ 

1318 { 

1319 'id': 123, 

1320 'name': 'Seed Tray', 

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

1322 }, 

1323 { 

1324 'pointer_type': 'ToolSlot', 

1325 'pullout_direction': 1, 

1326 'x': 0, 

1327 'y': 0, 

1328 'z': 0, 

1329 'tool_id': 123, 

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

1331 **tray_data, 

1332 }, 

1333 ] 

1334 mock_response.json.return_value = mock_api_response 

1335 mock_response.status_code = 200 

1336 mock_response.text = 'text' 

1337 mock_request.return_value = mock_response 

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

1339 mock_request.assert_has_calls([ 

1340 call( 

1341 'GET', 

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

1343 headers={ 

1344 'authorization': 'encoded_token_value', 

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

1346 }, 

1347 json=None, 

1348 ), 

1349 call().json(), 

1350 call( 

1351 'GET', 

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

1353 headers={ 

1354 'authorization': 'encoded_token_value', 

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

1356 }, 

1357 json=None, 

1358 ), 

1359 call().json(), 

1360 ]) 

1361 self.assertEqual(cell, expected_xyz, kwargs) 

1362 

1363 def test_get_seed_tray_cell(self): 

1364 '''Test get_seed_tray_cell''' 

1365 test_cases = [ 

1366 { 

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

1368 'cell': 'a1', 

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

1370 }, 

1371 { 

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

1373 'cell': 'b2', 

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

1375 }, 

1376 { 

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

1378 'cell': 'd4', 

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

1380 }, 

1381 { 

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

1383 'cell': 'a1', 

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

1385 }, 

1386 { 

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

1388 'cell': 'b2', 

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

1390 }, 

1391 { 

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

1393 'cell': 'd4', 

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

1395 }, 

1396 { 

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

1398 'cell': 'd4', 

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

1400 }, 

1401 ] 

1402 for test_case in test_cases: 

1403 self.helper_get_seed_tray_cell(**test_case) 

1404 

1405 @patch('requests.request') 

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

1407 '''Test helper for get_seed_tray_cell command errors''' 

1408 mock_request = args[0] 

1409 tray_data = kwargs['tray_data'] 

1410 cell = kwargs['cell'] 

1411 error = kwargs['error'] 

1412 mock_response = Mock() 

1413 mock_api_response = [ 

1414 { 

1415 'id': 123, 

1416 'name': 'Seed Tray', 

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

1418 }, 

1419 { 

1420 'pointer_type': 'ToolSlot', 

1421 'pullout_direction': 1, 

1422 'x': 0, 

1423 'y': 0, 

1424 'z': 0, 

1425 'tool_id': 123, 

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

1427 **tray_data, 

1428 }, 

1429 ] 

1430 mock_response.json.return_value = mock_api_response 

1431 mock_response.status_code = 200 

1432 mock_response.text = 'text' 

1433 mock_request.return_value = mock_response 

1434 with self.assertRaises(ValueError) as cm: 

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

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

1437 mock_request.assert_has_calls([ 

1438 call( 

1439 'GET', 

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

1441 headers={ 

1442 'authorization': 'encoded_token_value', 

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

1444 }, 

1445 json=None, 

1446 ), 

1447 call().json(), 

1448 call( 

1449 'GET', 

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

1451 headers={ 

1452 'authorization': 'encoded_token_value', 

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

1454 }, 

1455 json=None, 

1456 ), 

1457 call().json(), 

1458 ]) 

1459 

1460 def test_get_seed_tray_cell_invalid_cell_name(self): 

1461 '''Test get_seed_tray_cell: invalid cell name''' 

1462 self.helper_get_seed_tray_cell_error( 

1463 tray_data={}, 

1464 cell='e4', 

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

1466 ) 

1467 

1468 def test_get_seed_tray_cell_invalid_pullout_direction(self): 

1469 '''Test get_seed_tray_cell: invalid pullout direction''' 

1470 self.helper_get_seed_tray_cell_error( 

1471 tray_data={'pullout_direction': 0}, 

1472 cell='d4', 

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

1474 ) 

1475 

1476 @patch('requests.request') 

1477 def test_get_seed_tray_cell_no_tray(self, mock_request): 

1478 '''Test get_seed_tray_cell: no seed tray''' 

1479 mock_response = Mock() 

1480 mock_api_response = [] 

1481 mock_response.json.return_value = mock_api_response 

1482 mock_response.status_code = 200 

1483 mock_response.text = 'text' 

1484 mock_request.return_value = mock_response 

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

1486 mock_request.assert_has_calls([ 

1487 call( 

1488 'GET', 

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

1490 headers={ 

1491 'authorization': 'encoded_token_value', 

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

1493 }, 

1494 json=None, 

1495 ), 

1496 call().json(), 

1497 ]) 

1498 self.assertIsNone(result) 

1499 

1500 @patch('requests.request') 

1501 def test_get_seed_tray_cell_not_mounted(self, mock_request): 

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

1503 mock_response = Mock() 

1504 mock_api_response = [{ 

1505 'id': 123, 

1506 'name': 'Seed Tray', 

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

1508 }] 

1509 mock_response.json.return_value = mock_api_response 

1510 mock_response.status_code = 200 

1511 mock_response.text = 'text' 

1512 mock_request.return_value = mock_response 

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

1514 mock_request.assert_has_calls([ 

1515 call( 

1516 'GET', 

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

1518 headers={ 

1519 'authorization': 'encoded_token_value', 

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

1521 }, 

1522 json=None, 

1523 ), 

1524 call().json(), 

1525 ]) 

1526 self.assertIsNone(result) 

1527 

1528 def test_get_job_one(self): 

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

1530 def exec_command(): 

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

1532 'jobs': { 

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

1534 }, 

1535 } 

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

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

1538 self.send_command_test_helper( 

1539 exec_command, 

1540 expected_command={ 

1541 'kind': 'read_status', 

1542 'args': {}, 

1543 }, 

1544 extra_rpc_args={}, 

1545 mock_api_response={}) 

1546 

1547 def test_get_job_all(self): 

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

1549 def exec_command(): 

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

1551 'jobs': { 

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

1553 }, 

1554 } 

1555 jobs = self.fb.get_job() 

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

1557 self.send_command_test_helper( 

1558 exec_command, 

1559 expected_command={ 

1560 'kind': 'read_status', 

1561 'args': {}, 

1562 }, 

1563 extra_rpc_args={}, 

1564 mock_api_response={}) 

1565 

1566 def test_get_job_no_status(self): 

1567 '''Test get_job command: no status''' 

1568 def exec_command(): 

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

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

1571 self.assertIsNone(job) 

1572 self.send_command_test_helper( 

1573 exec_command, 

1574 expected_command={ 

1575 'kind': 'read_status', 

1576 'args': {}, 

1577 }, 

1578 extra_rpc_args={}, 

1579 mock_api_response={}) 

1580 

1581 def test_set_job(self): 

1582 '''Test set_job command''' 

1583 def exec_command(): 

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

1585 self.send_command_test_helper( 

1586 exec_command, 

1587 expected_command={ 

1588 'kind': 'lua', 

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

1590 set_job(job_name) 

1591 

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

1593 set_job(job_name, { 

1594 status = "working", 

1595 percent = 50 

1596 })'''}, 

1597 }, 

1598 extra_rpc_args={}, 

1599 mock_api_response={}) 

1600 

1601 def test_complete_job(self): 

1602 '''Test complete_job command''' 

1603 def exec_command(): 

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

1605 self.send_command_test_helper( 

1606 exec_command, 

1607 expected_command={ 

1608 'kind': 'lua', 

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

1610 }, 

1611 extra_rpc_args={}, 

1612 mock_api_response={}) 

1613 

1614 def test_lua(self): 

1615 '''Test lua command''' 

1616 def exec_command(): 

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

1618 self.send_command_test_helper( 

1619 exec_command, 

1620 expected_command={ 

1621 'kind': 'lua', 

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

1623 }, 

1624 extra_rpc_args={}, 

1625 mock_api_response={}) 

1626 

1627 def test_if_statement(self): 

1628 '''Test if_statement command''' 

1629 def exec_command(): 

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

1631 self.send_command_test_helper( 

1632 exec_command, 

1633 expected_command={ 

1634 'kind': '_if', 

1635 'args': { 

1636 'lhs': 'pin10', 

1637 'op': 'is', 

1638 'rhs': 0, 

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

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

1641 } 

1642 }, 

1643 extra_rpc_args={}, 

1644 mock_api_response=[]) 

1645 

1646 def test_if_statement_with_named_pin(self): 

1647 '''Test if_statement command with named pin''' 

1648 def exec_command(): 

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

1650 self.send_command_test_helper( 

1651 exec_command, 

1652 expected_command={ 

1653 'kind': '_if', 

1654 'args': { 

1655 'lhs': { 

1656 'kind': 'named_pin', 

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

1658 }, 

1659 'op': 'is', 

1660 'rhs': 0, 

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

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

1663 } 

1664 }, 

1665 extra_rpc_args={}, 

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

1667 

1668 def test_if_statement_with_named_pin_not_found(self): 

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

1670 def exec_command(): 

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

1672 self.send_command_test_helper( 

1673 exec_command, 

1674 expected_command=None, 

1675 extra_rpc_args={}, 

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

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

1678 

1679 def test_if_statement_with_sequences(self): 

1680 '''Test if_statement command with sequences''' 

1681 def exec_command(): 

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

1683 self.send_command_test_helper( 

1684 exec_command, 

1685 expected_command={ 

1686 'kind': '_if', 

1687 'args': { 

1688 'lhs': 'pin10', 

1689 'op': '<', 

1690 'rhs': 0, 

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

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

1693 } 

1694 }, 

1695 extra_rpc_args={}, 

1696 mock_api_response=[ 

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

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

1699 ]) 

1700 

1701 def test_if_statement_with_sequence_not_found(self): 

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

1703 def exec_command(): 

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

1705 self.send_command_test_helper( 

1706 exec_command, 

1707 expected_command=None, 

1708 extra_rpc_args={}, 

1709 mock_api_response=[]) 

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

1711 

1712 def test_if_statement_invalid_operator(self): 

1713 '''Test if_statement command: invalid operator''' 

1714 def exec_command(): 

1715 with self.assertRaises(ValueError) as cm: 

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

1717 self.assertEqual( 

1718 cm.exception.args[0], 

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

1720 self.send_command_test_helper( 

1721 exec_command, 

1722 expected_command=None, 

1723 extra_rpc_args={}, 

1724 mock_api_response=[]) 

1725 

1726 def test_if_statement_invalid_variable(self): 

1727 '''Test if_statement command: invalid variable''' 

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

1729 def exec_command(): 

1730 with self.assertRaises(ValueError) as cm: 

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

1732 self.assertEqual( 

1733 cm.exception.args[0], 

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

1735 self.send_command_test_helper( 

1736 exec_command, 

1737 expected_command=None, 

1738 extra_rpc_args={}, 

1739 mock_api_response=[]) 

1740 

1741 def test_if_statement_invalid_named_pin_type(self): 

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

1743 def exec_command(): 

1744 with self.assertRaises(ValueError) as cm: 

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

1746 self.assertEqual( 

1747 cm.exception.args[0], 

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

1749 self.send_command_test_helper( 

1750 exec_command, 

1751 expected_command=None, 

1752 extra_rpc_args={}, 

1753 mock_api_response=[]) 

1754 

1755 @staticmethod 

1756 def helper_get_print_strings(mock_print): 

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

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

1759 

1760 @patch('builtins.print') 

1761 def test_print_status(self, mock_print): 

1762 '''Test print_status.''' 

1763 self.fb.set_verbosity(0) 

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

1765 mock_print.assert_not_called() 

1766 self.fb.set_verbosity(1) 

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

1768 call_strings = self.helper_get_print_strings(mock_print) 

1769 self.assertIn('testing', call_strings) 

1770 mock_print.reset_mock() 

1771 self.fb.set_verbosity(2) 

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

1773 call_strings = self.helper_get_print_strings(mock_print) 

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

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

1776 self.assertIn('test_print_status', call_strings)