test_circup.py
1 # SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 """ 5 Unit tests for the circup module. 6 7 Copyright (c) 2019 Adafruit Industries 8 9 Permission is hereby granted, free of charge, to any person obtaining a copy 10 of this software and associated documentation files (the "Software"), to deal 11 in the Software without restriction, including without limitation the rights 12 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 copies of the Software, and to permit persons to whom the Software is 14 furnished to do so, subject to the following conditions: 15 16 The above copyright notice and this permission notice shall be included in all 17 copies or substantial portions of the Software. 18 19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 SOFTWARE. 26 """ 27 import os 28 import ctypes 29 import json 30 from unittest import mock 31 32 33 from click.testing import CliRunner 34 import pytest 35 import requests 36 37 import circup 38 39 40 def test_Module_init_file_module(): 41 """ 42 Ensure the Module instance is set up as expected and logged, is if for a 43 single file Python module. 44 """ 45 path = os.path.join("foo", "bar", "baz", "module.py") 46 repo = "https://github.com/adafruit/SomeLibrary.git" 47 device_version = "1.2.3" 48 bundle_version = "3.2.1" 49 with mock.patch("circup.logger.info") as mock_logger, mock.patch( 50 "circup.os.path.isfile", return_value=True 51 ), mock.patch("circup.CPY_VERSION", "4.1.2"), mock.patch( 52 "circup.os.walk", return_value=[["lib", "", ""]] 53 ) as mock_walk: 54 m = circup.Module(path, repo, device_version, bundle_version, False) 55 mock_logger.assert_called_once_with(m) 56 assert m.path == path 57 assert m.file == "module.py" 58 assert m.name == "module" 59 assert m.repo == repo 60 assert m.device_version == device_version 61 assert m.bundle_version == bundle_version 62 assert m.bundle_path == os.path.join("lib", m.file) 63 assert m.mpy is False 64 mock_walk.assert_called_once_with(circup.BUNDLE_DIR.format("py")) 65 66 67 def test_Module_init_directory_module(): 68 """ 69 Ensure the Module instance is set up as expected and logged, as if for a 70 directory based Python module. 71 """ 72 path = os.path.join("foo", "bar", "modulename", "") 73 repo = "https://github.com/adafruit/SomeLibrary.git" 74 device_version = "1.2.3" 75 bundle_version = "3.2.1" 76 mpy = True 77 with mock.patch("circup.logger.info") as mock_logger, mock.patch( 78 "circup.os.path.isfile", return_value=False 79 ), mock.patch("circup.CPY_VERSION", "4.1.2"), mock.patch( 80 "circup.os.walk", return_value=[["lib", "", ""]] 81 ) as mock_walk: 82 m = circup.Module(path, repo, device_version, bundle_version, mpy) 83 mock_logger.assert_called_once_with(m) 84 assert m.path == path 85 assert m.file is None 86 assert m.name == "modulename" 87 assert m.repo == repo 88 assert m.device_version == device_version 89 assert m.bundle_version == bundle_version 90 assert m.bundle_path == os.path.join("lib", m.name) 91 assert m.mpy is True 92 mock_walk.assert_called_once_with(circup.BUNDLE_DIR.format("4mpy")) 93 94 95 def test_Module_outofdate(): 96 """ 97 Ensure the ``outofdate`` property on a Module instance returns the expected 98 boolean value to correctly indicate if the referenced module is, in fact, 99 out of date. 100 """ 101 path = os.path.join("foo", "bar", "baz", "module.py") 102 repo = "https://github.com/adafruit/SomeLibrary.git" 103 bundle_path = os.path.join("baz", "bar", "foo", "module.py") 104 m1 = circup.Module(path, repo, "1.2.3", "3.2.1", bundle_path) 105 m2 = circup.Module(path, repo, "1.2.3", "1.2.3", bundle_path) 106 # shouldn't happen! 107 m3 = circup.Module(path, repo, "3.2.1", "1.2.3", bundle_path) 108 assert m1.outofdate is True 109 assert m2.outofdate is False 110 assert m3.outofdate is False 111 112 113 def test_Module_outofdate_bad_versions(): 114 """ 115 Sometimes, the version is not a valid semver value. In this case, the 116 ``outofdate`` property assumes the module should be updated (to correct 117 this problem). Such a problem should be logged. 118 """ 119 path = os.path.join("foo", "bar", "baz", "module.py") 120 repo = "https://github.com/adafruit/SomeLibrary.git" 121 device_version = "hello" 122 bundle_version = "3.2.1" 123 bundle_path = os.path.join("baz", "bar", "foo", "module.py") 124 m = circup.Module(path, repo, device_version, bundle_version, bundle_path) 125 with mock.patch("circup.logger.warning") as mock_logger: 126 assert m.outofdate is True 127 assert mock_logger.call_count == 2 128 129 130 def test_Module_major_update_bad_versions(): 131 """ 132 Sometimes, the version is not a valid semver value. In this case, the 133 ``major_update`` property assumes the module is a major update, so as not 134 to block the user from getting the latest update. 135 Such a problem should be logged. 136 """ 137 path = os.path.join("foo", "bar", "baz", "module.py") 138 repo = "https://github.com/adafruit/SomeLibrary.git" 139 device_version = "1.2.3" 140 bundle_version = "version-3" 141 bundle_path = os.path.join("baz", "bar", "foo", "module.py") 142 m = circup.Module(path, repo, device_version, bundle_version, bundle_path) 143 with mock.patch("circup.logger.warning") as mock_logger: 144 assert m.major_update is True 145 assert mock_logger.call_count == 2 146 147 148 def test_Module_row(): 149 """ 150 Ensure the tuple contains the expected items to be correctly displayed in 151 a table of version-related results. 152 """ 153 path = os.path.join("foo", "bar", "baz", "module.py") 154 repo = "https://github.com/adafruit/SomeLibrary.git" 155 device_version = "1.2.3" 156 bundle_version = None 157 bundle_path = os.path.join("baz", "bar", "foo", "module.py") 158 with mock.patch("circup.os.path.isfile", return_value=True): 159 m = circup.Module(path, repo, device_version, bundle_version, bundle_path) 160 # print(m.__dict__) 161 # print(m.row) 162 assert m.row == ("module", "1.2.3", "unknown", "True") 163 164 165 def test_Module_update_dir(): 166 """ 167 Ensure if the module is a directory, the expected actions take place to 168 update the module on the connected device. 169 """ 170 path = os.path.join("foo", "bar", "baz", "module.py") 171 repo = "https://github.com/adafruit/SomeLibrary.git" 172 device_version = "1.2.3" 173 bundle_version = None 174 bundle_path = os.path.join("baz", "bar", "foo", "module.py") 175 m = circup.Module(path, repo, device_version, bundle_version, bundle_path) 176 with mock.patch("circup.shutil") as mock_shutil, mock.patch( 177 "circup.os.path.isdir", return_value=True 178 ): 179 m.update() 180 mock_shutil.rmtree.assert_called_once_with(m.path, ignore_errors=True) 181 mock_shutil.copytree.assert_called_once_with(m.bundle_path, m.path) 182 183 184 def test_Module_update_file(): 185 """ 186 Ensure if the module is a file, the expected actions take place to 187 update the module on the connected device. 188 """ 189 path = os.path.join("foo", "bar", "baz", "module.py") 190 repo = "https://github.com/adafruit/SomeLibrary.git" 191 device_version = "1.2.3" 192 bundle_version = None 193 bundle_path = os.path.join("baz", "bar", "foo", "module.py") 194 m = circup.Module(path, repo, device_version, bundle_version, bundle_path) 195 with mock.patch("circup.shutil") as mock_shutil, mock.patch( 196 "circup.os.remove" 197 ) as mock_remove, mock.patch("circup.os.path.isdir", return_value=False): 198 m.update() 199 mock_remove.assert_called_once_with(m.path) 200 mock_shutil.copyfile.assert_called_once_with(m.bundle_path, m.path) 201 202 203 def test_Module_repr(): 204 """ 205 Ensure the repr(dict) is returned (helps when logging). 206 """ 207 path = os.path.join("foo", "bar", "baz", "module.py") 208 repo = "https://github.com/adafruit/SomeLibrary.git" 209 device_version = "1.2.3" 210 bundle_version = "3.2.1" 211 with mock.patch("circup.os.path.isfile", return_value=True), mock.patch( 212 "circup.CPY_VERSION", "4.1.2" 213 ), mock.patch("circup.os.walk", return_value=[["lib", "", ""]]): 214 m = circup.Module(path, repo, device_version, bundle_version, False) 215 assert repr(m) == repr( 216 { 217 "path": path, 218 "file": "module.py", 219 "name": "module", 220 "repo": repo, 221 "device_version": device_version, 222 "bundle_version": bundle_version, 223 "bundle_path": os.path.join("lib", m.file), 224 "mpy": False, 225 } 226 ) 227 228 229 def test_find_device_posix_exists(): 230 """ 231 Simulate being on os.name == 'posix' and a call to "mount" returns a 232 record indicating a connected device. 233 """ 234 with open("tests/mount_exists.txt", "rb") as fixture_file: 235 fixture = fixture_file.read() 236 with mock.patch("os.name", "posix"): 237 with mock.patch("circup.check_output", return_value=fixture): 238 assert circup.find_device() == "/media/ntoll/CIRCUITPY" 239 240 241 def test_find_device_posix_no_mount_command(): 242 """ 243 When the user doesn't have administrative privileges on OSX then the mount 244 command isn't on their path. In which case, check circup uses the more 245 explicit /sbin/mount instead. 246 """ 247 with open("tests/mount_exists.txt", "rb") as fixture_file: 248 fixture = fixture_file.read() 249 mock_check = mock.MagicMock(side_effect=[FileNotFoundError, fixture]) 250 with mock.patch("os.name", "posix"), mock.patch("circup.check_output", mock_check): 251 assert circup.find_device() == "/media/ntoll/CIRCUITPY" 252 assert mock_check.call_count == 2 253 assert mock_check.call_args_list[0][0][0] == "mount" 254 assert mock_check.call_args_list[1][0][0] == "/sbin/mount" 255 256 257 def test_find_device_posix_missing(): 258 """ 259 Simulate being on os.name == 'posix' and a call to "mount" returns no 260 records associated with an Adafruit device. 261 """ 262 with open("tests/mount_missing.txt", "rb") as fixture_file: 263 fixture = fixture_file.read() 264 with mock.patch("os.name", "posix"), mock.patch( 265 "circup.check_output", return_value=fixture 266 ): 267 assert circup.find_device() is None 268 269 270 def test_find_device_nt_exists(): 271 """ 272 Simulate being on os.name == 'nt' and a disk with a volume name 'CIRCUITPY' 273 exists indicating a connected device. 274 """ 275 mock_windll = mock.MagicMock() 276 mock_windll.kernel32 = mock.MagicMock() 277 mock_windll.kernel32.GetVolumeInformationW = mock.MagicMock() 278 mock_windll.kernel32.GetVolumeInformationW.return_value = None 279 fake_buffer = ctypes.create_unicode_buffer("CIRCUITPY") 280 with mock.patch("os.name", "nt"), mock.patch( 281 "os.path.exists", return_value=True 282 ), mock.patch("ctypes.create_unicode_buffer", return_value=fake_buffer): 283 ctypes.windll = mock_windll 284 assert circup.find_device() == "A:\\" 285 286 287 def test_find_device_nt_missing(): 288 """ 289 Simulate being on os.name == 'nt' and a disk with a volume name 'CIRCUITPY' 290 does not exist for a device. 291 """ 292 mock_windll = mock.MagicMock() 293 mock_windll.kernel32 = mock.MagicMock() 294 mock_windll.kernel32.GetVolumeInformationW = mock.MagicMock() 295 mock_windll.kernel32.GetVolumeInformationW.return_value = None 296 fake_buffer = ctypes.create_unicode_buffer(1024) 297 with mock.patch("os.name", "nt"), mock.patch( 298 "os.path.exists", return_value=True 299 ), mock.patch("ctypes.create_unicode_buffer", return_value=fake_buffer): 300 ctypes.windll = mock_windll 301 assert circup.find_device() is None 302 303 304 def test_find_device_unknown_os(): 305 """ 306 Raises a NotImplementedError if the host OS is not supported. 307 """ 308 with mock.patch("os.name", "foo"): 309 with pytest.raises(NotImplementedError) as ex: 310 circup.find_device() 311 assert ex.value.args[0] == 'OS "foo" not supported.' 312 313 314 def test_get_latest_release_from_url(): 315 """ 316 Ensure the expected tag value is extracted from the returned URL (resulting 317 from a call to the expected endpoint). 318 """ 319 response = mock.MagicMock() 320 response.headers = { 321 "Location": "https://github.com/adafruit" 322 "/Adafruit_CircuitPython_Bundle/releases/tag/20190903" 323 } 324 expected_url = ( 325 "https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/latest" 326 ) 327 with mock.patch("circup.requests.head", return_value=response) as mock_get: 328 result = circup.get_latest_release_from_url(expected_url) 329 assert result == "20190903" 330 mock_get.assert_called_once_with(expected_url) 331 332 333 def test_extract_metadata_python(): 334 """ 335 Ensure the dunder objects assigned in code are extracted into a Python 336 dictionary representing such metadata. 337 """ 338 code = ( 339 "# A comment\n" 340 '__version__ = "1.1.4"\n' 341 '__repo__ = "https://github.com/adafruit/SomeLibrary.git"\n' 342 'print("Hello, world!")\n' 343 ) 344 path = "foo.py" 345 with mock.patch("builtins.open", mock.mock_open(read_data=code)) as mock_open: 346 result = circup.extract_metadata(path) 347 mock_open.assert_called_once_with(path, encoding="utf-8") 348 assert len(result) == 3 349 assert result["__version__"] == "1.1.4" 350 assert result["__repo__"] == "https://github.com/adafruit/SomeLibrary.git" 351 assert result["mpy"] is False 352 353 354 def test_extract_metadata_byte_code(): 355 """ 356 Ensure the __version__ is correctly extracted from the bytecode ".mpy" 357 file. Version in test_module is 0.9.2 358 """ 359 result = circup.extract_metadata("tests/test_module.mpy") 360 assert result["__version__"] == "0.9.2" 361 assert result["mpy"] is True 362 363 364 def test_find_modules(): 365 """ 366 Ensure that the expected list of Module instances is returned given the 367 metadata dictionary fixtures for device and bundle modules. 368 """ 369 with open("tests/device.json") as f: 370 device_modules = json.load(f) 371 with open("tests/bundle.json") as f: 372 bundle_modules = json.load(f) 373 with mock.patch( 374 "circup.get_device_versions", return_value=device_modules 375 ), mock.patch( 376 "circup.get_bundle_versions", return_value=bundle_modules 377 ), mock.patch( 378 "circup.os.path.isfile", return_value=True 379 ): 380 result = circup.find_modules("") 381 assert len(result) == 1 382 assert result[0].name == "adafruit_74hc595" 383 assert ( 384 result[0].repo 385 == "https://github.com/adafruit/Adafruit_CircuitPython_74HC595.git" 386 ) 387 388 389 def test_find_modules_goes_bang(): 390 """ 391 Ensure if there's a problem getting metadata an error message is displayed 392 and the utility exists with an error code of 1. 393 """ 394 with mock.patch( 395 "circup.get_device_versions", side_effect=Exception("BANG!") 396 ), mock.patch("circup.click") as mock_click, mock.patch( 397 "circup.sys.exit" 398 ) as mock_exit: 399 circup.find_modules("") 400 assert mock_click.echo.call_count == 1 401 mock_exit.assert_called_once_with(1) 402 403 404 def test_get_bundle_versions(): 405 """ 406 Ensure get_modules is called with the path for the library bundle. 407 """ 408 dirs = (("foo/bar/lib", "", ""),) 409 with mock.patch("circup.ensure_latest_bundle"), mock.patch( 410 "circup.os.walk", return_value=dirs 411 ) as mock_walk, mock.patch("circup.get_modules", return_value="ok") as mock_gm: 412 assert circup.get_bundle_versions() == "ok" 413 mock_walk.assert_called_once_with(circup.BUNDLE_DIR.format("py")) 414 mock_gm.assert_called_once_with("foo/bar/lib") 415 416 417 def test_get_circuitpython_version(): 418 """ 419 Given valid content of a boot_out.txt file on a connected device, return 420 the version number of CircuitPython running on the board. 421 """ 422 data = ( 423 "Adafruit CircuitPython 4.1.0 on 2019-08-02; " 424 "Adafruit CircuitPlayground Express with samd21g18" 425 ) 426 mock_open = mock.mock_open(read_data=data) 427 device_path = "device" 428 with mock.patch("builtins.open", mock_open): 429 assert circup.get_circuitpython_version(device_path) == "4.1.0" 430 mock_open.assert_called_once_with(os.path.join(device_path, "boot_out.txt")) 431 432 433 def test_get_device_versions(): 434 """ 435 Ensure get_modules is called with the path for the attached device. 436 """ 437 with mock.patch("circup.get_modules", return_value="ok") as mock_gm: 438 assert circup.get_device_versions("TESTDIR") == "ok" 439 mock_gm.assert_called_once_with(os.path.join("TESTDIR", "lib")) 440 441 442 def test_get_modules_empty_path(): 443 """ 444 Sometimes a path to a device or bundle may be empty. Ensure, if this is the 445 case, an empty dictionary is returned. 446 """ 447 assert circup.get_modules("") == {} 448 449 450 def test_get_modules_that_are_files(): 451 """ 452 Check the expected dictionary containing metadata is returned given the 453 (mocked) results of glob and open on file based Python modules. 454 """ 455 path = "tests" # mocked away in function. 456 mods = [ 457 os.path.join("tests", "local_module.py"), 458 os.path.join("tests", ".hidden_module.py"), 459 ] 460 with mock.patch("circup.glob.glob", side_effect=[mods, [], []]): 461 result = circup.get_modules(path) 462 assert len(result) == 1 # Hidden files are ignored. 463 assert "local_module" in result 464 assert result["local_module"]["path"] == os.path.join( 465 "tests", "local_module.py" 466 ) 467 assert result["local_module"]["__version__"] == "1.2.3" # from fixture. 468 repo = "https://github.com/adafruit/SomeLibrary.git" # from fixture. 469 assert result["local_module"]["__repo__"] == repo 470 471 472 def test_get_modules_that_are_directories(): 473 """ 474 Check the expected dictionary containing metadata is returned given the 475 (mocked) results of glob and open, on directory based Python modules. 476 """ 477 path = "tests" # mocked away in function. 478 mods = [ 479 os.path.join("tests", "dir_module", ""), 480 os.path.join("tests", ".hidden_dir", ""), 481 ] 482 mod_files = ["tests/dir_module/my_module.py", "tests/dir_module/__init__.py"] 483 with mock.patch("circup.glob.glob", side_effect=[[], [], mods, mod_files, []]): 484 result = circup.get_modules(path) 485 assert len(result) == 1 486 assert "dir_module" in result 487 assert result["dir_module"]["path"] == os.path.join("tests", "dir_module", "") 488 assert result["dir_module"]["__version__"] == "3.2.1" # from fixture. 489 repo = "https://github.com/adafruit/SomeModule.git" # from fixture. 490 assert result["dir_module"]["__repo__"] == repo 491 492 493 def test_get_modules_that_are_directories_with_no_metadata(): 494 """ 495 Check the expected dictionary containing just the path is returned given 496 the (mocked) results of glob and open, on directory based Python modules. 497 """ 498 path = "tests" # mocked away in function. 499 mods = [os.path.join("tests", "bad_module", "")] 500 mod_files = ["tests/bad_module/my_module.py", "tests/bad_module/__init__.py"] 501 with mock.patch("circup.glob.glob", side_effect=[[], [], mods, mod_files, []]): 502 result = circup.get_modules(path) 503 assert len(result) == 1 504 assert "bad_module" in result 505 assert result["bad_module"]["path"] == os.path.join("tests", "bad_module", "") 506 assert "__version__" not in result["bad_module"] 507 assert "__repo__" not in result["bad_module"] 508 509 510 def test_ensure_latest_bundle_no_bundle_data(): 511 """ 512 If there's no BUNDLE_DATA file (containing previous current version of the 513 bundle) then default to update. 514 """ 515 with mock.patch("circup.get_latest_tag", return_value="12345"), mock.patch( 516 "circup.os.path.isfile", return_value=False 517 ), mock.patch("circup.get_bundle") as mock_gb, mock.patch( 518 "circup.json" 519 ) as mock_json: 520 circup.ensure_latest_bundle() 521 mock_gb.assert_called_once_with("12345") 522 assert mock_json.dump.call_count == 1 # Current version saved to file. 523 524 525 def test_ensure_latest_bundle_bad_bundle_data(): 526 """ 527 If there's a BUNDLE_DATA file (containing previous current version of the 528 bundle) but it has been corrupted (which has sometimes happened during 529 manual testing) then default to update. 530 """ 531 with mock.patch("circup.get_latest_tag", return_value="12345"), mock.patch( 532 "circup.os.path.isfile", return_value=True 533 ), mock.patch("circup.open"), mock.patch( 534 "circup.get_bundle" 535 ) as mock_gb, mock.patch( 536 "circup.json.load", side_effect=json.decoder.JSONDecodeError("BANG!", "doc", 1) 537 ), mock.patch( 538 "circup.json.dump" 539 ), mock.patch( 540 "circup.logger" 541 ) as mock_logger: 542 circup.ensure_latest_bundle() 543 mock_gb.assert_called_once_with("12345") 544 assert mock_logger.error.call_count == 1 545 assert mock_logger.exception.call_count == 1 546 547 548 def test_ensure_latest_bundle_to_update(): 549 """ 550 If the version found in the BUNDLE_DATA is out of date, then cause an 551 update to the bundle. 552 """ 553 with mock.patch("circup.get_latest_tag", return_value="54321"), mock.patch( 554 "circup.os.path.isfile", return_value=True 555 ), mock.patch("circup.open"), mock.patch( 556 "circup.get_bundle" 557 ) as mock_gb, mock.patch( 558 "circup.json" 559 ) as mock_json: 560 mock_json.load.return_value = {"tag": "12345"} 561 circup.ensure_latest_bundle() 562 mock_gb.assert_called_once_with("54321") 563 assert mock_json.dump.call_count == 1 # Current version saved to file. 564 565 566 def test_ensure_latest_bundle_to_update_http_error(): 567 # pylint: disable=pointless-statement 568 # 2nd to last line is deemed pointless. 569 # Don't understand but for now this is an accpetable workaround 570 # Tracking issue will be opened. 571 """ 572 If an HTTP error happens during a bundle update, print a friendly 573 error message and exit 1. 574 """ 575 with mock.patch("circup.get_latest_tag", return_value="54321"), mock.patch( 576 "circup.os.path.isfile", return_value=True 577 ), mock.patch("circup.open"), mock.patch( 578 "circup.get_bundle", side_effect=requests.exceptions.HTTPError("404") 579 ) as mock_gb, mock.patch( 580 "circup.json" 581 ) as mock_json, mock.patch( 582 "circup.click.secho" 583 ) as mock_click, mock.patch( 584 "circup.sys.exit" 585 ) as mock_exit: 586 mock_json.load.return_value = {"tag": "12345"} 587 circup.ensure_latest_bundle() 588 mock_gb.assert_called_once_with("54321") 589 assert mock_json.dump.call_count == 0 # not saved. 590 mock_click.call_count == 1 # friendly message. 591 mock_exit.assert_called_once_with(1) # exit 1. 592 593 594 def test_ensure_latest_bundle_no_update(): 595 """ 596 If the version found in the BUNDLE_DATA is NOT out of date, just log the 597 fact and don't update. 598 """ 599 with mock.patch("circup.get_latest_tag", return_value="12345"), mock.patch( 600 "circup.os.path.isfile", return_value=True 601 ), mock.patch("circup.open"), mock.patch( 602 "circup.get_bundle" 603 ) as mock_gb, mock.patch( 604 "circup.json" 605 ) as mock_json, mock.patch( 606 "circup.logger" 607 ) as mock_logger: 608 mock_json.load.return_value = {"tag": "12345"} 609 circup.ensure_latest_bundle() 610 assert mock_gb.call_count == 0 611 assert mock_logger.info.call_count == 2 612 613 614 def test_get_bundle(): 615 """ 616 Ensure the expected calls are made to get the referenced bundle and the 617 result is unzipped to the expected location. 618 """ 619 # All these mocks stop IO side effects and allow us to spy on the code to 620 # ensure the expected calls are made with the correct values. Warning! Here 621 # Be Dragons! (If in doubt, ask ntoll for details). 622 mock_progress = mock.MagicMock() 623 mock_progress().__enter__ = mock.MagicMock(return_value=["a", "b", "c"]) 624 mock_progress().__exit__ = mock.MagicMock() 625 with mock.patch("circup.requests") as mock_requests, mock.patch( 626 "circup.click" 627 ) as mock_click, mock.patch( 628 "circup.open", mock.mock_open() 629 ) as mock_open, mock.patch( 630 "circup.os.path.isdir", return_value=True 631 ), mock.patch( 632 "circup.shutil" 633 ) as mock_shutil, mock.patch( 634 "circup.zipfile" 635 ) as mock_zipfile: 636 mock_click.progressbar = mock_progress 637 mock_requests.get().status_code = mock_requests.codes.ok 638 mock_requests.get.reset_mock() 639 tag = "12345" 640 circup.get_bundle(tag) 641 assert mock_requests.get.call_count == 3 642 assert mock_open.call_count == 3 643 assert mock_shutil.rmtree.call_count == 3 644 assert mock_zipfile.ZipFile.call_count == 3 645 assert mock_zipfile.ZipFile().__enter__().extractall.call_count == 3 646 647 648 def test_get_bundle_network_error(): 649 """ 650 Ensure that if there is a network related error when grabbing the bundle 651 then the error is logged and re-raised for the HTTP status code. 652 """ 653 with mock.patch("circup.requests") as mock_requests, mock.patch( 654 "circup.logger" 655 ) as mock_logger: 656 # Force failure with != requests.codes.ok 657 mock_requests.get().status_code = mock_requests.codes.BANG 658 # Ensure raise_for_status actually raises an exception. 659 mock_requests.get().raise_for_status.return_value = Exception("Bang!") 660 mock_requests.get.reset_mock() 661 tag = "12345" 662 with pytest.raises(Exception) as ex: 663 circup.get_bundle(tag) 664 assert ex.value.args[0] == "Bang!" 665 url = ( 666 "https://github.com/adafruit/Adafruit_CircuitPython_Bundle" 667 "/releases/download" 668 "/{tag}/adafruit-circuitpython-bundle-py-{tag}.zip".format(tag=tag) 669 ) 670 mock_requests.get.assert_called_once_with(url, stream=True) 671 assert mock_logger.warning.call_count == 1 672 mock_requests.get().raise_for_status.assert_called_once_with() 673 674 675 def test_show_command(): 676 """ 677 test_show_command 678 """ 679 runner = CliRunner() 680 TEST_BUNDLE_MODULES = ["one.py", "two.py", "three.py"] 681 with mock.patch("circup.get_bundle_versions", return_value=TEST_BUNDLE_MODULES): 682 result = runner.invoke(circup.show) 683 assert result.exit_code == 0 684 assert all([m.replace(".py", "") in result.output for m in TEST_BUNDLE_MODULES]) 685 686 687 def test_show_match_command(): 688 """ 689 test_show_match_command 690 """ 691 runner = CliRunner() 692 TEST_BUNDLE_MODULES = ["one.py", "two.py", "three.py"] 693 with mock.patch("circup.get_bundle_versions", return_value=TEST_BUNDLE_MODULES): 694 result = runner.invoke(circup.show, ["t"]) 695 assert result.exit_code == 0 696 assert "one" not in result.output 697 698 699 def test_show_match_py_command(): 700 """ 701 Check that py does not match the .py extention in the module names 702 """ 703 runner = CliRunner() 704 TEST_BUNDLE_MODULES = ["one.py", "two.py", "three.py"] 705 with mock.patch("circup.get_bundle_versions", return_value=TEST_BUNDLE_MODULES): 706 result = runner.invoke(circup.show, ["py"]) 707 assert result.exit_code == 0 708 assert "0 shown" in result.output