Coverage for tests/tests_main.py: 100%

837 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-09-11 15:46 -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 

41 

42class TestFarmbot(unittest.TestCase): 

43 '''Farmbot tests''' 

44 

45 def setUp(self): 

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

47 self.fb = Farmbot() 

48 self.fb.set_token(MOCK_TOKEN) 

49 self.fb.set_verbosity(0) 

50 self.fb.state.test_env = True 

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

52 self.fb.clear_cache() 

53 

54 @patch('requests.post') 

55 def test_get_token_default_server(self, mock_post): 

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

57 mock_response = Mock() 

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

59 mock_response.json.return_value = expected_token 

60 mock_response.status_code = 200 

61 mock_response.text = 'text' 

62 mock_post.return_value = mock_response 

63 self.fb.set_token(None) 

64 # Call with default server 

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

66 mock_post.assert_called_once_with( 

67 url='https://my.farm.bot/api/tokens', 

68 **TOKEN_REQUEST_KWARGS, 

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

70 'password': 'test_pass_123'}}, 

71 ) 

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

73 

74 @patch('requests.post') 

75 def test_get_token_custom_server(self, mock_post): 

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

77 mock_response = Mock() 

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

79 mock_response.json.return_value = expected_token 

80 mock_response.status_code = 200 

81 mock_response.text = 'text' 

82 mock_post.return_value = mock_response 

83 self.fb.set_token(None) 

84 # Call with custom server 

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

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

87 mock_post.assert_called_once_with( 

88 url='https://staging.farm.bot/api/tokens', 

89 **TOKEN_REQUEST_KWARGS, 

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

91 'password': 'test_pass_123'}}, 

92 ) 

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

94 

95 @patch('requests.post') 

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

97 '''Test helper for get_token errors''' 

98 mock_post = args[0] 

99 status_code = kwargs['status_code'] 

100 error_msg = kwargs['error_msg'] 

101 mock_response = Mock() 

102 mock_response.status_code = status_code 

103 mock_post.return_value = mock_response 

104 self.fb.set_token(None) 

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

106 mock_post.assert_called_once_with( 

107 url='https://my.farm.bot/api/tokens', 

108 **TOKEN_REQUEST_KWARGS, 

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

110 'password': 'test_pass_123'}}, 

111 ) 

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

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

114 

115 def test_get_token_bad_email(self): 

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

117 self.helper_get_token_errors( 

118 status_code=422, 

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

120 ) 

121 

122 def test_get_token_bad_server(self): 

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

124 self.helper_get_token_errors( 

125 status_code=404, 

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

127 ) 

128 

129 def test_get_token_other_error(self): 

130 '''get_token: other error''' 

131 self.helper_get_token_errors( 

132 status_code=500, 

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

134 ) 

135 

136 @patch('requests.post') 

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

138 '''Test helper for get_token exceptions''' 

139 mock_post = args[0] 

140 exception = kwargs['exception'] 

141 error_msg = kwargs['error_msg'] 

142 mock_post.side_effect = exception 

143 self.fb.set_token(None) 

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

145 mock_post.assert_called_once_with( 

146 url='https://my.farm.bot/api/tokens', 

147 **TOKEN_REQUEST_KWARGS, 

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

149 'password': 'test_pass_123'}}, 

150 ) 

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

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

153 

154 def test_get_token_server_not_found(self): 

155 '''get_token: server not found''' 

156 self.helper_get_token_exceptions( 

157 exception=requests.exceptions.ConnectionError, 

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

159 ) 

160 

161 def test_get_token_timeout(self): 

162 '''get_token: timeout''' 

163 self.helper_get_token_exceptions( 

164 exception=requests.exceptions.Timeout, 

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

166 ) 

167 

168 def test_get_token_problem(self): 

169 '''get_token: problem''' 

170 self.helper_get_token_exceptions( 

171 exception=requests.exceptions.RequestException, 

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

173 ) 

174 

175 def test_get_token_other_exception(self): 

176 '''get_token: other exception''' 

177 self.helper_get_token_exceptions( 

178 exception=Exception('other'), 

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

180 ) 

181 

182 @patch('requests.request') 

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

184 '''Test helper for api_get errors''' 

185 mock_request = args[0] 

186 status_code = kwargs['status_code'] 

187 error_msg = kwargs['error_msg'] 

188 mock_response = Mock() 

189 mock_response.status_code = status_code 

190 mock_response.reason = 'reason' 

191 mock_response.text = 'text' 

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

193 mock_request.return_value = mock_response 

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

195 mock_request.assert_called_once_with( 

196 method='GET', 

197 url='https://my.farm.bot/api/device', 

198 **REQUEST_KWARGS, 

199 ) 

200 self.assertEqual(response, error_msg) 

201 

202 def test_api_get_errors(self): 

203 '''Test api_get errors''' 

204 msg_404 = 'CLIENT ERROR 404: The specified endpoint does not exist.' 

205 msg_404 += ' ({\n "error": "error"\n})' 

206 self.helper_api_get_error( 

207 status_code=404, 

208 error_msg=msg_404 

209 ) 

210 self.helper_api_get_error( 

211 status_code=500, 

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

213 ) 

214 self.helper_api_get_error( 

215 status_code=600, 

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

217 ) 

218 

219 @patch('requests.request') 

220 def test_api_string_error_response_handling(self, mock_request): 

221 '''Test API string response errors''' 

222 mock_response = Mock() 

223 mock_response.status_code = 404 

224 mock_response.reason = 'reason' 

225 mock_response.text = 'error string' 

226 mock_response.json.side_effect = requests.exceptions.JSONDecodeError( 

227 '', '', 0) 

228 mock_request.return_value = mock_response 

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

230 mock_request.assert_called_once_with( 

231 method='GET', 

232 url='https://my.farm.bot/api/device', 

233 **REQUEST_KWARGS, 

234 ) 

235 self.assertEqual( 

236 response, 

237 'CLIENT ERROR 404: The specified endpoint does not exist. (error string)') 

238 

239 @patch('requests.request') 

240 def test_api_string_error_response_handling_html(self, mock_request): 

241 '''Test API html string response errors''' 

242 mock_response = Mock() 

243 mock_response.status_code = 404 

244 mock_response.reason = 'reason' 

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

246 mock_response.json.side_effect = requests.exceptions.JSONDecodeError( 

247 '', '', 0) 

248 mock_request.return_value = mock_response 

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

250 mock_request.assert_called_once_with( 

251 method='GET', 

252 url='https://my.farm.bot/api/device', 

253 **REQUEST_KWARGS, 

254 ) 

255 self.assertEqual( 

256 response, 

257 'CLIENT ERROR 404: The specified endpoint does not exist. (error0 error1)') 

258 

259 @patch('requests.request') 

260 def test_api_get_endpoint_only(self, mock_request): 

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

262 mock_response = Mock() 

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

264 mock_response.json.return_value = expected_response 

265 mock_response.status_code = 200 

266 mock_response.text = 'text' 

267 mock_request.return_value = mock_response 

268 # Call with endpoint only 

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

270 mock_request.assert_called_once_with( 

271 method='GET', 

272 url='https://my.farm.bot/api/device', 

273 **REQUEST_KWARGS, 

274 ) 

275 self.assertEqual(response, expected_response) 

276 

277 @patch('requests.request') 

278 def test_api_get_with_id(self, mock_request): 

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

280 mock_response = Mock() 

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

282 mock_response.json.return_value = expected_response 

283 mock_response.status_code = 200 

284 mock_response.text = 'text' 

285 mock_request.return_value = mock_response 

286 # Call with specific ID 

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

288 mock_request.assert_called_once_with( 

289 method='GET', 

290 url='https://my.farm.bot/api/peripherals/12345', 

291 **REQUEST_KWARGS, 

292 ) 

293 self.assertEqual(response, expected_response) 

294 

295 @patch('requests.request') 

296 def test_check_token_api_request(self, mock_request): 

297 '''Test check_token: API request''' 

298 self.fb.set_token(None) 

299 with self.assertRaises(ValueError) as cm: 

300 self.fb.api_get('points') 

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

302 mock_request.assert_not_called() 

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

304 

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

306 @patch('requests.request') 

307 def test_check_token_broker(self, mock_request, mock_mqtt): 

308 '''Test check_token: broker''' 

309 mock_client = Mock() 

310 mock_mqtt.return_value = mock_client 

311 self.fb.set_token(None) 

312 with self.assertRaises(ValueError) as cm: 

313 self.fb.on(123) 

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

315 with self.assertRaises(ValueError) as cm: 

316 self.fb.read_sensor(123) 

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

318 with self.assertRaises(ValueError) as cm: 

319 self.fb.get_xyz() 

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

321 with self.assertRaises(ValueError) as cm: 

322 self.fb.read_status() 

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

324 mock_request.assert_not_called() 

325 mock_client.publish.assert_not_called() 

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

327 

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

329 def test_publish_disabled(self, mock_mqtt): 

330 '''Test publish disabled''' 

331 mock_client = Mock() 

332 mock_mqtt.return_value = mock_client 

333 self.fb.state.dry_run = True 

334 self.fb.on(123) 

335 mock_client.publish.assert_not_called() 

336 

337 @patch('requests.request') 

338 def test_api_patch(self, mock_request): 

339 '''test api_patch function''' 

340 mock_response = Mock() 

341 mock_response.status_code = 200 

342 mock_response.text = 'text' 

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

344 mock_request.return_value = mock_response 

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

346 mock_request.assert_has_calls([call( 

347 method='PATCH', 

348 url='https://my.farm.bot/api/device', 

349 **REQUEST_KWARGS_WITH_PAYLOAD, 

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

351 ), 

352 call().json(), 

353 ]) 

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

355 

356 @patch('requests.request') 

357 def test_api_post(self, mock_request): 

358 '''test api_post function''' 

359 mock_response = Mock() 

360 mock_response.status_code = 200 

361 mock_response.text = 'text' 

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

363 mock_request.return_value = mock_response 

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

365 mock_request.assert_has_calls([call( 

366 method='POST', 

367 url='https://my.farm.bot/api/points', 

368 **REQUEST_KWARGS_WITH_PAYLOAD, 

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

370 ), 

371 call().json(), 

372 ]) 

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

374 

375 @patch('requests.request') 

376 def test_api_delete(self, mock_request): 

377 '''test api_delete function''' 

378 mock_response = Mock() 

379 mock_response.status_code = 200 

380 mock_response.text = 'text' 

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

382 mock_request.return_value = mock_response 

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

384 mock_request.assert_called_once_with( 

385 method='DELETE', 

386 url='https://my.farm.bot/api/points/12345', 

387 **REQUEST_KWARGS, 

388 ) 

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

390 

391 @patch('requests.request') 

392 def test_api_delete_requests_disabled(self, mock_request): 

393 '''test api_delete function: requests disabled''' 

394 self.fb.state.dry_run = True 

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

396 mock_request.assert_not_called() 

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

398 

399 @patch('requests.request') 

400 def test_group_one(self, mock_request): 

401 '''test group function: get one group''' 

402 mock_response = Mock() 

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

404 mock_response.status_code = 200 

405 mock_response.text = 'text' 

406 mock_request.return_value = mock_response 

407 group_info = self.fb.group(12345) 

408 mock_request.assert_called_once_with( 

409 method='GET', 

410 url='https://my.farm.bot/api/point_groups/12345', 

411 **REQUEST_KWARGS, 

412 ) 

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

414 

415 @patch('requests.request') 

416 def test_group_all(self, mock_request): 

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

418 mock_response = Mock() 

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

420 mock_response.status_code = 200 

421 mock_response.text = 'text' 

422 mock_request.return_value = mock_response 

423 group_info = self.fb.group() 

424 mock_request.assert_called_once_with( 

425 method='GET', 

426 url='https://my.farm.bot/api/point_groups', 

427 **REQUEST_KWARGS, 

428 ) 

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

430 

431 @patch('requests.request') 

432 def test_curve_one(self, mock_request): 

433 '''test curve function: get one curve''' 

434 mock_response = Mock() 

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

436 mock_response.status_code = 200 

437 mock_response.text = 'text' 

438 mock_request.return_value = mock_response 

439 curve_info = self.fb.curve(12345) 

440 mock_request.assert_called_once_with( 

441 method='GET', 

442 url='https://my.farm.bot/api/curves/12345', 

443 **REQUEST_KWARGS, 

444 ) 

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

446 

447 @patch('requests.request') 

448 def test_curve_all(self, mock_request): 

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

450 mock_response = Mock() 

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

452 mock_response.status_code = 200 

453 mock_response.text = 'text' 

454 mock_request.return_value = mock_response 

455 curve_info = self.fb.curve() 

456 mock_request.assert_called_once_with( 

457 method='GET', 

458 url='https://my.farm.bot/api/curves', 

459 **REQUEST_KWARGS, 

460 ) 

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

462 

463 @patch('requests.request') 

464 def test_safe_z(self, mock_request): 

465 '''test safe_z function''' 

466 mock_response = Mock() 

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

468 mock_response.status_code = 200 

469 mock_response.text = 'text' 

470 mock_request.return_value = mock_response 

471 safe_height = self.fb.safe_z() 

472 mock_request.assert_called_once_with( 

473 method='GET', 

474 url='https://my.farm.bot/api/fbos_config', 

475 **REQUEST_KWARGS, 

476 ) 

477 self.assertEqual(safe_height, 100) 

478 

479 @patch('requests.request') 

480 def test_garden_size(self, mock_request): 

481 '''test garden_size function''' 

482 mock_response = Mock() 

483 mock_response.json.return_value = { 

484 'movement_axis_nr_steps_x': 1000, 

485 'movement_axis_nr_steps_y': 2000, 

486 'movement_axis_nr_steps_z': 40000, 

487 'movement_step_per_mm_x': 5, 

488 'movement_step_per_mm_y': 5, 

489 'movement_step_per_mm_z': 25, 

490 } 

491 mock_response.status_code = 200 

492 mock_response.text = 'text' 

493 mock_request.return_value = mock_response 

494 garden_size = self.fb.garden_size() 

495 mock_request.assert_called_once_with( 

496 method='GET', 

497 url='https://my.farm.bot/api/firmware_config', 

498 **REQUEST_KWARGS, 

499 ) 

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

501 

502 @patch('requests.request') 

503 def test_log(self, mock_request): 

504 '''test log function''' 

505 mock_response = Mock() 

506 mock_response.status_code = 200 

507 mock_response.text = 'text' 

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

509 mock_request.return_value = mock_response 

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

511 mock_request.assert_called_once_with( 

512 method='POST', 

513 url='https://my.farm.bot/api/logs', 

514 **REQUEST_KWARGS_WITH_PAYLOAD, 

515 json={ 

516 'message': 'test message', 

517 'type': 'info', 

518 'channels': ['toast'], 

519 }, 

520 ) 

521 

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

523 def test_connect_broker(self, mock_mqtt): 

524 '''Test test_connect_broker command''' 

525 mock_client = Mock() 

526 mock_mqtt.return_value = mock_client 

527 self.fb.connect_broker() 

528 mock_client.username_pw_set.assert_called_once_with( 

529 username='device_0', 

530 password='encoded_token_value') 

531 mock_client.connect.assert_called_once_with( 

532 'mqtt_url', 

533 port=1883, 

534 keepalive=60) 

535 mock_client.loop_start.assert_called() 

536 

537 def test_disconnect_broker(self): 

538 '''Test disconnect_broker command''' 

539 mock_client = Mock() 

540 self.fb.broker.client = mock_client 

541 self.fb.disconnect_broker() 

542 mock_client.loop_stop.assert_called_once() 

543 mock_client.disconnect.assert_called_once() 

544 

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

546 def test_listen(self, mock_mqtt): 

547 '''Test listen command''' 

548 mock_client = Mock() 

549 mock_mqtt.return_value = mock_client 

550 self.fb.listen() 

551 

552 class MockMessage: 

553 '''Mock message class''' 

554 topic = 'topic' 

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

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

557 mock_client.username_pw_set.assert_called_once_with( 

558 username='device_0', 

559 password='encoded_token_value') 

560 mock_client.connect.assert_called_once_with( 

561 'mqtt_url', 

562 port=1883, 

563 keepalive=60) 

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

565 mock_client.loop_start.assert_called() 

566 mock_client.loop_stop.assert_called() 

567 

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

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

570 def test_listen_for_status_changes(self, mock_mqtt): 

571 '''Test listen_for_status_changes command''' 

572 self.maxDiff = None 

573 i = 0 

574 

575 mock_client = Mock() 

576 mock_mqtt.return_value = mock_client 

577 

578 class MockMessage: 

579 '''Mock message class''' 

580 

581 def __init__(self): 

582 self.topic = '/status' 

583 payload = { 

584 'location_data': { 

585 'position': { 

586 'x': i, 

587 'y': i + 10, 

588 'z': 100, 

589 }}} 

590 if i == 2: 

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

592 if i == 3: 

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

594 self.payload = json.dumps(payload) 

595 

596 def patched_sleep(_seconds): 

597 '''Patched sleep function''' 

598 nonlocal i 

599 mock_message = MockMessage() 

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

601 i += 1 

602 

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

604 self.fb.listen_for_status_changes( 

605 stop_count=5, 

606 info_path='location_data.position') 

607 

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

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

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

611 {'location_data': {'position': { 

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

613 {'location_data': {'position': { 

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

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

616 ]) 

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

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

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

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

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

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

623 ]) 

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

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

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

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

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

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

630 ]) 

631 

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

633 def test_listen_clear_last(self, mock_mqtt): 

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

635 mock_client = Mock() 

636 mock_mqtt.return_value = mock_client 

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

638 self.fb.state.test_env = False 

639 self.fb.listen() 

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

641 

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

643 def test_publish_apply_label(self, mock_mqtt): 

644 '''Test publish command: set uuid''' 

645 mock_client = Mock() 

646 mock_mqtt.return_value = mock_client 

647 self.fb.state.test_env = False 

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

649 label = self.fb.state.last_published.get('args', {}).get('label') 

650 self.assertNotIn(label, ['test', '', None]) 

651 

652 @patch('requests.request') 

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

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

655 '''Helper for testing command execution''' 

656 execute_command = args[0] 

657 mock_mqtt = args[1] 

658 mock_request = args[2] 

659 expected_command = kwargs.get('expected_command') 

660 extra_rpc_args = kwargs.get('extra_rpc_args') 

661 mock_api_response = kwargs.get('mock_api_response') 

662 error = kwargs.get('error') 

663 mock_client = Mock() 

664 mock_mqtt.return_value = mock_client 

665 mock_response = Mock() 

666 mock_response.json.return_value = mock_api_response 

667 mock_response.status_code = 200 

668 mock_response.text = 'text' 

669 mock_request.return_value = mock_response 

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

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

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

673 }] 

674 execute_command() 

675 if expected_command is None: 

676 mock_client.publish.assert_not_called() 

677 return 

678 expected_payload = { 

679 'kind': 'rpc_request', 

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

681 'body': [expected_command], 

682 } 

683 mock_client.username_pw_set.assert_called_once_with( 

684 username='device_0', 

685 password='encoded_token_value') 

686 mock_client.connect.assert_called_once_with( 

687 'mqtt_url', 

688 port=1883, 

689 keepalive=60) 

690 mock_client.loop_start.assert_called() 

691 mock_client.publish.assert_called_once_with( 

692 'bot/device_0/from_clients', 

693 payload=json.dumps(expected_payload)) 

694 if not error: 

695 self.assertNotEqual( 

696 self.fb.state.error, 

697 'RPC error response received.') 

698 

699 def test_message(self): 

700 '''Test message command''' 

701 def exec_command(): 

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

703 self.send_command_test_helper( 

704 exec_command, 

705 expected_command={ 

706 'kind': 'send_message', 

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

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

709 }, 

710 extra_rpc_args={}, 

711 mock_api_response={}) 

712 

713 def test_debug(self): 

714 '''Test debug command''' 

715 def exec_command(): 

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

717 self.send_command_test_helper( 

718 exec_command, 

719 expected_command={ 

720 'kind': 'send_message', 

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

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

723 }, 

724 extra_rpc_args={}, 

725 mock_api_response={}) 

726 

727 def test_toast(self): 

728 '''Test toast command''' 

729 def exec_command(): 

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

731 self.send_command_test_helper( 

732 exec_command, 

733 expected_command={ 

734 'kind': 'send_message', 

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

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

737 }, 

738 extra_rpc_args={}, 

739 mock_api_response={}) 

740 

741 def test_invalid_message_type(self): 

742 '''Test message_type validation''' 

743 def exec_command(): 

744 with self.assertRaises(ValueError) as cm: 

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

746 msg = 'Invalid message type: `nope` not in ' 

747 msg += "['assertion', 'busy', 'debug', 'error', 'fun', 'info', 'success', 'warn']" 

748 self.assertEqual(cm.exception.args[0], msg) 

749 self.send_command_test_helper( 

750 exec_command, 

751 expected_command=None, 

752 extra_rpc_args={}, 

753 mock_api_response={}) 

754 

755 def test_invalid_message_channel(self): 

756 '''Test message channel validation''' 

757 def exec_command(): 

758 with self.assertRaises(ValueError) as cm: 

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

760 self.assertEqual( 

761 cm.exception.args[0], 

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

763 self.send_command_test_helper( 

764 exec_command, 

765 expected_command=None, 

766 extra_rpc_args={}, 

767 mock_api_response={}) 

768 

769 def test_read_status(self): 

770 '''Test read_status command''' 

771 def exec_command(): 

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

773 'location_data': {'position': {'x': 100}}, 

774 }] 

775 result = self.fb.read_status() 

776 self.assertEqual( 

777 result, 

778 {'location_data': {'position': {'x': 100}}}) 

779 self.send_command_test_helper( 

780 exec_command, 

781 expected_command={ 

782 'kind': 'read_status', 

783 'args': {}, 

784 }, 

785 extra_rpc_args={}, 

786 mock_api_response={}) 

787 

788 def test_read_status_path(self): 

789 '''Test read_status command: specific path''' 

790 def exec_command(): 

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

792 'location_data': {'position': {'x': 100}}, 

793 }] 

794 result = self.fb.read_status('location_data.position.x') 

795 self.assertEqual(result, 100) 

796 self.send_command_test_helper( 

797 exec_command, 

798 expected_command={ 

799 'kind': 'read_status', 

800 'args': {}, 

801 }, 

802 extra_rpc_args={}, 

803 mock_api_response={}) 

804 

805 def test_read_pin(self): 

806 '''Test read_pin command''' 

807 def exec_command(): 

808 self.fb.read_pin(13) 

809 self.send_command_test_helper( 

810 exec_command, 

811 expected_command={ 

812 'kind': 'read_pin', 

813 'args': { 

814 'pin_number': 13, 

815 'label': '---', 

816 'pin_mode': 0, 

817 }, 

818 }, 

819 extra_rpc_args={}, 

820 mock_api_response={}) 

821 

822 def test_read_sensor(self): 

823 '''Test read_sensor command''' 

824 def exec_command(): 

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

826 self.send_command_test_helper( 

827 exec_command, 

828 expected_command={ 

829 'kind': 'read_pin', 

830 'args': { 

831 'pin_mode': 0, 

832 'label': '---', 

833 'pin_number': { 

834 'kind': 'named_pin', 

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

836 }, 

837 }, 

838 }, 

839 extra_rpc_args={}, 

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

841 

842 def test_read_sensor_not_found(self): 

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

844 def exec_command(): 

845 self.fb.read_sensor('Temperature') 

846 self.send_command_test_helper( 

847 exec_command, 

848 expected_command=None, 

849 extra_rpc_args={}, 

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

851 self.assertEqual( 

852 self.fb.state.error, 

853 "ERROR: 'Temperature' not in sensors: ['Tool Verification'].") 

854 

855 def test_assertion(self): 

856 '''Test assertion command''' 

857 def exec_command(): 

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

859 self.send_command_test_helper( 

860 exec_command, 

861 expected_command={ 

862 'kind': 'assertion', 

863 'args': { 

864 'assertion_type': 'abort', 

865 'lua': 'return true', 

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

867 } 

868 }, 

869 extra_rpc_args={}, 

870 mock_api_response={}) 

871 

872 def test_assertion_with_recovery_sequence(self): 

873 '''Test assertion command with recovery sequence''' 

874 def exec_command(): 

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

876 self.send_command_test_helper( 

877 exec_command, 

878 expected_command={ 

879 'kind': 'assertion', 

880 'args': { 

881 'assertion_type': 'abort', 

882 'lua': 'return true', 

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

884 } 

885 }, 

886 extra_rpc_args={}, 

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

888 

889 def test_assertion_recovery_sequence_not_found(self): 

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

891 def exec_command(): 

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

893 self.send_command_test_helper( 

894 exec_command, 

895 expected_command=None, 

896 extra_rpc_args={}, 

897 mock_api_response=[]) 

898 self.assertEqual( 

899 self.fb.state.error, 

900 "ERROR: 'Recovery Sequence' not in sequences: [].") 

901 

902 def test_assertion_invalid_assertion_type(self): 

903 '''Test assertion command: invalid assertion type''' 

904 def exec_command(): 

905 with self.assertRaises(ValueError) as cm: 

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

907 msg = 'Invalid assertion_type: nope not in ' 

908 msg += "['abort', 'recover', 'abort_recover', 'continue']" 

909 self.assertEqual(cm.exception.args[0], msg) 

910 self.send_command_test_helper( 

911 exec_command, 

912 expected_command=None, 

913 extra_rpc_args={}, 

914 mock_api_response={}) 

915 

916 def test_wait(self): 

917 '''Test wait command''' 

918 def exec_command(): 

919 self.fb.wait(123) 

920 self.send_command_test_helper( 

921 exec_command, 

922 expected_command={ 

923 'kind': 'wait', 

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

925 }, 

926 extra_rpc_args={}, 

927 mock_api_response={}) 

928 

929 def test_unlock(self): 

930 '''Test unlock command''' 

931 def exec_command(): 

932 self.fb.unlock() 

933 self.send_command_test_helper( 

934 exec_command, 

935 expected_command={ 

936 'kind': 'emergency_unlock', 

937 'args': {}, 

938 }, 

939 extra_rpc_args={'priority': 9000}, 

940 mock_api_response={}) 

941 

942 def test_e_stop(self): 

943 '''Test e_stop command''' 

944 def exec_command(): 

945 self.fb.e_stop() 

946 self.send_command_test_helper( 

947 exec_command, 

948 expected_command={ 

949 'kind': 'emergency_lock', 

950 'args': {}, 

951 }, 

952 extra_rpc_args={'priority': 9000}, 

953 mock_api_response={}) 

954 

955 def test_find_home(self): 

956 '''Test find_home command''' 

957 def exec_command(): 

958 self.fb.find_home() 

959 self.send_command_test_helper( 

960 exec_command, 

961 expected_command={ 

962 'kind': 'find_home', 

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

964 }, 

965 extra_rpc_args={}, 

966 mock_api_response={}) 

967 

968 def test_find_home_speed_error(self): 

969 '''Test find_home command: speed error''' 

970 def exec_command(): 

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

972 self.send_command_test_helper( 

973 exec_command, 

974 expected_command=None, 

975 extra_rpc_args={}, 

976 mock_api_response={}) 

977 self.assertEqual( 

978 self.fb.state.error, 

979 'ERROR: Speed constrained to 1-100.') 

980 

981 def test_find_home_invalid_axis(self): 

982 '''Test find_home command: invalid axis''' 

983 def exec_command(): 

984 with self.assertRaises(ValueError) as cm: 

985 self.fb.find_home('nope') 

986 self.assertEqual( 

987 cm.exception.args[0], 

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

989 self.send_command_test_helper( 

990 exec_command, 

991 expected_command=None, 

992 extra_rpc_args={}, 

993 mock_api_response={}) 

994 

995 def test_set_home(self): 

996 '''Test set_home command''' 

997 def exec_command(): 

998 self.fb.set_home() 

999 self.send_command_test_helper( 

1000 exec_command, 

1001 expected_command={ 

1002 'kind': 'zero', 

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

1004 }, 

1005 extra_rpc_args={}, 

1006 mock_api_response={}) 

1007 

1008 def test_toggle_peripheral(self): 

1009 '''Test toggle_peripheral command''' 

1010 def exec_command(): 

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

1012 self.send_command_test_helper( 

1013 exec_command, 

1014 expected_command={ 

1015 'kind': 'toggle_pin', 

1016 'args': { 

1017 'pin_number': { 

1018 'kind': 'named_pin', 

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

1020 }, 

1021 }, 

1022 }, 

1023 extra_rpc_args={}, 

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

1025 

1026 def test_toggle_peripheral_not_found(self): 

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

1028 def exec_command(): 

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

1030 self.send_command_test_helper( 

1031 exec_command, 

1032 expected_command=None, 

1033 extra_rpc_args={}, 

1034 mock_api_response=[]) 

1035 self.assertEqual( 

1036 self.fb.state.error, 

1037 'ERROR: \'New Peripheral\' not in peripherals: [].') 

1038 

1039 @patch('requests.request') 

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

1041 def test_toggle_peripheral_use_cache(self, mock_mqtt, mock_request): 

1042 '''Test toggle_peripheral command: use cache''' 

1043 mock_client = Mock() 

1044 mock_mqtt.return_value = mock_client 

1045 mock_response = Mock() 

1046 mock_response.json.return_value = [ 

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

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

1049 ] 

1050 mock_response.status_code = 200 

1051 mock_response.text = 'text' 

1052 mock_request.return_value = mock_response 

1053 # save cache 

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

1055 mock_request.assert_called() 

1056 mock_client.publish.assert_called() 

1057 mock_request.reset_mock() 

1058 mock_client.reset_mock() 

1059 # use cache 

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

1061 mock_request.assert_not_called() 

1062 mock_client.publish.assert_called() 

1063 mock_request.reset_mock() 

1064 mock_client.reset_mock() 

1065 # clear cache 

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

1067 mock_request.assert_not_called() 

1068 mock_client.publish.assert_not_called() 

1069 mock_request.reset_mock() 

1070 mock_client.reset_mock() 

1071 # save cache 

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

1073 mock_request.assert_called() 

1074 mock_client.publish.assert_called() 

1075 mock_request.reset_mock() 

1076 mock_client.reset_mock() 

1077 

1078 def test_on_digital(self): 

1079 '''Test on command: digital''' 

1080 def exec_command(): 

1081 self.fb.on(13) 

1082 self.send_command_test_helper( 

1083 exec_command, 

1084 expected_command={ 

1085 'kind': 'write_pin', 

1086 'args': { 

1087 'pin_value': 1, 

1088 'pin_mode': 0, 

1089 'pin_number': 13, 

1090 }, 

1091 }, 

1092 extra_rpc_args={}, 

1093 mock_api_response={}) 

1094 

1095 def test_off(self): 

1096 '''Test off command''' 

1097 def exec_command(): 

1098 self.fb.off(13) 

1099 self.send_command_test_helper( 

1100 exec_command, 

1101 expected_command={ 

1102 'kind': 'write_pin', 

1103 'args': { 

1104 'pin_value': 0, 

1105 'pin_mode': 0, 

1106 'pin_number': 13, 

1107 }, 

1108 }, 

1109 extra_rpc_args={}, 

1110 mock_api_response={}) 

1111 

1112 def test_move(self): 

1113 '''Test move command''' 

1114 def exec_command(): 

1115 self.fb.move() 

1116 self.send_command_test_helper( 

1117 exec_command, 

1118 expected_command={ 

1119 'kind': 'move', 

1120 'args': {}, 

1121 'body': [], 

1122 }, 

1123 extra_rpc_args={}, 

1124 mock_api_response={}) 

1125 

1126 def test_move_extras(self): 

1127 '''Test move command with extras''' 

1128 def exec_command(): 

1129 self.fb.move(1, 2, 3, safe_z=True, speed=50) 

1130 self.send_command_test_helper( 

1131 exec_command, 

1132 expected_command={ 

1133 'kind': 'move', 

1134 'args': {}, 

1135 'body': [ 

1136 {'kind': 'axis_overwrite', 'args': { 

1137 'axis': 'x', 

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

1139 {'kind': 'axis_overwrite', 'args': { 

1140 'axis': 'y', 

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

1142 {'kind': 'axis_overwrite', 'args': { 

1143 'axis': 'z', 

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

1145 {'kind': 'speed_overwrite', 'args': { 

1146 'axis': 'x', 

1147 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}}, 

1148 {'kind': 'speed_overwrite', 'args': { 

1149 'axis': 'y', 

1150 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}}, 

1151 {'kind': 'speed_overwrite', 'args': { 

1152 'axis': 'z', 

1153 'speed_setting': {'kind': 'numeric', 'args': {'number': 50}}}}, 

1154 {'kind': 'safe_z', 'args': {}}, 

1155 ], 

1156 }, 

1157 extra_rpc_args={}, 

1158 mock_api_response={}) 

1159 

1160 def test_reboot(self): 

1161 '''Test reboot command''' 

1162 def exec_command(): 

1163 self.fb.reboot() 

1164 self.send_command_test_helper( 

1165 exec_command, 

1166 expected_command={ 

1167 'kind': 'reboot', 

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

1169 }, 

1170 extra_rpc_args={}, 

1171 mock_api_response={}) 

1172 

1173 def test_shutdown(self): 

1174 '''Test shutdown command''' 

1175 def exec_command(): 

1176 self.fb.shutdown() 

1177 self.send_command_test_helper( 

1178 exec_command, 

1179 expected_command={ 

1180 'kind': 'power_off', 

1181 'args': {}, 

1182 }, 

1183 extra_rpc_args={}, 

1184 mock_api_response={}) 

1185 

1186 def test_find_axis_length(self): 

1187 '''Test find_axis_length command''' 

1188 def exec_command(): 

1189 self.fb.find_axis_length() 

1190 self.send_command_test_helper( 

1191 exec_command, 

1192 expected_command={ 

1193 'kind': 'calibrate', 

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

1195 }, 

1196 extra_rpc_args={}, 

1197 mock_api_response={}) 

1198 

1199 def test_write_pin(self): 

1200 '''Test write_pin command''' 

1201 def exec_command(): 

1202 self.fb.write_pin(13, 1, 'analog') 

1203 self.send_command_test_helper( 

1204 exec_command, 

1205 expected_command={ 

1206 'kind': 'write_pin', 

1207 'args': { 

1208 'pin_number': 13, 

1209 'pin_value': 1, 

1210 'pin_mode': 1, 

1211 }, 

1212 }, 

1213 extra_rpc_args={}, 

1214 mock_api_response={}) 

1215 

1216 def test_write_pin_invalid_mode(self): 

1217 '''Test write_pin command: invalid mode''' 

1218 def exec_command(): 

1219 with self.assertRaises(ValueError) as cm: 

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

1221 self.assertEqual( 

1222 cm.exception.args[0], 

1223 "Invalid mode: 1 not in ['digital', 'analog']") 

1224 self.send_command_test_helper( 

1225 exec_command, 

1226 expected_command=None, 

1227 extra_rpc_args={}, 

1228 mock_api_response={}) 

1229 

1230 def test_control_peripheral(self): 

1231 '''Test control_peripheral command''' 

1232 def exec_command(): 

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

1234 self.send_command_test_helper( 

1235 exec_command, 

1236 expected_command={ 

1237 'kind': 'write_pin', 

1238 'args': { 

1239 'pin_value': 1, 

1240 'pin_mode': 0, 

1241 'pin_number': { 

1242 'kind': 'named_pin', 

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

1244 }, 

1245 }, 

1246 }, 

1247 extra_rpc_args={}, 

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

1249 

1250 def test_control_peripheral_analog(self): 

1251 '''Test control_peripheral command: analog''' 

1252 def exec_command(): 

1253 self.fb.control_peripheral('New Peripheral', 1, 'analog') 

1254 self.send_command_test_helper( 

1255 exec_command, 

1256 expected_command={ 

1257 'kind': 'write_pin', 

1258 'args': { 

1259 'pin_value': 1, 

1260 'pin_mode': 1, 

1261 'pin_number': { 

1262 'kind': 'named_pin', 

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

1264 }, 

1265 }, 

1266 }, 

1267 extra_rpc_args={}, 

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

1269 

1270 def test_control_peripheral_not_found(self): 

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

1272 def exec_command(): 

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

1274 self.send_command_test_helper( 

1275 exec_command, 

1276 expected_command=None, 

1277 extra_rpc_args={}, 

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

1279 self.assertEqual( 

1280 self.fb.state.error, 

1281 "ERROR: 'New Peripheral' not in peripherals: ['Pump', 'Lights'].") 

1282 

1283 def test_measure_soil_height(self): 

1284 '''Test measure_soil_height command''' 

1285 def exec_command(): 

1286 self.fb.measure_soil_height() 

1287 self.send_command_test_helper( 

1288 exec_command, 

1289 expected_command={ 

1290 'kind': 'execute_script', 

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

1292 }, 

1293 extra_rpc_args={}, 

1294 mock_api_response={}) 

1295 

1296 def test_detect_weeds(self): 

1297 '''Test detect_weeds command''' 

1298 def exec_command(): 

1299 self.fb.detect_weeds() 

1300 self.send_command_test_helper( 

1301 exec_command, 

1302 expected_command={ 

1303 'kind': 'execute_script', 

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

1305 }, 

1306 extra_rpc_args={}, 

1307 mock_api_response={}) 

1308 

1309 def test_calibrate_camera(self): 

1310 '''Test calibrate_camera command''' 

1311 def exec_command(): 

1312 self.fb.calibrate_camera() 

1313 self.send_command_test_helper( 

1314 exec_command, 

1315 expected_command={ 

1316 'kind': 'execute_script', 

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

1318 }, 

1319 extra_rpc_args={}, 

1320 mock_api_response={}) 

1321 

1322 def test_sequence(self): 

1323 '''Test sequence command''' 

1324 def exec_command(): 

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

1326 self.send_command_test_helper( 

1327 exec_command, 

1328 expected_command={ 

1329 'kind': 'execute', 

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

1331 }, 

1332 extra_rpc_args={}, 

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

1334 

1335 def test_sequence_not_found(self): 

1336 '''Test sequence command: sequence not found''' 

1337 def exec_command(): 

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

1339 self.send_command_test_helper( 

1340 exec_command, 

1341 expected_command=None, 

1342 extra_rpc_args={}, 

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

1344 self.assertEqual( 

1345 self.fb.state.error, 

1346 "ERROR: 'My Sequence' not in sequences: ['Water'].") 

1347 

1348 def test_take_photo(self): 

1349 '''Test take_photo command''' 

1350 def exec_command(): 

1351 self.fb.take_photo() 

1352 self.send_command_test_helper( 

1353 exec_command, 

1354 expected_command={ 

1355 'kind': 'take_photo', 

1356 'args': {}, 

1357 }, 

1358 extra_rpc_args={}, 

1359 mock_api_response={}) 

1360 

1361 def test_control_servo(self): 

1362 '''Test control_servo command''' 

1363 def exec_command(): 

1364 self.fb.control_servo(4, 100) 

1365 self.send_command_test_helper( 

1366 exec_command, 

1367 expected_command={ 

1368 'kind': 'set_servo_angle', 

1369 'args': { 

1370 'pin_number': 4, 

1371 'pin_value': 100, 

1372 }, 

1373 }, 

1374 extra_rpc_args={}, 

1375 mock_api_response={'mode': 0}) 

1376 

1377 def test_control_servo_error(self): 

1378 '''Test control_servo command: error''' 

1379 def exec_command(): 

1380 self.fb.control_servo(4, 200) 

1381 self.send_command_test_helper( 

1382 exec_command, 

1383 expected_command=None, 

1384 extra_rpc_args={}, 

1385 mock_api_response={'mode': 0}) 

1386 

1387 def test_get_xyz(self): 

1388 '''Test get_xyz command''' 

1389 def exec_command(): 

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

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

1392 }] 

1393 position = self.fb.get_xyz() 

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

1395 self.send_command_test_helper( 

1396 exec_command, 

1397 expected_command={ 

1398 'kind': 'read_status', 

1399 'args': {}, 

1400 }, 

1401 extra_rpc_args={}, 

1402 mock_api_response={}) 

1403 

1404 def test_get_xyz_no_status(self): 

1405 '''Test get_xyz command: no status''' 

1406 def exec_command(): 

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

1408 position = self.fb.get_xyz() 

1409 self.assertIsNone(position) 

1410 self.send_command_test_helper( 

1411 exec_command, 

1412 expected_command={ 

1413 'kind': 'read_status', 

1414 'args': {}, 

1415 }, 

1416 extra_rpc_args={}, 

1417 mock_api_response={}) 

1418 

1419 def test_check_position(self): 

1420 '''Test check_position command: at position''' 

1421 def exec_command(): 

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

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

1424 }] 

1425 at_position = self.fb.check_position({'x': 1, 'y': 2, 'z': 3}, 0) 

1426 self.assertTrue(at_position) 

1427 self.send_command_test_helper( 

1428 exec_command, 

1429 expected_command={ 

1430 'kind': 'read_status', 

1431 'args': {}, 

1432 }, 

1433 extra_rpc_args={}, 

1434 mock_api_response={}) 

1435 

1436 def test_check_position_false(self): 

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

1438 def exec_command(): 

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

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

1441 }] 

1442 at_position = self.fb.check_position({'x': 0, 'y': 0, 'z': 0}, 2) 

1443 self.assertFalse(at_position) 

1444 self.send_command_test_helper( 

1445 exec_command, 

1446 expected_command={ 

1447 'kind': 'read_status', 

1448 'args': {}, 

1449 }, 

1450 extra_rpc_args={}, 

1451 mock_api_response={}) 

1452 

1453 def test_check_position_no_status(self): 

1454 '''Test check_position command: no status''' 

1455 def exec_command(): 

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

1457 at_position = self.fb.check_position({'x': 0, 'y': 0, 'z': 0}, 2) 

1458 self.assertFalse(at_position) 

1459 self.send_command_test_helper( 

1460 exec_command, 

1461 expected_command={ 

1462 'kind': 'read_status', 

1463 'args': {}, 

1464 }, 

1465 extra_rpc_args={}, 

1466 mock_api_response={}) 

1467 

1468 def test_mount_tool(self): 

1469 '''Test mount_tool command''' 

1470 def exec_command(): 

1471 self.fb.mount_tool('Weeder') 

1472 self.send_command_test_helper( 

1473 exec_command, 

1474 expected_command={ 

1475 'kind': 'lua', 

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

1477 }, 

1478 extra_rpc_args={}, 

1479 mock_api_response={}) 

1480 

1481 def test_dismount_tool(self): 

1482 '''Test dismount_tool command''' 

1483 def exec_command(): 

1484 self.fb.dismount_tool() 

1485 self.send_command_test_helper( 

1486 exec_command, 

1487 expected_command={ 

1488 'kind': 'lua', 

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

1490 }, 

1491 extra_rpc_args={}, 

1492 mock_api_response={}) 

1493 

1494 def test_water(self): 

1495 '''Test water command''' 

1496 def exec_command(): 

1497 self.fb.water(123) 

1498 self.send_command_test_helper( 

1499 exec_command, 

1500 expected_command={ 

1501 'kind': 'lua', 

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

1503 method = "GET", 

1504 url = "/api/points/123" 

1505 }) 

1506 water(plant)'''}, 

1507 }, 

1508 extra_rpc_args={}, 

1509 mock_api_response={}) 

1510 

1511 def test_dispense(self): 

1512 '''Test dispense command''' 

1513 def exec_command(): 

1514 self.fb.dispense(100) 

1515 self.send_command_test_helper( 

1516 exec_command, 

1517 expected_command={ 

1518 'kind': 'lua', 

1519 'args': { 

1520 'lua': 'dispense(100)', 

1521 }, 

1522 }, 

1523 extra_rpc_args={}, 

1524 mock_api_response={}) 

1525 

1526 def test_dispense_all_args(self): 

1527 '''Test dispense command with all args''' 

1528 def exec_command(): 

1529 self.fb.dispense(100, 'Nutrient Sprayer', 4) 

1530 self.send_command_test_helper( 

1531 exec_command, 

1532 expected_command={ 

1533 'kind': 'lua', 

1534 'args': { 

1535 'lua': 'dispense(100, {tool_name = "Nutrient Sprayer", pin = 4})', 

1536 }, 

1537 }, 

1538 extra_rpc_args={}, 

1539 mock_api_response={}) 

1540 

1541 def test_dispense_only_pin(self): 

1542 '''Test dispense command''' 

1543 def exec_command(): 

1544 self.fb.dispense(100, pin=4) 

1545 self.send_command_test_helper( 

1546 exec_command, 

1547 expected_command={ 

1548 'kind': 'lua', 

1549 'args': { 

1550 'lua': 'dispense(100, {pin = 4})', 

1551 }, 

1552 }, 

1553 extra_rpc_args={}, 

1554 mock_api_response={}) 

1555 

1556 def test_dispense_only_tool_name(self): 

1557 '''Test dispense command''' 

1558 def exec_command(): 

1559 self.fb.dispense(100, "Nutrient Sprayer") 

1560 self.send_command_test_helper( 

1561 exec_command, 

1562 expected_command={ 

1563 'kind': 'lua', 

1564 'args': { 

1565 'lua': 'dispense(100, {tool_name = "Nutrient Sprayer"})', 

1566 }, 

1567 }, 

1568 extra_rpc_args={}, 

1569 mock_api_response={}) 

1570 

1571 @patch('requests.request') 

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

1573 '''Test helper for get_seed_tray_cell command''' 

1574 mock_request = args[0] 

1575 tray_data = kwargs['tray_data'] 

1576 cell = kwargs['cell'] 

1577 expected_xyz = kwargs['expected_xyz'] 

1578 self.fb.clear_cache() 

1579 mock_response = Mock() 

1580 mock_api_response = [ 

1581 { 

1582 'id': 123, 

1583 'name': 'Seed Tray', 

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

1585 }, 

1586 { 

1587 'pointer_type': 'ToolSlot', 

1588 'pullout_direction': 1, 

1589 'x': 0, 

1590 'y': 0, 

1591 'z': 0, 

1592 'tool_id': 123, 

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

1594 **tray_data, 

1595 }, 

1596 ] 

1597 mock_response.json.return_value = mock_api_response 

1598 mock_response.status_code = 200 

1599 mock_response.text = 'text' 

1600 mock_request.return_value = mock_response 

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

1602 mock_request.assert_has_calls([ 

1603 call( 

1604 method='GET', 

1605 url='https://my.farm.bot/api/tools', 

1606 **REQUEST_KWARGS, 

1607 ), 

1608 call().json(), 

1609 call( 

1610 method='GET', 

1611 url='https://my.farm.bot/api/points', 

1612 **REQUEST_KWARGS, 

1613 ), 

1614 call().json(), 

1615 ]) 

1616 self.assertEqual(cell, expected_xyz, kwargs) 

1617 

1618 def test_get_seed_tray_cell(self): 

1619 '''Test get_seed_tray_cell''' 

1620 test_cases = [ 

1621 { 

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

1623 'cell': 'a1', 

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

1625 }, 

1626 { 

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

1628 'cell': 'b2', 

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

1630 }, 

1631 { 

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

1633 'cell': 'd4', 

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

1635 }, 

1636 { 

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

1638 'cell': 'a1', 

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

1640 }, 

1641 { 

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

1643 'cell': 'b2', 

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

1645 }, 

1646 { 

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

1648 'cell': 'd4', 

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

1650 }, 

1651 { 

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

1653 'cell': 'd4', 

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

1655 }, 

1656 ] 

1657 for test_case in test_cases: 

1658 self.helper_get_seed_tray_cell(**test_case) 

1659 

1660 @patch('requests.request') 

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

1662 '''Test helper for get_seed_tray_cell command errors''' 

1663 mock_request = args[0] 

1664 tray_data = kwargs['tray_data'] 

1665 cell = kwargs['cell'] 

1666 error = kwargs['error'] 

1667 mock_response = Mock() 

1668 mock_api_response = [ 

1669 { 

1670 'id': 123, 

1671 'name': 'Seed Tray', 

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

1673 }, 

1674 { 

1675 'pointer_type': 'ToolSlot', 

1676 'pullout_direction': 1, 

1677 'x': 0, 

1678 'y': 0, 

1679 'z': 0, 

1680 'tool_id': 123, 

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

1682 **tray_data, 

1683 }, 

1684 ] 

1685 mock_response.json.return_value = mock_api_response 

1686 mock_response.status_code = 200 

1687 mock_response.text = 'text' 

1688 mock_request.return_value = mock_response 

1689 with self.assertRaises(ValueError) as cm: 

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

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

1692 mock_request.assert_has_calls([ 

1693 call( 

1694 method='GET', 

1695 url='https://my.farm.bot/api/tools', 

1696 **REQUEST_KWARGS, 

1697 ), 

1698 call().json(), 

1699 call( 

1700 method='GET', 

1701 url='https://my.farm.bot/api/points', 

1702 **REQUEST_KWARGS, 

1703 ), 

1704 call().json(), 

1705 ]) 

1706 

1707 def test_get_seed_tray_cell_invalid_cell_name(self): 

1708 '''Test get_seed_tray_cell: invalid cell name''' 

1709 self.helper_get_seed_tray_cell_error( 

1710 tray_data={}, 

1711 cell='e4', 

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

1713 ) 

1714 

1715 def test_get_seed_tray_cell_invalid_pullout_direction(self): 

1716 '''Test get_seed_tray_cell: invalid pullout direction''' 

1717 self.helper_get_seed_tray_cell_error( 

1718 tray_data={'pullout_direction': 0}, 

1719 cell='d4', 

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

1721 ) 

1722 

1723 @patch('requests.request') 

1724 def test_get_seed_tray_cell_no_tray(self, mock_request): 

1725 '''Test get_seed_tray_cell: no seed tray''' 

1726 mock_response = Mock() 

1727 mock_api_response = [] 

1728 mock_response.json.return_value = mock_api_response 

1729 mock_response.status_code = 200 

1730 mock_response.text = 'text' 

1731 mock_request.return_value = mock_response 

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

1733 mock_request.assert_has_calls([ 

1734 call( 

1735 method='GET', 

1736 url='https://my.farm.bot/api/tools', 

1737 **REQUEST_KWARGS, 

1738 ), 

1739 call().json(), 

1740 ]) 

1741 self.assertIsNone(result) 

1742 

1743 @patch('requests.request') 

1744 def test_get_seed_tray_cell_not_mounted(self, mock_request): 

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

1746 mock_response = Mock() 

1747 mock_api_response = [{ 

1748 'id': 123, 

1749 'name': 'Seed Tray', 

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

1751 }] 

1752 mock_response.json.return_value = mock_api_response 

1753 mock_response.status_code = 200 

1754 mock_response.text = 'text' 

1755 mock_request.return_value = mock_response 

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

1757 mock_request.assert_has_calls([ 

1758 call( 

1759 method='GET', 

1760 url='https://my.farm.bot/api/tools', 

1761 **REQUEST_KWARGS, 

1762 ), 

1763 call().json(), 

1764 ]) 

1765 self.assertIsNone(result) 

1766 

1767 def test_get_job_one(self): 

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

1769 def exec_command(): 

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

1771 'jobs': { 

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

1773 }, 

1774 }] 

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

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

1777 self.send_command_test_helper( 

1778 exec_command, 

1779 expected_command={ 

1780 'kind': 'read_status', 

1781 'args': {}, 

1782 }, 

1783 extra_rpc_args={}, 

1784 mock_api_response={}) 

1785 

1786 def test_get_job_all(self): 

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

1788 def exec_command(): 

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

1790 'jobs': { 

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

1792 }, 

1793 }] 

1794 jobs = self.fb.get_job() 

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

1796 self.send_command_test_helper( 

1797 exec_command, 

1798 expected_command={ 

1799 'kind': 'read_status', 

1800 'args': {}, 

1801 }, 

1802 extra_rpc_args={}, 

1803 mock_api_response={}) 

1804 

1805 def test_get_job_no_status(self): 

1806 '''Test get_job command: no status''' 

1807 def exec_command(): 

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

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

1810 self.assertIsNone(job) 

1811 self.send_command_test_helper( 

1812 exec_command, 

1813 expected_command={ 

1814 'kind': 'read_status', 

1815 'args': {}, 

1816 }, 

1817 extra_rpc_args={}, 

1818 mock_api_response={}) 

1819 

1820 def test_set_job(self): 

1821 '''Test set_job command''' 

1822 def exec_command(): 

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

1824 self.send_command_test_helper( 

1825 exec_command, 

1826 expected_command={ 

1827 'kind': 'lua', 

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

1829 set_job(job_name) 

1830 

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

1832 set_job(job_name, { 

1833 status = "working", 

1834 percent = 50 

1835 })'''}, 

1836 }, 

1837 extra_rpc_args={}, 

1838 mock_api_response={}) 

1839 

1840 def test_complete_job(self): 

1841 '''Test complete_job command''' 

1842 def exec_command(): 

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

1844 self.send_command_test_helper( 

1845 exec_command, 

1846 expected_command={ 

1847 'kind': 'lua', 

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

1849 }, 

1850 extra_rpc_args={}, 

1851 mock_api_response={}) 

1852 

1853 def test_lua(self): 

1854 '''Test lua command''' 

1855 def exec_command(): 

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

1857 self.send_command_test_helper( 

1858 exec_command, 

1859 expected_command={ 

1860 'kind': 'lua', 

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

1862 }, 

1863 extra_rpc_args={}, 

1864 mock_api_response={}) 

1865 

1866 def test_if_statement(self): 

1867 '''Test if_statement command''' 

1868 def exec_command(): 

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

1870 self.send_command_test_helper( 

1871 exec_command, 

1872 expected_command={ 

1873 'kind': '_if', 

1874 'args': { 

1875 'lhs': 'pin10', 

1876 'op': 'is', 

1877 'rhs': 0, 

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

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

1880 } 

1881 }, 

1882 extra_rpc_args={}, 

1883 mock_api_response=[]) 

1884 

1885 def test_if_statement_with_named_pin(self): 

1886 '''Test if_statement command with named pin''' 

1887 def exec_command(): 

1888 self.fb.if_statement( 

1889 'Lights', 'is', 0, 

1890 named_pin_type='Peripheral') 

1891 self.send_command_test_helper( 

1892 exec_command, 

1893 expected_command={ 

1894 'kind': '_if', 

1895 'args': { 

1896 'lhs': { 

1897 'kind': 'named_pin', 

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

1899 }, 

1900 'op': 'is', 

1901 'rhs': 0, 

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

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

1904 } 

1905 }, 

1906 extra_rpc_args={}, 

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

1908 

1909 def test_if_statement_with_named_pin_not_found(self): 

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

1911 def exec_command(): 

1912 self.fb.if_statement( 

1913 'Lights', 'is', 0, 

1914 named_pin_type='Peripheral') 

1915 self.send_command_test_helper( 

1916 exec_command, 

1917 expected_command=None, 

1918 extra_rpc_args={}, 

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

1920 self.assertEqual( 

1921 self.fb.state.error, 

1922 "ERROR: 'Lights' not in peripherals: ['Pump'].") 

1923 

1924 def test_if_statement_with_sequences(self): 

1925 '''Test if_statement command with sequences''' 

1926 def exec_command(): 

1927 self.fb.if_statement( 

1928 'pin10', '<', 0, 

1929 'Watering Sequence', 

1930 'Drying Sequence') 

1931 self.send_command_test_helper( 

1932 exec_command, 

1933 expected_command={ 

1934 'kind': '_if', 

1935 'args': { 

1936 'lhs': 'pin10', 

1937 'op': '<', 

1938 'rhs': 0, 

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

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

1941 } 

1942 }, 

1943 extra_rpc_args={}, 

1944 mock_api_response=[ 

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

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

1947 ]) 

1948 

1949 def test_if_statement_with_sequence_not_found(self): 

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

1951 def exec_command(): 

1952 self.fb.if_statement( 

1953 'pin10', '<', 0, 

1954 'Watering Sequence', 

1955 'Drying Sequence') 

1956 self.send_command_test_helper( 

1957 exec_command, 

1958 expected_command=None, 

1959 extra_rpc_args={}, 

1960 mock_api_response=[]) 

1961 self.assertEqual( 

1962 self.fb.state.error, 

1963 "ERROR: 'Watering Sequence' not in sequences: [].") 

1964 

1965 def test_if_statement_invalid_operator(self): 

1966 '''Test if_statement command: invalid operator''' 

1967 def exec_command(): 

1968 with self.assertRaises(ValueError) as cm: 

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

1970 self.assertEqual( 

1971 cm.exception.args[0], 

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

1973 self.send_command_test_helper( 

1974 exec_command, 

1975 expected_command=None, 

1976 extra_rpc_args={}, 

1977 mock_api_response=[]) 

1978 

1979 def test_if_statement_invalid_variable(self): 

1980 '''Test if_statement command: invalid variable''' 

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

1982 

1983 def exec_command(): 

1984 with self.assertRaises(ValueError) as cm: 

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

1986 self.assertEqual( 

1987 cm.exception.args[0], 

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

1989 self.send_command_test_helper( 

1990 exec_command, 

1991 expected_command=None, 

1992 extra_rpc_args={}, 

1993 mock_api_response=[]) 

1994 

1995 def test_if_statement_invalid_named_pin_type(self): 

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

1997 def exec_command(): 

1998 with self.assertRaises(ValueError) as cm: 

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

2000 self.assertEqual( 

2001 cm.exception.args[0], 

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

2003 self.send_command_test_helper( 

2004 exec_command, 

2005 expected_command=None, 

2006 extra_rpc_args={}, 

2007 mock_api_response=[]) 

2008 

2009 def test_rpc_error(self): 

2010 '''Test rpc error handling''' 

2011 def exec_command(): 

2012 self.fb.wait(100) 

2013 self.assertEqual( 

2014 self.fb.state.error, 

2015 'RPC error response received.') 

2016 self.send_command_test_helper( 

2017 exec_command, 

2018 error=True, 

2019 expected_command={ 

2020 'kind': 'wait', 

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

2022 extra_rpc_args={}, 

2023 mock_api_response=[]) 

2024 

2025 def test_rpc_response_timeout(self): 

2026 '''Test rpc response timeout handling''' 

2027 def exec_command(): 

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

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

2030 ] 

2031 self.fb.wait(100) 

2032 self.assertEqual( 

2033 self.fb.state.error, 

2034 'Timed out waiting for RPC response.') 

2035 self.send_command_test_helper( 

2036 exec_command, 

2037 expected_command={ 

2038 'kind': 'wait', 

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

2040 extra_rpc_args={}, 

2041 mock_api_response=[]) 

2042 

2043 def test_set_verbosity(self): 

2044 '''Test set_verbosity.''' 

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

2046 self.fb.set_verbosity(1) 

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

2048 

2049 def test_set_timeout(self): 

2050 '''Test set_timeout.''' 

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

2052 self.fb.set_timeout(15) 

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

2054 

2055 @staticmethod 

2056 def helper_get_print_strings(mock_print): 

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

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

2059 

2060 @patch('builtins.print') 

2061 def test_print_status(self, mock_print): 

2062 '''Test print_status.''' 

2063 self.fb.set_verbosity(0) 

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

2065 mock_print.assert_not_called() 

2066 self.fb.set_verbosity(1) 

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

2068 call_strings = self.helper_get_print_strings(mock_print) 

2069 self.assertIn('testing', call_strings) 

2070 mock_print.reset_mock() 

2071 self.fb.set_verbosity(2) 

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

2073 call_strings = self.helper_get_print_strings(mock_print) 

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

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

2076 self.assertIn('test_print_status', call_strings)