Coverage for tests/tests_main.py: 100%

796 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-09-04 17:38 -0700

1''' 

2Farmbot class unit tests. 

3''' 

4 

5import json 

6import unittest 

7from unittest.mock import Mock, patch, call 

8import requests 

9 

10from farmbot_sidecar_starter_pack import Farmbot 

11 

12MOCK_TOKEN = { 

13 'token': { 

14 'unencoded': { 

15 'iss': '//my.farm.bot', 

16 'mqtt': 'mqtt_url', 

17 'bot': 'device_0', 

18 }, 

19 'encoded': 'encoded_token_value' 

20 } 

21} 

22 

23TOKEN_REQUEST_KWARGS = { 

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

25 'timeout': 0, 

26} 

27 

28REQUEST_KWARGS_WITH_PAYLOAD = { 

29 'headers': { 

30 'authorization': 'encoded_token_value', 

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

32 }, 

33 'timeout': 0, 

34} 

35 

36REQUEST_KWARGS = { 

37 **REQUEST_KWARGS_WITH_PAYLOAD, 

38 'json': None, 

39} 

40 

41class TestFarmbot(unittest.TestCase): 

42 '''Farmbot tests''' 

43 

44 def setUp(self): 

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

46 self.fb = Farmbot() 

47 self.fb.set_token(MOCK_TOKEN) 

48 self.fb.set_verbosity(0) 

49 self.fb.state.test_env = True 

50 self.fb.set_timeout(0, 'all') 

51 self.fb.state.clear_cache() 

52 

53 @patch('requests.post') 

54 def test_get_token_default_server(self, mock_post): 

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

56 mock_response = Mock() 

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

58 mock_response.json.return_value = expected_token 

59 mock_response.status_code = 200 

60 mock_response.text = 'text' 

61 mock_post.return_value = mock_response 

62 self.fb.set_token(None) 

63 # Call with default server 

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

65 mock_post.assert_called_once_with( 

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

67 **TOKEN_REQUEST_KWARGS, 

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

69 'password': 'test_pass_123'}}, 

70 ) 

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

72 

73 @patch('requests.post') 

74 def test_get_token_custom_server(self, mock_post): 

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

76 mock_response = Mock() 

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

78 mock_response.json.return_value = expected_token 

79 mock_response.status_code = 200 

80 mock_response.text = 'text' 

81 mock_post.return_value = mock_response 

82 self.fb.set_token(None) 

83 # Call with custom server 

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

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

86 mock_post.assert_called_once_with( 

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

88 **TOKEN_REQUEST_KWARGS, 

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

90 'password': 'test_pass_123'}}, 

91 ) 

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

93 

94 @patch('requests.post') 

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

96 '''Test helper for get_token errors''' 

97 mock_post = args[0] 

98 status_code = kwargs['status_code'] 

99 error_msg = kwargs['error_msg'] 

100 mock_response = Mock() 

101 mock_response.status_code = status_code 

102 mock_post.return_value = mock_response 

103 self.fb.set_token(None) 

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

105 mock_post.assert_called_once_with( 

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

107 **TOKEN_REQUEST_KWARGS, 

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

109 'password': 'test_pass_123'}}, 

110 ) 

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

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

113 

114 def test_get_token_bad_email(self): 

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

116 self.helper_get_token_errors( 

117 status_code=422, 

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

119 ) 

120 

121 def test_get_token_bad_server(self): 

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

123 self.helper_get_token_errors( 

124 status_code=404, 

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

126 ) 

127 

128 def test_get_token_other_error(self): 

129 '''get_token: other error''' 

130 self.helper_get_token_errors( 

131 status_code=500, 

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

133 ) 

134 

135 @patch('requests.post') 

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

137 '''Test helper for get_token exceptions''' 

138 mock_post = args[0] 

139 exception = kwargs['exception'] 

140 error_msg = kwargs['error_msg'] 

141 mock_post.side_effect = exception 

142 self.fb.set_token(None) 

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

144 mock_post.assert_called_once_with( 

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

146 **TOKEN_REQUEST_KWARGS, 

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

148 'password': 'test_pass_123'}}, 

149 ) 

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

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

152 

153 def test_get_token_server_not_found(self): 

154 '''get_token: server not found''' 

155 self.helper_get_token_exceptions( 

156 exception=requests.exceptions.ConnectionError, 

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

158 ) 

159 

160 def test_get_token_timeout(self): 

161 '''get_token: timeout''' 

162 self.helper_get_token_exceptions( 

163 exception=requests.exceptions.Timeout, 

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

165 ) 

166 

167 def test_get_token_problem(self): 

168 '''get_token: problem''' 

169 self.helper_get_token_exceptions( 

170 exception=requests.exceptions.RequestException, 

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

172 ) 

173 

174 def test_get_token_other_exception(self): 

175 '''get_token: other exception''' 

176 self.helper_get_token_exceptions( 

177 exception=Exception('other'), 

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

179 ) 

180 

181 @patch('requests.request') 

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

183 '''Test helper for api_get errors''' 

184 mock_request = args[0] 

185 status_code = kwargs['status_code'] 

186 error_msg = kwargs['error_msg'] 

187 mock_response = Mock() 

188 mock_response.status_code = status_code 

189 mock_response.reason = 'reason' 

190 mock_response.text = 'text' 

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

192 mock_request.return_value = mock_response 

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

194 mock_request.assert_called_once_with( 

195 'GET', 

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

197 **REQUEST_KWARGS, 

198 ) 

199 self.assertEqual(response, error_msg) 

200 

201 def test_api_get_errors(self): 

202 '''Test api_get errors''' 

203 self.helper_api_get_error( 

204 status_code=404, 

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

206 ) 

207 self.helper_api_get_error( 

208 status_code=500, 

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

210 ) 

211 self.helper_api_get_error( 

212 status_code=600, 

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

214 ) 

215 

216 @patch('requests.request') 

217 def test_api_string_error_response_handling(self, mock_request): 

218 '''Test API string response errors''' 

219 mock_response = Mock() 

220 mock_response.status_code = 404 

221 mock_response.reason = 'reason' 

222 mock_response.text = 'error string' 

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

224 mock_request.return_value = mock_response 

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

226 mock_request.assert_called_once_with( 

227 'GET', 

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

229 **REQUEST_KWARGS, 

230 ) 

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

232 

233 @patch('requests.request') 

234 def test_api_string_error_response_handling_html(self, mock_request): 

235 '''Test API html string response errors''' 

236 mock_response = Mock() 

237 mock_response.status_code = 404 

238 mock_response.reason = 'reason' 

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

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

241 mock_request.return_value = mock_response 

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

243 mock_request.assert_called_once_with( 

244 'GET', 

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

246 **REQUEST_KWARGS, 

247 ) 

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

249 

250 @patch('requests.request') 

251 def test_api_get_endpoint_only(self, mock_request): 

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

253 mock_response = Mock() 

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

255 mock_response.json.return_value = expected_response 

256 mock_response.status_code = 200 

257 mock_response.text = 'text' 

258 mock_request.return_value = mock_response 

259 # Call with endpoint only 

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

261 mock_request.assert_called_once_with( 

262 'GET', 

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

264 **REQUEST_KWARGS, 

265 ) 

266 self.assertEqual(response, expected_response) 

267 

268 @patch('requests.request') 

269 def test_api_get_with_id(self, mock_request): 

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

271 mock_response = Mock() 

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

273 mock_response.json.return_value = expected_response 

274 mock_response.status_code = 200 

275 mock_response.text = 'text' 

276 mock_request.return_value = mock_response 

277 # Call with specific ID 

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

279 mock_request.assert_called_once_with( 

280 'GET', 

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

282 **REQUEST_KWARGS, 

283 ) 

284 self.assertEqual(response, expected_response) 

285 

286 @patch('requests.request') 

287 def test_check_token_api_request(self, mock_request): 

288 '''Test check_token: API request''' 

289 self.fb.set_token(None) 

290 with self.assertRaises(ValueError) as cm: 

291 self.fb.api_get('points') 

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

293 mock_request.assert_not_called() 

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

295 

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

297 @patch('requests.request') 

298 def test_check_token_broker(self, mock_request, mock_mqtt): 

299 '''Test check_token: broker''' 

300 mock_client = Mock() 

301 mock_mqtt.return_value = mock_client 

302 self.fb.set_token(None) 

303 with self.assertRaises(ValueError) as cm: 

304 self.fb.on(123) 

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

306 with self.assertRaises(ValueError) as cm: 

307 self.fb.read_sensor(123) 

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

309 with self.assertRaises(ValueError) as cm: 

310 self.fb.get_xyz() 

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

312 with self.assertRaises(ValueError) as cm: 

313 self.fb.read_status() 

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

315 mock_request.assert_not_called() 

316 mock_client.publish.assert_not_called() 

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

318 

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

320 def test_publish_disabled(self, mock_mqtt): 

321 '''Test publish disabled''' 

322 mock_client = Mock() 

323 mock_mqtt.return_value = mock_client 

324 self.fb.state.dry_run = True 

325 self.fb.on(123) 

326 mock_client.publish.assert_not_called() 

327 

328 @patch('requests.request') 

329 def test_api_patch(self, mock_request): 

330 '''test api_patch function''' 

331 mock_response = Mock() 

332 mock_response.status_code = 200 

333 mock_response.text = 'text' 

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

335 mock_request.return_value = mock_response 

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

337 mock_request.assert_has_calls([call( 

338 'PATCH', 

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

340 **REQUEST_KWARGS_WITH_PAYLOAD, 

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

342 ), 

343 call().json(), 

344 ]) 

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

346 

347 @patch('requests.request') 

348 def test_api_post(self, mock_request): 

349 '''test api_post function''' 

350 mock_response = Mock() 

351 mock_response.status_code = 200 

352 mock_response.text = 'text' 

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

354 mock_request.return_value = mock_response 

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

356 mock_request.assert_has_calls([call( 

357 'POST', 

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

359 **REQUEST_KWARGS_WITH_PAYLOAD, 

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

361 ), 

362 call().json(), 

363 ]) 

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

365 

366 @patch('requests.request') 

367 def test_api_delete(self, mock_request): 

368 '''test api_delete function''' 

369 mock_response = Mock() 

370 mock_response.status_code = 200 

371 mock_response.text = 'text' 

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

373 mock_request.return_value = mock_response 

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

375 mock_request.assert_called_once_with( 

376 'DELETE', 

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

378 **REQUEST_KWARGS, 

379 ) 

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

381 

382 @patch('requests.request') 

383 def test_api_delete_requests_disabled(self, mock_request): 

384 '''test api_delete function: requests disabled''' 

385 self.fb.state.dry_run = True 

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

387 mock_request.assert_not_called() 

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

389 

390 @patch('requests.request') 

391 def test_group_one(self, mock_request): 

392 '''test group function: get one group''' 

393 mock_response = Mock() 

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

395 mock_response.status_code = 200 

396 mock_response.text = 'text' 

397 mock_request.return_value = mock_response 

398 group_info = self.fb.group(12345) 

399 mock_request.assert_called_once_with( 

400 'GET', 

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

402 **REQUEST_KWARGS, 

403 ) 

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

405 

406 @patch('requests.request') 

407 def test_group_all(self, mock_request): 

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

409 mock_response = Mock() 

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

411 mock_response.status_code = 200 

412 mock_response.text = 'text' 

413 mock_request.return_value = mock_response 

414 group_info = self.fb.group() 

415 mock_request.assert_called_once_with( 

416 'GET', 

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

418 **REQUEST_KWARGS, 

419 ) 

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

421 

422 @patch('requests.request') 

423 def test_curve_one(self, mock_request): 

424 '''test curve function: get one curve''' 

425 mock_response = Mock() 

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

427 mock_response.status_code = 200 

428 mock_response.text = 'text' 

429 mock_request.return_value = mock_response 

430 curve_info = self.fb.curve(12345) 

431 mock_request.assert_called_once_with( 

432 'GET', 

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

434 **REQUEST_KWARGS, 

435 ) 

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

437 

438 @patch('requests.request') 

439 def test_curve_all(self, mock_request): 

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

441 mock_response = Mock() 

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

443 mock_response.status_code = 200 

444 mock_response.text = 'text' 

445 mock_request.return_value = mock_response 

446 curve_info = self.fb.curve() 

447 mock_request.assert_called_once_with( 

448 'GET', 

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

450 **REQUEST_KWARGS, 

451 ) 

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

453 

454 @patch('requests.request') 

455 def test_safe_z(self, mock_request): 

456 '''test safe_z function''' 

457 mock_response = Mock() 

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

459 mock_response.status_code = 200 

460 mock_response.text = 'text' 

461 mock_request.return_value = mock_response 

462 safe_height = self.fb.safe_z() 

463 mock_request.assert_called_once_with( 

464 'GET', 

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

466 **REQUEST_KWARGS, 

467 ) 

468 self.assertEqual(safe_height, 100) 

469 

470 @patch('requests.request') 

471 def test_garden_size(self, mock_request): 

472 '''test garden_size function''' 

473 mock_response = Mock() 

474 mock_response.json.return_value = { 

475 'movement_axis_nr_steps_x': 1000, 

476 'movement_axis_nr_steps_y': 2000, 

477 'movement_axis_nr_steps_z': 40000, 

478 'movement_step_per_mm_x': 5, 

479 'movement_step_per_mm_y': 5, 

480 'movement_step_per_mm_z': 25, 

481 } 

482 mock_response.status_code = 200 

483 mock_response.text = 'text' 

484 mock_request.return_value = mock_response 

485 garden_size = self.fb.garden_size() 

486 mock_request.assert_called_once_with( 

487 'GET', 

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

489 **REQUEST_KWARGS, 

490 ) 

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

492 

493 @patch('requests.request') 

494 def test_log(self, mock_request): 

495 '''test log function''' 

496 mock_response = Mock() 

497 mock_response.status_code = 200 

498 mock_response.text = 'text' 

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

500 mock_request.return_value = mock_response 

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

502 mock_request.assert_called_once_with( 

503 'POST', 

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

505 **REQUEST_KWARGS_WITH_PAYLOAD, 

506 json={ 

507 'message': 'test message', 

508 'type': 'info', 

509 'channels': ['toast'], 

510 }, 

511 ) 

512 

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

514 def test_connect_broker(self, mock_mqtt): 

515 '''Test test_connect_broker command''' 

516 mock_client = Mock() 

517 mock_mqtt.return_value = mock_client 

518 self.fb.connect_broker() 

519 mock_client.username_pw_set.assert_called_once_with( 

520 username='device_0', 

521 password='encoded_token_value') 

522 mock_client.connect.assert_called_once_with( 

523 'mqtt_url', 

524 port=1883, 

525 keepalive=60) 

526 mock_client.loop_start.assert_called() 

527 

528 def test_disconnect_broker(self): 

529 '''Test disconnect_broker command''' 

530 mock_client = Mock() 

531 self.fb.broker.client = mock_client 

532 self.fb.disconnect_broker() 

533 mock_client.loop_stop.assert_called_once() 

534 mock_client.disconnect.assert_called_once() 

535 

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

537 def test_listen(self, mock_mqtt): 

538 '''Test listen command''' 

539 mock_client = Mock() 

540 mock_mqtt.return_value = mock_client 

541 self.fb.listen() 

542 

543 class MockMessage: 

544 '''Mock message class''' 

545 topic = 'topic' 

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

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

548 mock_client.username_pw_set.assert_called_once_with( 

549 username='device_0', 

550 password='encoded_token_value') 

551 mock_client.connect.assert_called_once_with( 

552 'mqtt_url', 

553 port=1883, 

554 keepalive=60) 

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

556 mock_client.loop_start.assert_called() 

557 mock_client.loop_stop.assert_called() 

558 

559 @patch('math.inf', 0.1) 

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

561 def test_listen_for_status_changes(self, mock_mqtt): 

562 '''Test listen_for_status_changes command''' 

563 self.maxDiff = None 

564 i = 0 

565 

566 mock_client = Mock() 

567 mock_mqtt.return_value = mock_client 

568 

569 class MockMessage: 

570 '''Mock message class''' 

571 def __init__(self): 

572 self.topic = '/status' 

573 payload = { 

574 'location_data': { 

575 'position': { 

576 'x': i, 

577 'y': i + 10, 

578 'z': 100, 

579 }}} 

580 if i == 2: 

581 payload['location_data']['position']['extra'] = {'idx': 2} 

582 if i == 3: 

583 payload['location_data']['position']['extra'] = {'idx': 3} 

584 self.payload = json.dumps(payload) 

585 

586 def patched_sleep(_seconds): 

587 '''Patched sleep function''' 

588 nonlocal i 

589 mock_message = MockMessage() 

590 mock_client.on_message('', '', mock_message) 

591 i += 1 

592 

593 with patch('time.sleep', new=patched_sleep): 

594 self.fb.listen_for_status_changes(stop_count=5, info_path='location_data.position') 

595 

596 self.assertEqual(self.fb.state.last_messages['status'], [ 

597 {'location_data': {'position': {'x': 0, 'y': 10, 'z': 100}}}, 

598 {'location_data': {'position': {'x': 1, 'y': 11, 'z': 100}}}, 

599 {'location_data': {'position': {'extra': {'idx': 2}, 'x': 2, 'y': 12, 'z': 100}}}, 

600 {'location_data': {'position': {'extra': {'idx': 3}, 'x': 3, 'y': 13, 'z': 100}}}, 

601 {'location_data': {'position': {'x': 4, 'y': 14, 'z': 100}}} 

602 ]) 

603 self.assertEqual(self.fb.state.last_messages['status_diffs'], [ 

604 {'x': 0, 'y': 10, 'z': 100}, 

605 {'x': 1, 'y': 11}, 

606 {'extra': {'idx': 2}, 'x': 2, 'y': 12}, 

607 {'extra': {'idx': 3}, 'x': 3, 'y': 13}, 

608 {'x': 4, 'y': 14}, 

609 ]) 

610 self.assertEqual(self.fb.state.last_messages['status_excerpt'], [ 

611 {'x': 0, 'y': 10, 'z': 100}, 

612 {'x': 1, 'y': 11, 'z': 100}, 

613 {'extra': {'idx': 2}, 'x': 2, 'y': 12, 'z': 100}, 

614 {'extra': {'idx': 3}, 'x': 3, 'y': 13, 'z': 100}, 

615 {'x': 4, 'y': 14, 'z': 100}, 

616 ]) 

617 

618 

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

620 def test_listen_clear_last(self, mock_mqtt): 

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

622 mock_client = Mock() 

623 mock_mqtt.return_value = mock_client 

624 self.fb.state.last_messages = [{'#': "message"}] 

625 self.fb.state.test_env = False 

626 self.fb.listen() 

627 self.assertEqual(len(self.fb.state.last_messages['#']), 0) 

628 

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

630 def test_publish_apply_label(self, mock_mqtt): 

631 '''Test publish command: set uuid''' 

632 mock_client = Mock() 

633 mock_mqtt.return_value = mock_client 

634 self.fb.state.test_env = False 

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

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

637 

638 @patch('requests.request') 

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

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

641 '''Helper for testing command execution''' 

642 execute_command = args[0] 

643 mock_mqtt = args[1] 

644 mock_request = args[2] 

645 expected_command = kwargs.get('expected_command') 

646 extra_rpc_args = kwargs.get('extra_rpc_args') 

647 mock_api_response = kwargs.get('mock_api_response') 

648 error = kwargs.get('error') 

649 mock_client = Mock() 

650 mock_mqtt.return_value = mock_client 

651 mock_response = Mock() 

652 mock_response.json.return_value = mock_api_response 

653 mock_response.status_code = 200 

654 mock_response.text = 'text' 

655 mock_request.return_value = mock_response 

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

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

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

659 }] 

660 execute_command() 

661 if expected_command is None: 

662 mock_client.publish.assert_not_called() 

663 return 

664 expected_payload = { 

665 'kind': 'rpc_request', 

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

667 'body': [expected_command], 

668 } 

669 mock_client.username_pw_set.assert_called_once_with( 

670 username='device_0', 

671 password='encoded_token_value') 

672 mock_client.connect.assert_called_once_with( 

673 'mqtt_url', 

674 port=1883, 

675 keepalive=60) 

676 mock_client.loop_start.assert_called() 

677 mock_client.publish.assert_called_once_with( 

678 'bot/device_0/from_clients', 

679 payload=json.dumps(expected_payload)) 

680 if not error: 

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

682 

683 def test_message(self): 

684 '''Test message command''' 

685 def exec_command(): 

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

687 self.send_command_test_helper( 

688 exec_command, 

689 expected_command={ 

690 'kind': 'send_message', 

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

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

693 }, 

694 extra_rpc_args={}, 

695 mock_api_response={}) 

696 

697 def test_debug(self): 

698 '''Test debug command''' 

699 def exec_command(): 

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

701 self.send_command_test_helper( 

702 exec_command, 

703 expected_command={ 

704 'kind': 'send_message', 

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

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

707 }, 

708 extra_rpc_args={}, 

709 mock_api_response={}) 

710 

711 def test_toast(self): 

712 '''Test toast command''' 

713 def exec_command(): 

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

715 self.send_command_test_helper( 

716 exec_command, 

717 expected_command={ 

718 'kind': 'send_message', 

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

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

721 }, 

722 extra_rpc_args={}, 

723 mock_api_response={}) 

724 

725 def test_invalid_message_type(self): 

726 '''Test message_type validation''' 

727 def exec_command(): 

728 with self.assertRaises(ValueError) as cm: 

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

730 self.assertEqual( 

731 cm.exception.args[0], 

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

733 self.send_command_test_helper( 

734 exec_command, 

735 expected_command=None, 

736 extra_rpc_args={}, 

737 mock_api_response={}) 

738 

739 def test_invalid_message_channel(self): 

740 '''Test message channel validation''' 

741 def exec_command(): 

742 with self.assertRaises(ValueError) as cm: 

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

744 self.assertEqual( 

745 cm.exception.args[0], 

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

747 self.send_command_test_helper( 

748 exec_command, 

749 expected_command=None, 

750 extra_rpc_args={}, 

751 mock_api_response={}) 

752 

753 def test_read_status(self): 

754 '''Test read_status command''' 

755 def exec_command(): 

756 self.fb.read_status() 

757 self.send_command_test_helper( 

758 exec_command, 

759 expected_command={ 

760 'kind': 'read_status', 

761 'args': {}, 

762 }, 

763 extra_rpc_args={}, 

764 mock_api_response={}) 

765 

766 def test_read_pin(self): 

767 '''Test read_pin command''' 

768 def exec_command(): 

769 self.fb.read_pin(13) 

770 self.send_command_test_helper( 

771 exec_command, 

772 expected_command={ 

773 'kind': 'read_pin', 

774 'args': { 

775 'pin_number': 13, 

776 'label': '---', 

777 'pin_mode': 0, 

778 }, 

779 }, 

780 extra_rpc_args={}, 

781 mock_api_response={}) 

782 

783 def test_read_sensor(self): 

784 '''Test read_sensor command''' 

785 def exec_command(): 

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

787 self.send_command_test_helper( 

788 exec_command, 

789 expected_command={ 

790 'kind': 'read_pin', 

791 'args': { 

792 'pin_mode': 0, 

793 'label': '---', 

794 'pin_number': { 

795 'kind': 'named_pin', 

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

797 }, 

798 }, 

799 }, 

800 extra_rpc_args={}, 

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

802 

803 def test_read_sensor_not_found(self): 

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

805 def exec_command(): 

806 self.fb.read_sensor('Temperature') 

807 self.send_command_test_helper( 

808 exec_command, 

809 expected_command=None, 

810 extra_rpc_args={}, 

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

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

813 

814 def test_assertion(self): 

815 '''Test assertion command''' 

816 def exec_command(): 

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

818 self.send_command_test_helper( 

819 exec_command, 

820 expected_command={ 

821 'kind': 'assertion', 

822 'args': { 

823 'assertion_type': 'abort', 

824 'lua': 'return true', 

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

826 } 

827 }, 

828 extra_rpc_args={}, 

829 mock_api_response={}) 

830 

831 def test_assertion_with_recovery_sequence(self): 

832 '''Test assertion command with recovery sequence''' 

833 def exec_command(): 

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

835 self.send_command_test_helper( 

836 exec_command, 

837 expected_command={ 

838 'kind': 'assertion', 

839 'args': { 

840 'assertion_type': 'abort', 

841 'lua': 'return true', 

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

843 } 

844 }, 

845 extra_rpc_args={}, 

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

847 

848 def test_assertion_recovery_sequence_not_found(self): 

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

850 def exec_command(): 

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

852 self.send_command_test_helper( 

853 exec_command, 

854 expected_command=None, 

855 extra_rpc_args={}, 

856 mock_api_response=[]) 

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

858 

859 def test_assertion_invalid_assertion_type(self): 

860 '''Test assertion command: invalid assertion type''' 

861 def exec_command(): 

862 with self.assertRaises(ValueError) as cm: 

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

864 self.assertEqual( 

865 cm.exception.args[0], 

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

867 self.send_command_test_helper( 

868 exec_command, 

869 expected_command=None, 

870 extra_rpc_args={}, 

871 mock_api_response={}) 

872 

873 def test_wait(self): 

874 '''Test wait command''' 

875 def exec_command(): 

876 self.fb.wait(123) 

877 self.send_command_test_helper( 

878 exec_command, 

879 expected_command={ 

880 'kind': 'wait', 

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

882 }, 

883 extra_rpc_args={}, 

884 mock_api_response={}) 

885 

886 def test_unlock(self): 

887 '''Test unlock command''' 

888 def exec_command(): 

889 self.fb.unlock() 

890 self.send_command_test_helper( 

891 exec_command, 

892 expected_command={ 

893 'kind': 'emergency_unlock', 

894 'args': {}, 

895 }, 

896 extra_rpc_args={'priority': 9000}, 

897 mock_api_response={}) 

898 

899 def test_e_stop(self): 

900 '''Test e_stop command''' 

901 def exec_command(): 

902 self.fb.e_stop() 

903 self.send_command_test_helper( 

904 exec_command, 

905 expected_command={ 

906 'kind': 'emergency_lock', 

907 'args': {}, 

908 }, 

909 extra_rpc_args={'priority': 9000}, 

910 mock_api_response={}) 

911 

912 def test_find_home(self): 

913 '''Test find_home command''' 

914 def exec_command(): 

915 self.fb.find_home() 

916 self.send_command_test_helper( 

917 exec_command, 

918 expected_command={ 

919 'kind': 'find_home', 

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

921 }, 

922 extra_rpc_args={}, 

923 mock_api_response={}) 

924 

925 def test_find_home_speed_error(self): 

926 '''Test find_home command: speed error''' 

927 def exec_command(): 

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

929 self.send_command_test_helper( 

930 exec_command, 

931 expected_command=None, 

932 extra_rpc_args={}, 

933 mock_api_response={}) 

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

935 

936 def test_find_home_invalid_axis(self): 

937 '''Test find_home command: invalid axis''' 

938 def exec_command(): 

939 with self.assertRaises(ValueError) as cm: 

940 self.fb.find_home('nope') 

941 self.assertEqual( 

942 cm.exception.args[0], 

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

944 self.send_command_test_helper( 

945 exec_command, 

946 expected_command=None, 

947 extra_rpc_args={}, 

948 mock_api_response={}) 

949 

950 def test_set_home(self): 

951 '''Test set_home command''' 

952 def exec_command(): 

953 self.fb.set_home() 

954 self.send_command_test_helper( 

955 exec_command, 

956 expected_command={ 

957 'kind': 'zero', 

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

959 }, 

960 extra_rpc_args={}, 

961 mock_api_response={}) 

962 

963 def test_toggle_peripheral(self): 

964 '''Test toggle_peripheral command''' 

965 def exec_command(): 

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

967 self.send_command_test_helper( 

968 exec_command, 

969 expected_command={ 

970 'kind': 'toggle_pin', 

971 'args': { 

972 'pin_number': { 

973 'kind': 'named_pin', 

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

975 }, 

976 }, 

977 }, 

978 extra_rpc_args={}, 

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

980 

981 def test_toggle_peripheral_not_found(self): 

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

983 def exec_command(): 

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

985 self.send_command_test_helper( 

986 exec_command, 

987 expected_command=None, 

988 extra_rpc_args={}, 

989 mock_api_response=[]) 

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

991 

992 @patch('requests.request') 

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

994 def test_toggle_peripheral_use_cache(self, mock_mqtt, mock_request): 

995 '''Test toggle_peripheral command: use cache''' 

996 mock_client = Mock() 

997 mock_mqtt.return_value = mock_client 

998 mock_response = Mock() 

999 mock_response.json.return_value = [ 

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

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

1002 ] 

1003 mock_response.status_code = 200 

1004 mock_response.text = 'text' 

1005 mock_request.return_value = mock_response 

1006 # save cache 

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

1008 mock_request.assert_called() 

1009 mock_client.publish.assert_called() 

1010 mock_request.reset_mock() 

1011 mock_client.reset_mock() 

1012 # use cache 

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

1014 mock_request.assert_not_called() 

1015 mock_client.publish.assert_called() 

1016 mock_request.reset_mock() 

1017 mock_client.reset_mock() 

1018 # clear cache 

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

1020 mock_request.assert_not_called() 

1021 mock_client.publish.assert_not_called() 

1022 mock_request.reset_mock() 

1023 mock_client.reset_mock() 

1024 # save cache 

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

1026 mock_request.assert_called() 

1027 mock_client.publish.assert_called() 

1028 mock_request.reset_mock() 

1029 mock_client.reset_mock() 

1030 

1031 def test_on_digital(self): 

1032 '''Test on command: digital''' 

1033 def exec_command(): 

1034 self.fb.on(13) 

1035 self.send_command_test_helper( 

1036 exec_command, 

1037 expected_command={ 

1038 'kind': 'write_pin', 

1039 'args': { 

1040 'pin_value': 1, 

1041 'pin_mode': 0, 

1042 'pin_number': 13, 

1043 }, 

1044 }, 

1045 extra_rpc_args={}, 

1046 mock_api_response={}) 

1047 

1048 def test_off(self): 

1049 '''Test off command''' 

1050 def exec_command(): 

1051 self.fb.off(13) 

1052 self.send_command_test_helper( 

1053 exec_command, 

1054 expected_command={ 

1055 'kind': 'write_pin', 

1056 'args': { 

1057 'pin_value': 0, 

1058 'pin_mode': 0, 

1059 'pin_number': 13, 

1060 }, 

1061 }, 

1062 extra_rpc_args={}, 

1063 mock_api_response={}) 

1064 

1065 def test_move(self): 

1066 '''Test move command''' 

1067 def exec_command(): 

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

1069 self.send_command_test_helper( 

1070 exec_command, 

1071 expected_command={ 

1072 'kind': 'move', 

1073 'args': {}, 

1074 'body': [ 

1075 {'kind': 'axis_overwrite', 'args': { 

1076 'axis': 'x', 

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

1078 {'kind': 'axis_overwrite', 'args': { 

1079 'axis': 'y', 

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

1081 {'kind': 'axis_overwrite', 'args': { 

1082 'axis': 'z', 

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

1084 ], 

1085 }, 

1086 extra_rpc_args={}, 

1087 mock_api_response={}) 

1088 

1089 def test_reboot(self): 

1090 '''Test reboot command''' 

1091 def exec_command(): 

1092 self.fb.reboot() 

1093 self.send_command_test_helper( 

1094 exec_command, 

1095 expected_command={ 

1096 'kind': 'reboot', 

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

1098 }, 

1099 extra_rpc_args={}, 

1100 mock_api_response={}) 

1101 

1102 def test_shutdown(self): 

1103 '''Test shutdown command''' 

1104 def exec_command(): 

1105 self.fb.shutdown() 

1106 self.send_command_test_helper( 

1107 exec_command, 

1108 expected_command={ 

1109 'kind': 'power_off', 

1110 'args': {}, 

1111 }, 

1112 extra_rpc_args={}, 

1113 mock_api_response={}) 

1114 

1115 def test_find_axis_length(self): 

1116 '''Test find_axis_length command''' 

1117 def exec_command(): 

1118 self.fb.find_axis_length() 

1119 self.send_command_test_helper( 

1120 exec_command, 

1121 expected_command={ 

1122 'kind': 'calibrate', 

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

1124 }, 

1125 extra_rpc_args={}, 

1126 mock_api_response={}) 

1127 

1128 def test_write_pin(self): 

1129 '''Test write_pin command''' 

1130 def exec_command(): 

1131 self.fb.write_pin(13, 1, 1) 

1132 self.send_command_test_helper( 

1133 exec_command, 

1134 expected_command={ 

1135 'kind': 'write_pin', 

1136 'args': { 

1137 'pin_number': 13, 

1138 'pin_value': 1, 

1139 'pin_mode': 1, 

1140 }, 

1141 }, 

1142 extra_rpc_args={}, 

1143 mock_api_response={}) 

1144 

1145 def test_control_peripheral(self): 

1146 '''Test control_peripheral command''' 

1147 def exec_command(): 

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

1149 self.send_command_test_helper( 

1150 exec_command, 

1151 expected_command={ 

1152 'kind': 'write_pin', 

1153 'args': { 

1154 'pin_value': 1, 

1155 'pin_mode': 0, 

1156 'pin_number': { 

1157 'kind': 'named_pin', 

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

1159 }, 

1160 }, 

1161 }, 

1162 extra_rpc_args={}, 

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

1164 

1165 def test_control_peripheral_not_found(self): 

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

1167 def exec_command(): 

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

1169 self.send_command_test_helper( 

1170 exec_command, 

1171 expected_command=None, 

1172 extra_rpc_args={}, 

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

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

1175 

1176 def test_measure_soil_height(self): 

1177 '''Test measure_soil_height command''' 

1178 def exec_command(): 

1179 self.fb.measure_soil_height() 

1180 self.send_command_test_helper( 

1181 exec_command, 

1182 expected_command={ 

1183 'kind': 'execute_script', 

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

1185 }, 

1186 extra_rpc_args={}, 

1187 mock_api_response={}) 

1188 

1189 def test_detect_weeds(self): 

1190 '''Test detect_weeds command''' 

1191 def exec_command(): 

1192 self.fb.detect_weeds() 

1193 self.send_command_test_helper( 

1194 exec_command, 

1195 expected_command={ 

1196 'kind': 'execute_script', 

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

1198 }, 

1199 extra_rpc_args={}, 

1200 mock_api_response={}) 

1201 

1202 def test_calibrate_camera(self): 

1203 '''Test calibrate_camera command''' 

1204 def exec_command(): 

1205 self.fb.calibrate_camera() 

1206 self.send_command_test_helper( 

1207 exec_command, 

1208 expected_command={ 

1209 'kind': 'execute_script', 

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

1211 }, 

1212 extra_rpc_args={}, 

1213 mock_api_response={}) 

1214 

1215 def test_sequence(self): 

1216 '''Test sequence command''' 

1217 def exec_command(): 

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

1219 self.send_command_test_helper( 

1220 exec_command, 

1221 expected_command={ 

1222 'kind': 'execute', 

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

1224 }, 

1225 extra_rpc_args={}, 

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

1227 

1228 def test_sequence_not_found(self): 

1229 '''Test sequence command: sequence not found''' 

1230 def exec_command(): 

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

1232 self.send_command_test_helper( 

1233 exec_command, 

1234 expected_command=None, 

1235 extra_rpc_args={}, 

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

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

1238 

1239 def test_take_photo(self): 

1240 '''Test take_photo command''' 

1241 def exec_command(): 

1242 self.fb.take_photo() 

1243 self.send_command_test_helper( 

1244 exec_command, 

1245 expected_command={ 

1246 'kind': 'take_photo', 

1247 'args': {}, 

1248 }, 

1249 extra_rpc_args={}, 

1250 mock_api_response={}) 

1251 

1252 def test_control_servo(self): 

1253 '''Test control_servo command''' 

1254 def exec_command(): 

1255 self.fb.control_servo(4, 100) 

1256 self.send_command_test_helper( 

1257 exec_command, 

1258 expected_command={ 

1259 'kind': 'set_servo_angle', 

1260 'args': { 

1261 'pin_number': 4, 

1262 'pin_value': 100, 

1263 }, 

1264 }, 

1265 extra_rpc_args={}, 

1266 mock_api_response={'mode': 0}) 

1267 

1268 def test_control_servo_error(self): 

1269 '''Test control_servo command: error''' 

1270 def exec_command(): 

1271 self.fb.control_servo(4, 200) 

1272 self.send_command_test_helper( 

1273 exec_command, 

1274 expected_command=None, 

1275 extra_rpc_args={}, 

1276 mock_api_response={'mode': 0}) 

1277 

1278 def test_get_xyz(self): 

1279 '''Test get_xyz command''' 

1280 def exec_command(): 

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

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

1283 }] 

1284 position = self.fb.get_xyz() 

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

1286 self.send_command_test_helper( 

1287 exec_command, 

1288 expected_command={ 

1289 'kind': 'read_status', 

1290 'args': {}, 

1291 }, 

1292 extra_rpc_args={}, 

1293 mock_api_response={}) 

1294 

1295 def test_get_xyz_no_status(self): 

1296 '''Test get_xyz command: no status''' 

1297 def exec_command(): 

1298 self.fb.state.last_messages['status'] = [] 

1299 position = self.fb.get_xyz() 

1300 self.assertIsNone(position) 

1301 self.send_command_test_helper( 

1302 exec_command, 

1303 expected_command={ 

1304 'kind': 'read_status', 

1305 'args': {}, 

1306 }, 

1307 extra_rpc_args={}, 

1308 mock_api_response={}) 

1309 

1310 def test_check_position(self): 

1311 '''Test check_position command: at position''' 

1312 def exec_command(): 

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

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

1315 }] 

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

1317 self.assertTrue(at_position) 

1318 self.send_command_test_helper( 

1319 exec_command, 

1320 expected_command={ 

1321 'kind': 'read_status', 

1322 'args': {}, 

1323 }, 

1324 extra_rpc_args={}, 

1325 mock_api_response={}) 

1326 

1327 def test_check_position_false(self): 

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

1329 def exec_command(): 

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

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

1332 }] 

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

1334 self.assertFalse(at_position) 

1335 self.send_command_test_helper( 

1336 exec_command, 

1337 expected_command={ 

1338 'kind': 'read_status', 

1339 'args': {}, 

1340 }, 

1341 extra_rpc_args={}, 

1342 mock_api_response={}) 

1343 

1344 def test_check_position_no_status(self): 

1345 '''Test check_position command: no status''' 

1346 def exec_command(): 

1347 self.fb.state.last_messages['status'] = [] 

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

1349 self.assertFalse(at_position) 

1350 self.send_command_test_helper( 

1351 exec_command, 

1352 expected_command={ 

1353 'kind': 'read_status', 

1354 'args': {}, 

1355 }, 

1356 extra_rpc_args={}, 

1357 mock_api_response={}) 

1358 

1359 def test_mount_tool(self): 

1360 '''Test mount_tool command''' 

1361 def exec_command(): 

1362 self.fb.mount_tool('Weeder') 

1363 self.send_command_test_helper( 

1364 exec_command, 

1365 expected_command={ 

1366 'kind': 'lua', 

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

1368 }, 

1369 extra_rpc_args={}, 

1370 mock_api_response={}) 

1371 

1372 def test_dismount_tool(self): 

1373 '''Test dismount_tool command''' 

1374 def exec_command(): 

1375 self.fb.dismount_tool() 

1376 self.send_command_test_helper( 

1377 exec_command, 

1378 expected_command={ 

1379 'kind': 'lua', 

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

1381 }, 

1382 extra_rpc_args={}, 

1383 mock_api_response={}) 

1384 

1385 def test_water(self): 

1386 '''Test water command''' 

1387 def exec_command(): 

1388 self.fb.water(123) 

1389 self.send_command_test_helper( 

1390 exec_command, 

1391 expected_command={ 

1392 'kind': 'lua', 

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

1394 method = "GET", 

1395 url = "/api/points/123" 

1396 }) 

1397 water(plant)'''}, 

1398 }, 

1399 extra_rpc_args={}, 

1400 mock_api_response={}) 

1401 

1402 def test_dispense(self): 

1403 '''Test dispense command''' 

1404 def exec_command(): 

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

1406 self.send_command_test_helper( 

1407 exec_command, 

1408 expected_command={ 

1409 'kind': 'lua', 

1410 'args': { 

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

1412 }, 

1413 }, 

1414 extra_rpc_args={}, 

1415 mock_api_response={}) 

1416 

1417 @patch('requests.request') 

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

1419 '''Test helper for get_seed_tray_cell command''' 

1420 mock_request = args[0] 

1421 tray_data = kwargs['tray_data'] 

1422 cell = kwargs['cell'] 

1423 expected_xyz = kwargs['expected_xyz'] 

1424 self.fb.state.clear_cache() 

1425 mock_response = Mock() 

1426 mock_api_response = [ 

1427 { 

1428 'id': 123, 

1429 'name': 'Seed Tray', 

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

1431 }, 

1432 { 

1433 'pointer_type': 'ToolSlot', 

1434 'pullout_direction': 1, 

1435 'x': 0, 

1436 'y': 0, 

1437 'z': 0, 

1438 'tool_id': 123, 

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

1440 **tray_data, 

1441 }, 

1442 ] 

1443 mock_response.json.return_value = mock_api_response 

1444 mock_response.status_code = 200 

1445 mock_response.text = 'text' 

1446 mock_request.return_value = mock_response 

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

1448 mock_request.assert_has_calls([ 

1449 call( 

1450 'GET', 

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

1452 **REQUEST_KWARGS, 

1453 ), 

1454 call().json(), 

1455 call( 

1456 'GET', 

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

1458 **REQUEST_KWARGS, 

1459 ), 

1460 call().json(), 

1461 ]) 

1462 self.assertEqual(cell, expected_xyz, kwargs) 

1463 

1464 def test_get_seed_tray_cell(self): 

1465 '''Test get_seed_tray_cell''' 

1466 test_cases = [ 

1467 { 

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

1469 'cell': 'a1', 

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

1471 }, 

1472 { 

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

1474 'cell': 'b2', 

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

1476 }, 

1477 { 

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

1479 'cell': 'd4', 

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

1481 }, 

1482 { 

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

1484 'cell': 'a1', 

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

1486 }, 

1487 { 

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

1489 'cell': 'b2', 

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

1491 }, 

1492 { 

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

1494 'cell': 'd4', 

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

1496 }, 

1497 { 

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

1499 'cell': 'd4', 

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

1501 }, 

1502 ] 

1503 for test_case in test_cases: 

1504 self.helper_get_seed_tray_cell(**test_case) 

1505 

1506 @patch('requests.request') 

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

1508 '''Test helper for get_seed_tray_cell command errors''' 

1509 mock_request = args[0] 

1510 tray_data = kwargs['tray_data'] 

1511 cell = kwargs['cell'] 

1512 error = kwargs['error'] 

1513 mock_response = Mock() 

1514 mock_api_response = [ 

1515 { 

1516 'id': 123, 

1517 'name': 'Seed Tray', 

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

1519 }, 

1520 { 

1521 'pointer_type': 'ToolSlot', 

1522 'pullout_direction': 1, 

1523 'x': 0, 

1524 'y': 0, 

1525 'z': 0, 

1526 'tool_id': 123, 

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

1528 **tray_data, 

1529 }, 

1530 ] 

1531 mock_response.json.return_value = mock_api_response 

1532 mock_response.status_code = 200 

1533 mock_response.text = 'text' 

1534 mock_request.return_value = mock_response 

1535 with self.assertRaises(ValueError) as cm: 

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

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

1538 mock_request.assert_has_calls([ 

1539 call( 

1540 'GET', 

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

1542 **REQUEST_KWARGS, 

1543 ), 

1544 call().json(), 

1545 call( 

1546 'GET', 

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

1548 **REQUEST_KWARGS, 

1549 ), 

1550 call().json(), 

1551 ]) 

1552 

1553 def test_get_seed_tray_cell_invalid_cell_name(self): 

1554 '''Test get_seed_tray_cell: invalid cell name''' 

1555 self.helper_get_seed_tray_cell_error( 

1556 tray_data={}, 

1557 cell='e4', 

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

1559 ) 

1560 

1561 def test_get_seed_tray_cell_invalid_pullout_direction(self): 

1562 '''Test get_seed_tray_cell: invalid pullout direction''' 

1563 self.helper_get_seed_tray_cell_error( 

1564 tray_data={'pullout_direction': 0}, 

1565 cell='d4', 

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

1567 ) 

1568 

1569 @patch('requests.request') 

1570 def test_get_seed_tray_cell_no_tray(self, mock_request): 

1571 '''Test get_seed_tray_cell: no seed tray''' 

1572 mock_response = Mock() 

1573 mock_api_response = [] 

1574 mock_response.json.return_value = mock_api_response 

1575 mock_response.status_code = 200 

1576 mock_response.text = 'text' 

1577 mock_request.return_value = mock_response 

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

1579 mock_request.assert_has_calls([ 

1580 call( 

1581 'GET', 

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

1583 **REQUEST_KWARGS, 

1584 ), 

1585 call().json(), 

1586 ]) 

1587 self.assertIsNone(result) 

1588 

1589 @patch('requests.request') 

1590 def test_get_seed_tray_cell_not_mounted(self, mock_request): 

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

1592 mock_response = Mock() 

1593 mock_api_response = [{ 

1594 'id': 123, 

1595 'name': 'Seed Tray', 

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

1597 }] 

1598 mock_response.json.return_value = mock_api_response 

1599 mock_response.status_code = 200 

1600 mock_response.text = 'text' 

1601 mock_request.return_value = mock_response 

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

1603 mock_request.assert_has_calls([ 

1604 call( 

1605 'GET', 

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

1607 **REQUEST_KWARGS, 

1608 ), 

1609 call().json(), 

1610 ]) 

1611 self.assertIsNone(result) 

1612 

1613 def test_get_job_one(self): 

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

1615 def exec_command(): 

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

1617 'jobs': { 

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

1619 }, 

1620 }] 

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

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

1623 self.send_command_test_helper( 

1624 exec_command, 

1625 expected_command={ 

1626 'kind': 'read_status', 

1627 'args': {}, 

1628 }, 

1629 extra_rpc_args={}, 

1630 mock_api_response={}) 

1631 

1632 def test_get_job_all(self): 

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

1634 def exec_command(): 

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

1636 'jobs': { 

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

1638 }, 

1639 }] 

1640 jobs = self.fb.get_job() 

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

1642 self.send_command_test_helper( 

1643 exec_command, 

1644 expected_command={ 

1645 'kind': 'read_status', 

1646 'args': {}, 

1647 }, 

1648 extra_rpc_args={}, 

1649 mock_api_response={}) 

1650 

1651 def test_get_job_no_status(self): 

1652 '''Test get_job command: no status''' 

1653 def exec_command(): 

1654 self.fb.state.last_messages['status'] = [] 

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

1656 self.assertIsNone(job) 

1657 self.send_command_test_helper( 

1658 exec_command, 

1659 expected_command={ 

1660 'kind': 'read_status', 

1661 'args': {}, 

1662 }, 

1663 extra_rpc_args={}, 

1664 mock_api_response={}) 

1665 

1666 def test_set_job(self): 

1667 '''Test set_job command''' 

1668 def exec_command(): 

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

1670 self.send_command_test_helper( 

1671 exec_command, 

1672 expected_command={ 

1673 'kind': 'lua', 

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

1675 set_job(job_name) 

1676 

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

1678 set_job(job_name, { 

1679 status = "working", 

1680 percent = 50 

1681 })'''}, 

1682 }, 

1683 extra_rpc_args={}, 

1684 mock_api_response={}) 

1685 

1686 def test_complete_job(self): 

1687 '''Test complete_job command''' 

1688 def exec_command(): 

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

1690 self.send_command_test_helper( 

1691 exec_command, 

1692 expected_command={ 

1693 'kind': 'lua', 

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

1695 }, 

1696 extra_rpc_args={}, 

1697 mock_api_response={}) 

1698 

1699 def test_lua(self): 

1700 '''Test lua command''' 

1701 def exec_command(): 

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

1703 self.send_command_test_helper( 

1704 exec_command, 

1705 expected_command={ 

1706 'kind': 'lua', 

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

1708 }, 

1709 extra_rpc_args={}, 

1710 mock_api_response={}) 

1711 

1712 def test_if_statement(self): 

1713 '''Test if_statement command''' 

1714 def exec_command(): 

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

1716 self.send_command_test_helper( 

1717 exec_command, 

1718 expected_command={ 

1719 'kind': '_if', 

1720 'args': { 

1721 'lhs': 'pin10', 

1722 'op': 'is', 

1723 'rhs': 0, 

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

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

1726 } 

1727 }, 

1728 extra_rpc_args={}, 

1729 mock_api_response=[]) 

1730 

1731 def test_if_statement_with_named_pin(self): 

1732 '''Test if_statement command with named pin''' 

1733 def exec_command(): 

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

1735 self.send_command_test_helper( 

1736 exec_command, 

1737 expected_command={ 

1738 'kind': '_if', 

1739 'args': { 

1740 'lhs': { 

1741 'kind': 'named_pin', 

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

1743 }, 

1744 'op': 'is', 

1745 'rhs': 0, 

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

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

1748 } 

1749 }, 

1750 extra_rpc_args={}, 

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

1752 

1753 def test_if_statement_with_named_pin_not_found(self): 

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

1755 def exec_command(): 

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

1757 self.send_command_test_helper( 

1758 exec_command, 

1759 expected_command=None, 

1760 extra_rpc_args={}, 

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

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

1763 

1764 def test_if_statement_with_sequences(self): 

1765 '''Test if_statement command with sequences''' 

1766 def exec_command(): 

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

1768 self.send_command_test_helper( 

1769 exec_command, 

1770 expected_command={ 

1771 'kind': '_if', 

1772 'args': { 

1773 'lhs': 'pin10', 

1774 'op': '<', 

1775 'rhs': 0, 

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

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

1778 } 

1779 }, 

1780 extra_rpc_args={}, 

1781 mock_api_response=[ 

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

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

1784 ]) 

1785 

1786 def test_if_statement_with_sequence_not_found(self): 

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

1788 def exec_command(): 

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

1790 self.send_command_test_helper( 

1791 exec_command, 

1792 expected_command=None, 

1793 extra_rpc_args={}, 

1794 mock_api_response=[]) 

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

1796 

1797 def test_if_statement_invalid_operator(self): 

1798 '''Test if_statement command: invalid operator''' 

1799 def exec_command(): 

1800 with self.assertRaises(ValueError) as cm: 

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

1802 self.assertEqual( 

1803 cm.exception.args[0], 

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

1805 self.send_command_test_helper( 

1806 exec_command, 

1807 expected_command=None, 

1808 extra_rpc_args={}, 

1809 mock_api_response=[]) 

1810 

1811 def test_if_statement_invalid_variable(self): 

1812 '''Test if_statement command: invalid variable''' 

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

1814 def exec_command(): 

1815 with self.assertRaises(ValueError) as cm: 

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

1817 self.assertEqual( 

1818 cm.exception.args[0], 

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

1820 self.send_command_test_helper( 

1821 exec_command, 

1822 expected_command=None, 

1823 extra_rpc_args={}, 

1824 mock_api_response=[]) 

1825 

1826 def test_if_statement_invalid_named_pin_type(self): 

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

1828 def exec_command(): 

1829 with self.assertRaises(ValueError) as cm: 

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

1831 self.assertEqual( 

1832 cm.exception.args[0], 

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

1834 self.send_command_test_helper( 

1835 exec_command, 

1836 expected_command=None, 

1837 extra_rpc_args={}, 

1838 mock_api_response=[]) 

1839 

1840 def test_rpc_error(self): 

1841 '''Test rpc error handling''' 

1842 def exec_command(): 

1843 self.fb.wait(100) 

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

1845 self.send_command_test_helper( 

1846 exec_command, 

1847 error=True, 

1848 expected_command={ 

1849 'kind': 'wait', 

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

1851 extra_rpc_args={}, 

1852 mock_api_response=[]) 

1853 

1854 def test_rpc_response_timeout(self): 

1855 '''Test rpc response timeout handling''' 

1856 def exec_command(): 

1857 self.fb.state.last_messages['from_device'] = [ 

1858 {'kind': 'rpc_ok', 'args': {'label': 'wrong label'}}, 

1859 ] 

1860 self.fb.wait(100) 

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

1862 self.send_command_test_helper( 

1863 exec_command, 

1864 expected_command={ 

1865 'kind': 'wait', 

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

1867 extra_rpc_args={}, 

1868 mock_api_response=[]) 

1869 

1870 def test_set_verbosity(self): 

1871 '''Test set_verbosity.''' 

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

1873 self.fb.set_verbosity(1) 

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

1875 

1876 def test_set_timeout(self): 

1877 '''Test set_timeout.''' 

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

1879 self.fb.set_timeout(15) 

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

1881 

1882 @staticmethod 

1883 def helper_get_print_strings(mock_print): 

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

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

1886 

1887 @patch('builtins.print') 

1888 def test_print_status(self, mock_print): 

1889 '''Test print_status.''' 

1890 self.fb.set_verbosity(0) 

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

1892 mock_print.assert_not_called() 

1893 self.fb.set_verbosity(1) 

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

1895 call_strings = self.helper_get_print_strings(mock_print) 

1896 self.assertIn('testing', call_strings) 

1897 mock_print.reset_mock() 

1898 self.fb.set_verbosity(2) 

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

1900 call_strings = self.helper_get_print_strings(mock_print) 

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

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

1903 self.assertIn('test_print_status', call_strings)