From c88943fa98e9211401ca93999e067af2d9be0455 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Wed, 3 May 2023 10:39:36 -0400 Subject: [PATCH 01/14] Add conditional coverage flags for accurate platform coverage --- .github/workflows/ci.yml | 167 +++++++++++++----- changes/1260.misc.rst | 1 + pyproject.toml | 32 ++++ setup.cfg | 5 +- src/briefcase/__init__.py | 2 +- src/briefcase/commands/base.py | 12 +- src/briefcase/commands/open.py | 6 +- src/briefcase/config.py | 2 +- src/briefcase/integrations/docker.py | 13 +- src/briefcase/integrations/linuxdeploy.py | 2 +- src/briefcase/integrations/windows_sdk.py | 2 +- src/briefcase/platforms/__init__.py | 2 +- src/briefcase/platforms/linux/__init__.py | 4 +- src/briefcase/platforms/linux/appimage.py | 2 +- src/briefcase/platforms/linux/system.py | 10 +- src/briefcase/platforms/macOS/__init__.py | 8 +- src/briefcase/platforms/web/static.py | 2 +- tests/commands/dev/test_get_environment.py | 17 ++ .../android_sdk/AndroidSDK/test_verify.py | 54 ++++-- tests/platforms/iOS/xcode/test_mixin.py | 39 ++++ tests/platforms/linux/appimage/test_mixin.py | 11 ++ .../linux/test_LocalRequirementsMixin.py | 3 +- tests/platforms/macOS/app/test_package.py | 25 ++- tests/platforms/macOS/xcode/test_package.py | 57 +++++- .../windows/visualstudio/test_build.py | 16 +- tox.ini | 43 ++++- 26 files changed, 419 insertions(+), 118 deletions(-) create mode 100644 changes/1260.misc.rst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc8efa524..418d38185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,17 +29,17 @@ jobs: uses: beeware/.github/.github/workflows/towncrier-run.yml@main package: - name: Python Package + name: Python package uses: beeware/.github/.github/workflows/python-package-create.yml@main unit-tests: name: Unit tests - needs: [pre-commit, towncrier, package] + needs: [ pre-commit, towncrier, package ] runs-on: ${{ matrix.platform }}-latest continue-on-error: ${{ matrix.experimental }} strategy: matrix: - platform: [ "macos", "ubuntu", "windows" ] + platform: [ "macOS", "Ubuntu", "Windows" ] python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12-dev" ] include: - experimental: false @@ -47,7 +47,7 @@ jobs: - python-version: "3.12-dev" experimental: true # Run tests against the latest Windows Store Python - - platform: "windows" + - platform: "Windows" python-version: "winstore" experimental: false steps: @@ -57,7 +57,7 @@ jobs: fetch-depth: 0 - name: Set up Python - if: matrix.python-version != 'winstore' + if: startswith(matrix.python-version, '3') uses: actions/setup-python@v4.6.0 with: python-version: ${{ matrix.python-version }} @@ -65,44 +65,121 @@ jobs: - name: Install Windows Store Python if: matrix.python-version == 'winstore' uses: beeware/.github/.github/actions/install-win-store-python@main - with: - python-version: "3.11" - - name: Get packages + - name: Get Packages uses: actions/download-artifact@v3.0.2 with: name: ${{ needs.package.outputs.artifact-name }} path: dist - - name: Install dev dependencies + - name: Install dev Dependencies run: | - # pip 23.1 has an issue with --user installs. - # See https://github.com/pypa/pip/issues/11982 for details - python -m pip install --upgrade "pip!=23.1" + python -m pip install --upgrade pip python -m pip install --upgrade setuptools # We don't actually want to install briefcase; we just # want the dev extras so we have a known version of tox. python -m pip install $(ls dist/briefcase-*.whl)[dev] - name: Test + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}.${{ matrix.python-version }}" run: tox -e py --installpkg dist/briefcase-*.whl - - name: Store coverage data + - name: Store Coverage Data + if: success() || failure() uses: actions/upload-artifact@v3.1.2 with: name: coverage-data path: ".coverage.*" if-no-files-found: ignore - - name: Report platform coverage - run: tox -e coverage + coverage-platform: + name: Platform coverage - ${{ matrix.platform }} + runs-on: ${{ matrix.platform }}-latest + needs: unit-tests + strategy: + fail-fast: false + matrix: + platform: [ "macOS", "Ubuntu", "Windows" ] + steps: + - name: Checkout + uses: actions/checkout@v3.5.2 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v4.6.0 + with: + python-version: | + 3.12-dev + 3.11 + 3.10 + 3.9 + 3.8 + + - name: Install dev Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools + # We don't actually want to install briefcase; we just + # want the dev extras so we have a known version of tox. + python -m pip install -e .[dev] + + - name: Retrieve Coverage Data + uses: actions/download-artifact@v3.0.2 + with: + name: coverage-data - coverage: - name: Combine & check coverage. + - name: ${{ matrix.platform }} Coverage Report + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}" + run: tox -qe coverage-platform-html-keep + + - name: Python 3.12 on ${{ matrix.platform }} Coverage Report + if: success() || failure() + continue-on-error: true + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.12-dev" + run: tox -qe coverage312-html-keep + + - name: Python 3.11 on ${{ matrix.platform }} Coverage Report + if: success() || failure() + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.11" + run: tox -qe coverage311-html-keep + + - name: Python 3.10 on ${{ matrix.platform }} Coverage Report + if: success() || failure() + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.10" + run: tox -qe coverage310-html-keep + + - name: Python 3.9 on ${{ matrix.platform }} Coverage Report + if: success() || failure() + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.9" + run: tox -qe coverage39-html-keep + + - name: Python 3.8 on ${{ matrix.platform }} Coverage Report + if: success() || failure() + env: + COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.8" + run: tox -qe coverage38-html-keep + + - name: Upload HTML Coverage Report + if: failure() + uses: actions/upload-artifact@v3.1.2 + with: + name: html-platform-coverage-report-${{ matrix.platform }} + path: htmlcov + + coverage-project: + name: Project coverage runs-on: ubuntu-latest needs: unit-tests steps: - - uses: actions/checkout@v3.5.2 + - name: Checkout + uses: actions/checkout@v3.5.2 with: fetch-depth: 0 @@ -113,46 +190,44 @@ jobs: # https://github.com/nedbat/coveragepy/issues/1572#issuecomment-1522546425 python-version: "3.8" - - name: Install dev dependencies + - name: Install dev Dependencies run: | - # pip 23.1 has an issue with --user installs. - # See https://github.com/pypa/pip/issues/11982 for details - python -m pip install --upgrade "pip!=23.1" + python -m pip install --upgrade pip python -m pip install --upgrade setuptools # We don't actually want to install briefcase; we just # want the dev extras so we have a known version of tox. python -m pip install -e .[dev] - - name: Retrieve coverage data + - name: Retrieve Coverage Data uses: actions/download-artifact@v3.0.2 with: name: coverage-data - - name: Generate coverage report - run: tox -e coverage-html-fail + - name: Project Coverage Report + run: tox -qe coverage-project-html - - name: Upload HTML report if check failed. - if: ${{ failure() }} + - name: Upload HTML Coverage Report + if: failure() uses: actions/upload-artifact@v3.1.2 with: - name: html-coverage-report + name: html-project-coverage-report path: htmlcov - verify-apps: - name: Build App - needs: unit-tests - uses: beeware/.github/.github/workflows/app-build-verify.yml@main - with: - # This *must* be the version of Python that is the system Python on the - # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04 - # rather than `-latest` because at some point, `-latest` will become - # `-24.04`, but it will be a soft changeover, which will cause havoc with - # the hard Python version requirement for local system packages. - python-version: "3.10" - runner-os: ${{ matrix.runner-os }} - framework: ${{ matrix.framework }} - strategy: - fail-fast: false - matrix: - framework: [ "toga", "pyside2", "pyside6", "ppb", "pygame" ] - runner-os: [ "macos-latest", "ubuntu-22.04", "windows-latest" ] +# verify-apps: +# name: Build app +# needs: unit-tests +# uses: beeware/.github/.github/workflows/app-build-verify.yml@main +# with: +# # This *must* be the version of Python that is the system Python on the +# # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04 +# # rather than `-latest` because at some point, `-latest` will become +# # `-24.04`, but it will be a soft changeover, which will cause havoc with +# # the hard Python version requirement for local system packages. +# python-version: "3.10" +# runner-os: ${{ matrix.runner-os }} +# framework: ${{ matrix.framework }} +# strategy: +# fail-fast: false +# matrix: +# framework: [ "toga", "pyside2", "pyside6", "ppb", "pygame" ] +# runner-os: [ "macos-latest", "ubuntu-22.04", "windows-latest" ] diff --git a/changes/1260.misc.rst b/changes/1260.misc.rst new file mode 100644 index 000000000..66af39fb3 --- /dev/null +++ b/changes/1260.misc.rst @@ -0,0 +1 @@ +Coverage reporting for a specific versions of Python or a specific platform is now supported. diff --git a/pyproject.toml b/pyproject.toml index 9c1d2e449..3e4bd5154 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ requires = ["setuptools>=60", "setuptools_scm[toml]>=7.0"] build-backend = "setuptools.build_meta" [tool.coverage.run] +plugins = ["coverage_conditional_plugin"] parallel = true branch = true relative_files = true @@ -26,6 +27,37 @@ exclude_lines = [ "if TYPE_CHECKING:", ] +[tool.coverage.coverage_conditional_plugin.rules] +# Packages/Modules +no-cover-if-missing-setuptools_scm = "not is_installed('setuptools_scm')" +# Linux +no-cover-if-is-linux = "sys_platform == 'linux' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-not-linux = "sys_platform != 'linux' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +# macOS +no-cover-if-is-macos = "sys_platform == 'darwin' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-not-macos = "sys_platform != 'darwin' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +# Windows +no-cover-if-is-windows = "sys_platform == 'win32' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-not-windows = "sys_platform != 'win32' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +# Python 3.12 +no-cover-if-is-py312 = "python_version == '3.12' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py312 = "sys_version_info < (3, 12) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py312 = "sys_version_info > (3, 12) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +# Python 3.11 +no-cover-if-is-py311 = "python_version == '3.11' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py311 = "sys_version_info > (3, 11) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +# Python 3.10 +no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +# Python 3.9 +no-cover-if-is-py39 = "python_version == '3.9' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py39 = "sys_version_info < (3, 9) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py39 = "sys_version_info > (3, 9) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +# Python 3.8 +no-cover-if-is-py38 = "python_version == '3.8' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" + [tool.isort] profile = "black" skip_glob = [ diff --git a/setup.cfg b/setup.cfg index 3bebf580a..08f995a96 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,9 +64,7 @@ install_requires = # the most recent version. importlib_metadata >= 4.4; python_version <= "3.9" packaging >= 22.0 - # pip 23.1 has an issue with --user installs. - # See https://github.com/pypa/pip/issues/11982 for details - pip >= 22, != 23.1 + pip >= 23.1.1 setuptools >= 60 wheel >= 0.37 build >= 0.10 @@ -91,6 +89,7 @@ install_requires = # ensure environment consistency. dev = coverage[toml] == 7.2.5 + coverage-conditional-plugin == 0.8.0 pre-commit == 3.2.2 pytest == 7.3.1 pytest-xdist == 3.2.1 diff --git a/src/briefcase/__init__.py b/src/briefcase/__init__.py index 3fa52c9c6..0ed6e069d 100644 --- a/src/briefcase/__init__.py +++ b/src/briefcase/__init__.py @@ -10,7 +10,7 @@ # Excluded from coverage because a pure test environment (such as the one # used by tox in CI) won't have setuptools_scm __version__ = get_version("../..", relative_to=__file__) # pragma: no cover -except (ModuleNotFoundError, LookupError): +except (ModuleNotFoundError, LookupError): # pragma: no-cover-if-missing-setuptools_scm # If setuptools_scm isn't in the environment, the call to import will fail. # If it *is* in the environment, but the code isn't a git checkout (e.g., # it's been pip installed non-editable) the call to get_version() will fail. diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index b434bc211..9364ea7c9 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -17,12 +17,12 @@ try: import importlib_metadata -except ImportError: +except ImportError: # pragma: no-cover-if-lt-py310 import importlib.metadata as importlib_metadata try: import tomllib -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no-cover-if-gte-py310 import tomli as tomllib from briefcase import __version__ @@ -177,10 +177,10 @@ def validate_data_path(self, data_path): "The path specified by BRIEFCASE_HOME does not exist." ) except KeyError: - if platform.system() == "Darwin": + if platform.system() == "Darwin": # pragma: no-cover-if-not-macos # macOS uses a bundle name, rather than just the app name app_name = "org.beeware.briefcase" - else: + else: # pragma: no-cover-if-is-macos app_name = "briefcase" data_path = PlatformDirs( @@ -214,7 +214,7 @@ def validate_data_path(self, data_path): # performed via ``cmd.exe`` in a different process. Once this # directory exists in the "real" %LOCALAPPDATA%, Windows will # allow normal interactions without attempting to sandbox them. - if platform.system() == "Windows": + if platform.system() == "Windows": # pragma: no-cover-if-not-windows subprocess.run( ["mkdir", data_path], shell=True, @@ -222,7 +222,7 @@ def validate_data_path(self, data_path): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - else: + else: # pragma: no-cover-if-is-windows os.makedirs(data_path, exist_ok=True) except (subprocess.CalledProcessError, OSError): raise BriefcaseCommandError( diff --git a/src/briefcase/commands/open.py b/src/briefcase/commands/open.py index 3b0fe4d3e..e63397a49 100644 --- a/src/briefcase/commands/open.py +++ b/src/briefcase/commands/open.py @@ -10,11 +10,11 @@ class OpenCommand(BaseCommand): description = "Open an app in the build tool for the target platform." def _open_app(self, app: BaseConfig): - if self.tools.host_os == "Windows": + if self.tools.host_os == "Windows": # pragma: no-cover-if-not-windows self.tools.os.startfile(self.project_path(app)) - elif self.tools.host_os == "Darwin": + elif self.tools.host_os == "Darwin": # pragma: no-cover-if-not-macos self.tools.subprocess.Popen(["open", self.project_path(app)]) - else: + else: # pragma: no-cover-if-not-linux self.tools.subprocess.Popen(["xdg-open", self.project_path(app)]) def open_app(self, app: BaseConfig, **options): diff --git a/src/briefcase/config.py b/src/briefcase/config.py index be1303186..824d34685 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -6,7 +6,7 @@ try: import tomllib -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no-cover-if-gte-py310 import tomli as tomllib from briefcase.platforms import get_output_formats, get_platforms diff --git a/src/briefcase/integrations/docker.py b/src/briefcase/integrations/docker.py index b4b09329e..cb05c1f46 100644 --- a/src/briefcase/integrations/docker.py +++ b/src/briefcase/integrations/docker.py @@ -347,7 +347,7 @@ def prepare( f"Error building Docker container image for {self.app.app_name}." ) from e - def _dockerize_path(self, arg: str): + def _dockerize_path(self, arg: str): # pragma: no-cover-if-is-windows """Relocate any local path into the equivalent location on the docker filesystem. @@ -366,13 +366,20 @@ def _dockerize_path(self, arg: str): return arg - def _dockerize_args(self, args, interactive=False, mounts=None, env=None, cwd=None): + def _dockerize_args( + self, + args, + interactive=False, + mounts=None, + env=None, + cwd=None, + ): # pragma: no-cover-if-is-windows """Convert arguments and environment into a Docker-compatible form. Convert an argument and environment specification into a form that can be used as arguments to invoke Docker. This involves: * Configuring the Docker invocation to reference the - appropriate container image, and clean up afterwards + appropriate container image, and clean up afterward * Setting volume mounts for the container instance * Transforming any references to local paths into Docker path references. diff --git a/src/briefcase/integrations/linuxdeploy.py b/src/briefcase/integrations/linuxdeploy.py index 9b48d0901..784fb9982 100644 --- a/src/briefcase/integrations/linuxdeploy.py +++ b/src/briefcase/integrations/linuxdeploy.py @@ -368,7 +368,7 @@ def verify_plugins(self, plugin_definitions, bundle_path): if plugin_name.startswith(("https://", "http://")): self.tools.logger.info(f"Using URL plugin {plugin_name}") plugin = LinuxDeployURLPlugin.verify(self.tools, url=plugin_name) - else: + else: # pragma: no-cover-if-is-windows self.tools.logger.info(f"Using local file plugin {plugin_name}") plugin = LinuxDeployLocalFilePlugin.verify( self.tools, diff --git a/src/briefcase/integrations/windows_sdk.py b/src/briefcase/integrations/windows_sdk.py index 141b4c39c..f2176d3cd 100644 --- a/src/briefcase/integrations/windows_sdk.py +++ b/src/briefcase/integrations/windows_sdk.py @@ -7,7 +7,7 @@ # winreg can only be imported on Windows try: import winreg -except ImportError: +except ImportError: # pragma: no-cover-if-is-windows winreg = None from briefcase.exceptions import BriefcaseCommandError diff --git a/src/briefcase/platforms/__init__.py b/src/briefcase/platforms/__init__.py index ee52aedb8..12b043b78 100644 --- a/src/briefcase/platforms/__init__.py +++ b/src/briefcase/platforms/__init__.py @@ -5,7 +5,7 @@ # Therefore, we try to import the compatibility shim first; and fall # back to the stdlib module if the shim isn't there. from importlib_metadata import entry_points -except ImportError: +except ImportError: # pragma: no-cover-if-lt-py310 from importlib.metadata import entry_points diff --git a/src/briefcase/platforms/linux/__init__.py b/src/briefcase/platforms/linux/__init__.py index 1775951d7..05864ba10 100644 --- a/src/briefcase/platforms/linux/__init__.py +++ b/src/briefcase/platforms/linux/__init__.py @@ -112,7 +112,7 @@ def vendor_details(self, freedesktop_info): return vendor, codename, vendor_base -class LocalRequirementsMixin: +class LocalRequirementsMixin: # pragma: no-cover-if-is-windows # A mixin that captures the process of compiling requirements that are specified # as local file references into sdists, and then installing those requirements # from the sdist. @@ -209,7 +209,7 @@ def _pip_requires(self, app: AppConfig, requires: List[str]): return final -class DockerOpenCommand(OpenCommand): +class DockerOpenCommand(OpenCommand): # pragma: no-cover-if-is-windows # A command that redirects Open to an interactive shell in the container # if Docker is being used. Relies on the final command to provide # verification that Docker is available, and verify the app context. diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index 6f69c3828..085d7a11c 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -188,7 +188,7 @@ def verify_tools(self): super().verify_tools() LinuxDeploy.verify(tools=self.tools) - def build_app(self, app: AppConfig, **kwargs): + def build_app(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-windows """Build an application. :param app: The application to build diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 804fa5c37..866c69fee 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -120,11 +120,11 @@ def app_python_version_tag(self, app): def platform_freedesktop_info(self, app): try: - if sys.version_info < (3, 10): + if sys.version_info < (3, 10): # pragma: no-cover-if-gte-py310 # This reproduces the Python 3.10 platform.freedesktop_os_release() function. with self.tools.ETC_OS_RELEASE.open(encoding="utf-8") as f: freedesktop_info = parse_freedesktop_os_release(f.read()) - else: + else: # pragma: no-cover-if-lt-py310 freedesktop_info = self.tools.platform.freedesktop_os_release() except OSError as e: @@ -684,7 +684,7 @@ def build_app(self, app: AppConfig, **kwargs): new_perms = user_perms | (world_perms << 3) | world_perms # If there's been any change in permissions, apply them - if new_perms != old_perms: + if new_perms != old_perms: # pragma: no-cover-if-is-windows self.logger.info( "Updating file permissions on " f"{path.relative_to(self.bundle_path(app))} " @@ -881,7 +881,7 @@ def _package_deb(self, app: AppConfig, **kwargs): self.distribution_path(app), ) - def _package_rpm(self, app: AppConfig, **kwargs): + def _package_rpm(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-windows self.logger.info("Building .rpm package...", prefix=app.app_name) # The long description *must* exist. @@ -1040,7 +1040,7 @@ def _package_rpm(self, app: AppConfig, **kwargs): self.distribution_path(app), ) - def _package_pkg(self, app: AppConfig, **kwargs): + def _package_pkg(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-windows self.logger.info("Building .pkg.tar.zst package...", prefix=app.app_name) # The description *must* exist. diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index f40cfdd55..f0cb50430 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -18,11 +18,9 @@ try: import dmgbuild -except ImportError: # pragma: no cover +except ImportError: # pragma: no-cover-if-is-macos # On non-macOS platforms, dmgbuild won't be installed. # Allow the plugin to be loaded; raise an error when tools are verified. - # We don't need to worry about coverage of this branch because it's - # handled by the verification process. dmgbuild = None @@ -167,12 +165,12 @@ def run_app( raise BriefcaseCommandError(f"Unable to start app {app.app_name}.") finally: # Ensure the App also terminates when exiting - if app_pid: + if app_pid: # pragma: no-cover-if-is-py310 with suppress(ProcessLookupError): self.tools.os.kill(app_pid, SIGTERM) -def is_mach_o_binary(path): +def is_mach_o_binary(path): # pragma: no-cover-if-is-windows """Determine if the file at the given path is a Mach-O binary. :param path: The path to check diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 071e707a3..f4bf327ba 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -11,7 +11,7 @@ try: import tomllib -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no-cover-if-gte-py310 import tomli as tomllib import tomli_w diff --git a/tests/commands/dev/test_get_environment.py b/tests/commands/dev/test_get_environment.py index d8a70c9f0..178f16d16 100644 --- a/tests/commands/dev/test_get_environment.py +++ b/tests/commands/dev/test_get_environment.py @@ -1,5 +1,6 @@ import sys from pathlib import Path +from unittest.mock import PropertyMock import pytest @@ -7,6 +8,22 @@ PYTHONMALLOC = "PYTHONMALLOC" +@pytest.mark.parametrize("platform", ["windows", "darwin", "linux", "wonky"]) +def test_pythonmalloc_only_set_for_windows( + dev_command, + first_app, + platform, + monkeypatch, +): + """PYTHONMALLOC env var is only set on Windows.""" + monkeypatch.setattr( + type(dev_command), "platform", PropertyMock(return_value=platform) + ) + env = dev_command.get_environment(first_app, test_mode=False) + expected = "default" if platform == "windows" else "missing" + assert env.get(PYTHONMALLOC, "missing") == expected + + @pytest.mark.skipif(sys.platform != "win32", reason="Relevant only for windows") def test_pythonpath_with_one_source_in_windows(dev_command, first_app): """Test get environment with one source.""" diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py index 3237d13a5..439e757da 100644 --- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py +++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py @@ -1,7 +1,6 @@ import os import platform import shutil -import sys from unittest.mock import MagicMock import pytest @@ -11,6 +10,15 @@ from briefcase.integrations.base import ToolCache +def sdk_download_tag(host_platform: str) -> str: + """Maps ``platform.system()`` to the OS tag in the sdk download URL.""" + return { + "Windows": "win", + "Darwin": "mac", + "Linux": "linux", + }[host_platform] + + @pytest.fixture def mock_tools(mock_tools) -> ToolCache: # Mock the os environment, but copy over other key functions. @@ -20,11 +28,7 @@ def mock_tools(mock_tools) -> ToolCache: mock_tools.os.X_OK = os.X_OK # Identify the host platform - mock_tools._test_download_tag = { - "Windows": "win", - "Darwin": "mac", - "Linux": "linux", - }[mock_tools.host_os] + mock_tools._test_download_tag = sdk_download_tag(mock_tools.host_os) # Use the original module rmtree implementation mock_tools.shutil.rmtree = shutil.rmtree @@ -166,11 +170,22 @@ def test_invalid_user_provided_sdk(mock_tools, tmp_path): assert sdk.root_path == android_sdk_root_path -def test_download_sdk(mock_tools, tmp_path): +@pytest.mark.parametrize("mock_host_os", ["Darwin", "Linux", "Windows"]) +def test_download_sdk(mock_tools, mock_host_os, tmp_path): """If an SDK is not available, one will be downloaded.""" android_sdk_root_path = tmp_path / "tools" / "android_sdk" cmdline_tools_base_path = android_sdk_root_path / "cmdline-tools" + # Mock the host os + mock_tools.host_os = mock_host_os + + # Allow calls to `os.access()` on when the actual host is Windows + if platform.system() == "Windows": + mock_tools.os.access = MagicMock( + spec_set=os.access, + side_effect=[True, False, True], + ) + # The download will produce a cached file. cache_file = MagicMock() mock_tools.download.file.return_value = cache_file @@ -187,7 +202,7 @@ def test_download_sdk(mock_tools, tmp_path): # Validate that the SDK was downloaded and unpacked url = ( "https://dl.google.com/android/repository/" - f"commandlinetools-{mock_tools._test_download_tag}-8092744_latest.zip" + f"commandlinetools-{sdk_download_tag(mock_host_os)}-8092744_latest.zip" ) mock_tools.download.file.assert_called_once_with( url=url, @@ -210,11 +225,20 @@ def test_download_sdk(mock_tools, tmp_path): assert sdk.cmdline_tools_path.is_dir() assert sdk.cmdline_tools_version_path.is_file() - if platform.system() != "Windows": - # On non-Windows, ensure the unpacked binary was made executable - assert os.access( - cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", os.X_OK - ) + # Verify binaries marked executable for non-Windows + if mock_host_os != "Windows": + if platform.system() == "Windows": + # When the actual host is Windows, check the mock was called + mock_tools.os.access.assert_called_with( + cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", + os.X_OK, + ) + else: + # When the actual host is non-Windows, ensure the unpacked binary is executable + assert os.access( + cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", + os.X_OK, + ) # The license has been accepted assert (android_sdk_root_path / "licenses" / "android-sdk-license").exists() @@ -306,10 +330,6 @@ def test_no_install(mock_tools, tmp_path): assert mock_tools.download.file.call_count == 0 -@pytest.mark.skipif( - sys.platform == "win32", - reason="executable permission doesn't make sense on Windows", -) def test_download_sdk_if_sdkmanager_not_executable(mock_tools, tmp_path): """An SDK will be downloaded and unpacked if `tools/bin/sdkmanager` exists but does not have its permissions set properly.""" diff --git a/tests/platforms/iOS/xcode/test_mixin.py b/tests/platforms/iOS/xcode/test_mixin.py index 6d0b3f6a5..a94d3ea48 100644 --- a/tests/platforms/iOS/xcode/test_mixin.py +++ b/tests/platforms/iOS/xcode/test_mixin.py @@ -1,5 +1,8 @@ +from unittest.mock import MagicMock + import pytest +import briefcase.integrations.xcode from briefcase.console import Console, Log from briefcase.exceptions import NoDistributionArtefact from briefcase.platforms.iOS.xcode import iOSXcodeCreateCommand @@ -37,3 +40,39 @@ def test_distribution_path(create_command, first_app_config, tmp_path): match=r"WARNING: No distributable artefact has been generated", ): create_command.distribution_path(first_app_config) + + +def test_verify(create_command, monkeypatch): + """If you're on macOS, you can verify tools.""" + create_command.tools.host_os = "Darwin" + + mock_ensure_xcode_is_installed = MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_xcode_is_installed", + mock_ensure_xcode_is_installed, + ) + mock_ensure_command_line_tools_are_installed = MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_command_line_tools_are_installed", + mock_ensure_command_line_tools_are_installed, + ) + mock_confirm_xcode_license_accepted = MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "confirm_xcode_license_accepted", + mock_confirm_xcode_license_accepted, + ) + + create_command.verify_tools() + + assert create_command.tools.xcode_cli is not None + mock_ensure_xcode_is_installed.assert_called_once_with( + create_command.tools, + min_version=(10, 0, 0), + ) + mock_ensure_command_line_tools_are_installed.assert_called_once_with( + create_command.tools + ) + mock_confirm_xcode_license_accepted.assert_called_once_with(create_command.tools) diff --git a/tests/platforms/linux/appimage/test_mixin.py b/tests/platforms/linux/appimage/test_mixin.py index 2d09a4d17..5baf7680c 100644 --- a/tests/platforms/linux/appimage/test_mixin.py +++ b/tests/platforms/linux/appimage/test_mixin.py @@ -39,6 +39,17 @@ def test_binary_path(create_command, first_app_config, tmp_path): ) +def test_project_path(create_command, first_app_config, tmp_path): + """The project path is the bundle path.""" + project_path = create_command.project_path(first_app_config) + bundle_path = create_command.bundle_path(first_app_config) + + expected_path = ( + tmp_path / "base_path" / "build" / "first-app" / "linux" / "appimage" + ) + assert expected_path == project_path == bundle_path + + def test_distribution_path(create_command, first_app_config, tmp_path): # Force the architecture to x86_64 for test purposes. create_command.tools.host_arch = "x86_64" diff --git a/tests/platforms/linux/test_LocalRequirementsMixin.py b/tests/platforms/linux/test_LocalRequirementsMixin.py index 744e72ad2..c68e8e6b2 100644 --- a/tests/platforms/linux/test_LocalRequirementsMixin.py +++ b/tests/platforms/linux/test_LocalRequirementsMixin.py @@ -17,7 +17,8 @@ class DummyCreateCommand(LocalRequirementsMixin, CreateCommand): - # An command that provides the stubs required to satisfy LocalRequirementeMixin + """A command that provides the stubs required to satisfy LocalRequirementsMixin.""" + platform = "Tester" output_format = "Dummy" diff --git a/tests/platforms/macOS/app/test_package.py b/tests/platforms/macOS/app/test_package.py index 06f41145b..a8e86510f 100644 --- a/tests/platforms/macOS/app/test_package.py +++ b/tests/platforms/macOS/app/test_package.py @@ -5,6 +5,7 @@ import pytest +import briefcase.integrations.xcode from briefcase.console import Console, Log from briefcase.exceptions import BriefcaseCommandError from briefcase.platforms.macOS.app import macOSAppPackageCommand @@ -834,14 +835,28 @@ def test_dmg_with_missing_installer_background( ) -def test_verify(package_command): +def test_verify(package_command, monkeypatch): """If you're on macOS, you can verify tools.""" + package_command.tools.host_os = "Darwin" + # Mock the existence of the command line tools - package_command.tools.subprocess.check_output.side_effect = [ - subprocess.CalledProcessError(cmd=["xcode-select", "--install"], returncode=1), - "clang 37.42", # clang --version - ] + mock_ensure_command_line_tools_are_installed = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_command_line_tools_are_installed", + mock_ensure_command_line_tools_are_installed, + ) + mock_confirm_xcode_license_accepted = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "confirm_xcode_license_accepted", + mock_confirm_xcode_license_accepted, + ) package_command.verify_tools() assert package_command.tools.xcode_cli is not None + mock_ensure_command_line_tools_are_installed.assert_called_once_with( + package_command.tools + ) + mock_confirm_xcode_license_accepted.assert_called_once_with(package_command.tools) diff --git a/tests/platforms/macOS/xcode/test_package.py b/tests/platforms/macOS/xcode/test_package.py index aa8eef7d1..3a1610b97 100644 --- a/tests/platforms/macOS/xcode/test_package.py +++ b/tests/platforms/macOS/xcode/test_package.py @@ -1 +1,56 @@ -# skip since packaging uses the same code as app command +from unittest import mock + +import pytest + +import briefcase.integrations.xcode +from briefcase.console import Console, Log +from briefcase.platforms.macOS.xcode import macOSXcodePackageCommand + +# skip most tests since packaging uses the same code as app command + + +@pytest.fixture +def package_command(tmp_path): + command = macOSXcodePackageCommand( + logger=Log(), + console=Console(), + base_path=tmp_path / "base_path", + data_path=tmp_path / "briefcase", + ) + return command + + +def test_verify(package_command, monkeypatch): + """If you're on macOS, you can verify tools.""" + package_command.tools.host_os = "Darwin" + + mock_ensure_xcode_is_installed = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_xcode_is_installed", + mock_ensure_xcode_is_installed, + ) + mock_ensure_command_line_tools_are_installed = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_command_line_tools_are_installed", + mock_ensure_command_line_tools_are_installed, + ) + mock_confirm_xcode_license_accepted = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "confirm_xcode_license_accepted", + mock_confirm_xcode_license_accepted, + ) + + package_command.verify_tools() + + assert package_command.tools.xcode_cli is not None + mock_ensure_xcode_is_installed.assert_called_once_with( + package_command.tools, + min_version=(10, 0, 0), + ) + mock_ensure_command_line_tools_are_installed.assert_called_once_with( + package_command.tools + ) + mock_confirm_xcode_license_accepted.assert_called_once_with(package_command.tools) diff --git a/tests/platforms/windows/visualstudio/test_build.py b/tests/platforms/windows/visualstudio/test_build.py index 2d438ce1f..b8b3c3cc2 100644 --- a/tests/platforms/windows/visualstudio/test_build.py +++ b/tests/platforms/windows/visualstudio/test_build.py @@ -1,10 +1,10 @@ -import platform import subprocess from pathlib import Path from unittest import mock import pytest +import briefcase.platforms.windows.visualstudio from briefcase.console import Console, Log from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.subprocess import Subprocess @@ -28,15 +28,21 @@ def build_command(tmp_path): return command -@pytest.mark.skipif(platform.system() != "Windows", reason="Windows specific tests") -def test_verify(build_command): +def test_verify(build_command, monkeypatch): """Verifying on Windows creates a VisualStudio wrapper.""" + build_command.tools.host_os = "Windows" - build_command.tools.subprocess = mock.MagicMock(spec_set=Subprocess) + mock_visualstudio_verify = mock.MagicMock(wraps=VisualStudio.verify) + monkeypatch.setattr( + briefcase.platforms.windows.visualstudio.VisualStudio, + "verify", + mock_visualstudio_verify, + ) build_command.verify_tools() - # No error and an SDK wrapper is created + # VisualStudio tool was verified + mock_visualstudio_verify.assert_called_once_with(build_command.tools) assert isinstance(build_command.tools.visualstudio, VisualStudio) diff --git a/tox.ini b/tox.ini index 517d46e61..7b44e1bc7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,10 @@ [tox] -envlist = py{38,39,310,311,312},coverage,pre-commit,docs-lint,towncrier-check -isolated_build = True +envlist = towncrier-check,docs-lint,pre-commit,py{38,39,310,311,312},coverage +labels = + test = py{38,39,310,311,312},coverage + test-fast = py{38,39,310,311,312}-fast + test-cov = py{38,39,310,311,312},coverage-platform + ci = towncrier-check,docs-lint,pre-commit,py{38,39,310,311,312},coverage-platform skip_missing_interpreters = True [pkgenv] @@ -21,6 +25,7 @@ use_develop = fast: True skip_sdist = fast: True # Needed on Windows to test data directory creation passenv = LOCALAPPDATA +setenv = COVERAGE_FILE = {env:COVERAGE_FILE:.coverage} extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} @@ -28,19 +33,39 @@ commands = !fast : python -m coverage run -m pytest {posargs:-vv} fast : python -m pytest {posargs:-vv -n auto} -[testenv:coverage{,-html}{,-fail}] +[testenv:coverage{,38,39,310,311,312}{,-platform}{,-project}{,-html}{,-keep}{,-nofail}] depends = py{,38,39,310,311,312} -# coverage should run on oldest supported Python -base_python = py38,py39,py310,py311 +# by default, coverage should run on oldest supported Python for testing platform coverage. +# however, coverage for a particular Python version should match the version used for pytest. +base_python = + coverage: py38,py39,py310,py311,py312 + coverage38: py38 + coverage39: py39 + coverage310: py310 + coverage311: py311 + coverage312: py312 parallel_show_output = True extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} +setenv = + COVERAGE_FILE = {env:COVERAGE_FILE:.coverage} + keep: COMBINE_FLAGS = --keep + !nofail: REPORT_FLAGS = --fail-under=100 + # disable coverage exclusions for Python version to test entire platform + {platform,project}: COV_EXCLUDE_PYTHON_VERSION=disable + # disable coverage exclusions for host platform to test entire project + project: COV_EXCLUDE_PLATFORM=disable +commands_pre = + python --version + python -c 'if 1: \ + import os; \ + print("COV_EXCLUDE_PYTHON_VERSION", os.environ.get("COV_EXCLUDE_PYTHON_VERSION", "")); \ + print("COV_EXCLUDE_PLATFORM", os.environ.get("COV_EXCLUDE_PLATFORM", ""))' commands = - -python -m coverage combine - html : python -m coverage html --skip-covered --skip-empty - !fail : python -m coverage report - fail : python -m coverage report --fail-under=100 + -python -m coverage combine {env:COMBINE_FLAGS} + html: python -m coverage html --skip-covered --skip-empty + python -m coverage report {env:REPORT_FLAGS} [testenv:towncrier-check] package_env = none From edc27046a3fe8735c6c0f60513973febeb772583 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 5 May 2023 12:09:35 -0400 Subject: [PATCH 02/14] Allow platform to be spoofed for coverage --- .github/workflows/ci.yml | 178 ++++++++++++++++++--------------------- pyproject.toml | 38 ++++----- tox.ini | 14 +-- 3 files changed, 109 insertions(+), 121 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 418d38185..eb6206796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,8 +38,9 @@ jobs: runs-on: ${{ matrix.platform }}-latest continue-on-error: ${{ matrix.experimental }} strategy: + fail-fast: false matrix: - platform: [ "macOS", "Ubuntu", "Windows" ] + platform: [ "macos", "ubuntu", "windows" ] python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12-dev" ] include: - experimental: false @@ -47,8 +48,8 @@ jobs: - python-version: "3.12-dev" experimental: true # Run tests against the latest Windows Store Python - - platform: "Windows" - python-version: "winstore" + - platform: "windows" + python-version: "winstore3.11" experimental: false steps: - name: Checkout @@ -63,8 +64,10 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Windows Store Python - if: matrix.python-version == 'winstore' + if: startswith(matrix.python-version, 'winstore') uses: beeware/.github/.github/actions/install-win-store-python@main + with: + python-version: "3.11" - name: Get Packages uses: actions/download-artifact@v3.0.2 @@ -81,26 +84,36 @@ jobs: python -m pip install $(ls dist/briefcase-*.whl)[dev] - name: Test + id: test env: COVERAGE_FILE: ".coverage.${{ matrix.platform }}.${{ matrix.python-version }}" run: tox -e py --installpkg dist/briefcase-*.whl - name: Store Coverage Data - if: success() || failure() + if: always() && (steps.test.outcome == 'failure' || steps.test.outcome == 'success') uses: actions/upload-artifact@v3.1.2 with: name: coverage-data path: ".coverage.*" if-no-files-found: ignore - coverage-platform: - name: Platform coverage - ${{ matrix.platform }} - runs-on: ${{ matrix.platform }}-latest + - name: Report Platform Coverage + id: coverage + if: always() && (steps.test.outcome == 'failure' || steps.test.outcome == 'success') + run: tox -qe coverage$(echo '${{ matrix.python-version }}' | tr -dc '0-9')-html + + - name: Upload HTML Coverage Report + if: always() && steps.coverage.outcome == 'failure' + uses: actions/upload-artifact@v3.1.2 + with: + name: html-coverage-report-${{ matrix.platform }}-${{ matrix.python-version }} + path: htmlcov + + coverage: + name: Project coverage + runs-on: ubuntu-latest needs: unit-tests - strategy: - fail-fast: false - matrix: - platform: [ "macOS", "Ubuntu", "Windows" ] + if: always() && (needs.unit-tests.result == 'success' || needs.unit-tests.result == 'failure') steps: - name: Checkout uses: actions/checkout@v3.5.2 @@ -110,12 +123,9 @@ jobs: - name: Setup Python uses: actions/setup-python@v4.6.0 with: - python-version: | - 3.12-dev - 3.11 - 3.10 - 3.9 - 3.8 + # Use minimum version of python for coverage to avoid phantom branches + # https://github.com/nedbat/coveragepy/issues/1572#issuecomment-1522546425 + python-version: "3.8" - name: Install dev Dependencies run: | @@ -130,104 +140,78 @@ jobs: with: name: coverage-data - - name: ${{ matrix.platform }} Coverage Report - env: - COVERAGE_FILE: ".coverage.${{ matrix.platform }}" - run: tox -qe coverage-platform-html-keep - - - name: Python 3.12 on ${{ matrix.platform }} Coverage Report + - name: Linux Coverage Report + id: linux-coverage if: success() || failure() - continue-on-error: true env: - COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.12-dev" - run: tox -qe coverage312-html-keep + COVERAGE_FILE: ".coverage.ubuntu" + COVERAGE_PLATFORM: "linux" + run: tox -qe coverage-platform-html - - name: Python 3.11 on ${{ matrix.platform }} Coverage Report - if: success() || failure() - env: - COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.11" - run: tox -qe coverage311-html-keep + - name: Upload Linux Coverage HTML Report + if: always() && steps.linux-coverage.outcome == 'failure' + uses: actions/upload-artifact@v3.1.2 + with: + name: html-coverage-report-Linux + path: htmlcov - - name: Python 3.10 on ${{ matrix.platform }} Coverage Report + - name: macOS Coverage Report + id: macos-coverage if: success() || failure() env: - COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.10" - run: tox -qe coverage310-html-keep + COVERAGE_FILE: ".coverage.macos" + COVERAGE_PLATFORM: "darwin" + run: tox -qe coverage-platform-html - - name: Python 3.9 on ${{ matrix.platform }} Coverage Report - if: success() || failure() - env: - COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.9" - run: tox -qe coverage39-html-keep + - name: Upload macOS Coverage HTML Report + if: always() && steps.macos-coverage.outcome == 'failure' + uses: actions/upload-artifact@v3.1.2 + with: + name: html-coverage-report-macOS + path: htmlcov - - name: Python 3.8 on ${{ matrix.platform }} Coverage Report + - name: Windows Coverage Report + id: windows-coverage if: success() || failure() env: - COVERAGE_FILE: ".coverage.${{ matrix.platform }}.3.8" - run: tox -qe coverage38-html-keep + COVERAGE_FILE: ".coverage.windows" + COVERAGE_PLATFORM: "win32" + run: tox -qe coverage-platform-html - - name: Upload HTML Coverage Report - if: failure() + - name: Upload Windows Coverage HTML Report + if: always() && steps.windows-coverage.outcome == 'failure' uses: actions/upload-artifact@v3.1.2 with: - name: html-platform-coverage-report-${{ matrix.platform }} + name: html-coverage-report-Windows path: htmlcov - coverage-project: - name: Project coverage - runs-on: ubuntu-latest - needs: unit-tests - steps: - - name: Checkout - uses: actions/checkout@v3.5.2 - with: - fetch-depth: 0 - - - name: Setup Python - uses: actions/setup-python@v4.6.0 - with: - # Use minimum version of python for coverage to avoid phantom branches - # https://github.com/nedbat/coveragepy/issues/1572#issuecomment-1522546425 - python-version: "3.8" - - - name: Install dev Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools - # We don't actually want to install briefcase; we just - # want the dev extras so we have a known version of tox. - python -m pip install -e .[dev] - - - name: Retrieve Coverage Data - uses: actions/download-artifact@v3.0.2 - with: - name: coverage-data - - name: Project Coverage Report + id: project-coverage + if: success() || failure() run: tox -qe coverage-project-html - - name: Upload HTML Coverage Report - if: failure() + - name: Upload Project Coverage HTML Report + if: always() && steps.project-coverage.outcome == 'failure' uses: actions/upload-artifact@v3.1.2 with: - name: html-project-coverage-report + name: html-coverage-report-project path: htmlcov -# verify-apps: -# name: Build app -# needs: unit-tests -# uses: beeware/.github/.github/workflows/app-build-verify.yml@main -# with: -# # This *must* be the version of Python that is the system Python on the -# # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04 -# # rather than `-latest` because at some point, `-latest` will become -# # `-24.04`, but it will be a soft changeover, which will cause havoc with -# # the hard Python version requirement for local system packages. -# python-version: "3.10" -# runner-os: ${{ matrix.runner-os }} -# framework: ${{ matrix.framework }} -# strategy: -# fail-fast: false -# matrix: -# framework: [ "toga", "pyside2", "pyside6", "ppb", "pygame" ] -# runner-os: [ "macos-latest", "ubuntu-22.04", "windows-latest" ] + verify-apps: + name: Build app + needs: unit-tests + uses: beeware/.github/.github/workflows/app-build-verify.yml@main + with: + # This *must* be the version of Python that is the system Python on the + # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04 + # rather than `-latest` because at some point, `-latest` will become + # `-24.04`, but it will be a soft changeover, which will cause havoc with + # the hard Python version requirement for local system packages. + python-version: "3.10" + runner-os: ${{ matrix.runner-os }} + framework: ${{ matrix.framework }} + strategy: + fail-fast: false + matrix: + framework: [ "toga", "pyside2", "pyside6", "ppb", "pygame" ] + runner-os: [ "macos-latest", "ubuntu-22.04", "windows-latest" ] diff --git a/pyproject.toml b/pyproject.toml index 3e4bd5154..3201c4986 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,32 +31,32 @@ exclude_lines = [ # Packages/Modules no-cover-if-missing-setuptools_scm = "not is_installed('setuptools_scm')" # Linux -no-cover-if-is-linux = "sys_platform == 'linux' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" -no-cover-if-not-linux = "sys_platform != 'linux' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-is-linux = "'linux' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-not-linux = "'linux' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" # macOS -no-cover-if-is-macos = "sys_platform == 'darwin' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" -no-cover-if-not-macos = "sys_platform != 'darwin' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-is-macos = "'darwin' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-not-macos = "'darwin' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" # Windows -no-cover-if-is-windows = "sys_platform == 'win32' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" -no-cover-if-not-windows = "sys_platform != 'win32' and os_environ.get('COV_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-is-windows = "'win32' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-not-windows = "'win32' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" # Python 3.12 -no-cover-if-is-py312 = "python_version == '3.12' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py312 = "sys_version_info < (3, 12) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py312 = "sys_version_info > (3, 12) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-is-py312 = "python_version == '3.12' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py312 = "sys_version_info < (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py312 = "sys_version_info > (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" # Python 3.11 -no-cover-if-is-py311 = "python_version == '3.11' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py311 = "sys_version_info > (3, 11) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-is-py311 = "python_version == '3.11' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py311 = "sys_version_info > (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" # Python 3.10 -no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" # Python 3.9 -no-cover-if-is-py39 = "python_version == '3.9' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py39 = "sys_version_info < (3, 9) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py39 = "sys_version_info > (3, 9) and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-is-py39 = "python_version == '3.9' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-lt-py39 = "sys_version_info < (3, 9) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py39 = "sys_version_info > (3, 9) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" # Python 3.8 -no-cover-if-is-py38 = "python_version == '3.8' and os_environ.get('COV_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-is-py38 = "python_version == '3.8' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" [tool.isort] profile = "black" diff --git a/tox.ini b/tox.ini index 7b44e1bc7..69f7d475e 100644 --- a/tox.ini +++ b/tox.ini @@ -48,20 +48,24 @@ parallel_show_output = True extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} +passenv = + COVERAGE_FILE + COVERAGE_PLATFORM setenv = - COVERAGE_FILE = {env:COVERAGE_FILE:.coverage} keep: COMBINE_FLAGS = --keep !nofail: REPORT_FLAGS = --fail-under=100 # disable coverage exclusions for Python version to test entire platform - {platform,project}: COV_EXCLUDE_PYTHON_VERSION=disable + {platform,project}: COVERAGE_EXCLUDE_PYTHON_VERSION=disable # disable coverage exclusions for host platform to test entire project - project: COV_EXCLUDE_PLATFORM=disable + project: COVERAGE_EXCLUDE_PLATFORM=disable commands_pre = python --version python -c 'if 1: \ import os; \ - print("COV_EXCLUDE_PYTHON_VERSION", os.environ.get("COV_EXCLUDE_PYTHON_VERSION", "")); \ - print("COV_EXCLUDE_PLATFORM", os.environ.get("COV_EXCLUDE_PLATFORM", ""))' + print("COVERAGE_FILE", os.environ.get("COVERAGE_FILE", "")); \ + print("COVERAGE_PLATFORM", os.environ.get("COVERAGE_PLATFORM", "")); \ + print("COVERAGE_EXCLUDE_PYTHON_VERSION", os.environ.get("COVERAGE_EXCLUDE_PYTHON_VERSION", "")); \ + print("COVERAGE_EXCLUDE_PLATFORM", os.environ.get("COVERAGE_EXCLUDE_PLATFORM", ""))' commands = -python -m coverage combine {env:COMBINE_FLAGS} html: python -m coverage html --skip-covered --skip-empty From be7095a281c4016d58dacb6fc22af756573dac37 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 5 May 2023 20:38:32 -0400 Subject: [PATCH 03/14] Add changenote. --- changes/{1260.misc.rst => 1262.misc.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{1260.misc.rst => 1262.misc.rst} (100%) diff --git a/changes/1260.misc.rst b/changes/1262.misc.rst similarity index 100% rename from changes/1260.misc.rst rename to changes/1262.misc.rst From f6f16a295c73cac4864183b232205e0dc6b7b5ca Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 8 May 2023 15:43:58 -0400 Subject: [PATCH 04/14] Set `FORCE_COLOR=1` in CI to enable colors for pytest and tox --- .github/workflows/ci.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb6206796..d48dffb83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,18 @@ on: description: "Name of the uploaded artifact; use for artifact retrieval." value: ${{ jobs.package.outputs.artifact-name }} -defaults: - run: - shell: bash # https://github.com/beeware/briefcase/pull/912 - # Cancel active CI runs for a PR before starting another run concurrency: group: ${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash # https://github.com/beeware/briefcase/pull/912 + +env: + FORCE_COLOR: "1" + jobs: pre-commit: name: Pre-commit checks From 0740657d0cb862c97deecf76089aa26a4e08a951 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 8 May 2023 16:35:43 -0400 Subject: [PATCH 05/14] Remove unused coverage rules, mark PYTHONMALLOC no cover, and stop printing coverage env vars in tox --- pyproject.toml | 20 +------------------- src/briefcase/commands/dev.py | 3 ++- tests/commands/dev/test_get_environment.py | 17 ----------------- tox.ini | 9 +-------- 4 files changed, 4 insertions(+), 45 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3201c4986..737ec9bab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,33 +28,15 @@ exclude_lines = [ ] [tool.coverage.coverage_conditional_plugin.rules] -# Packages/Modules no-cover-if-missing-setuptools_scm = "not is_installed('setuptools_scm')" -# Linux -no-cover-if-is-linux = "'linux' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" no-cover-if-not-linux = "'linux' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" -# macOS no-cover-if-is-macos = "'darwin' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" no-cover-if-not-macos = "'darwin' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" -# Windows no-cover-if-is-windows = "'win32' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" no-cover-if-not-windows = "'win32' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" -# Python 3.12 -no-cover-if-is-py312 = "python_version == '3.12' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py312 = "sys_version_info < (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py312 = "sys_version_info > (3, 12) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -# Python 3.11 -no-cover-if-is-py311 = "python_version == '3.11' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py311 = "sys_version_info > (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -# Python 3.10 -no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -# Python 3.9 -no-cover-if-is-py39 = "python_version == '3.9' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-lt-py39 = "sys_version_info < (3, 9) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -no-cover-if-gte-py39 = "sys_version_info > (3, 9) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" + # Python 3.8 no-cover-if-is-py38 = "python_version == '3.8' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" diff --git a/src/briefcase/commands/dev.py b/src/briefcase/commands/dev.py index 07d2c1996..27d5c9e65 100644 --- a/src/briefcase/commands/dev.py +++ b/src/briefcase/commands/dev.py @@ -147,7 +147,8 @@ def get_environment(self, app, test_mode: bool): # On Windows, we need to disable the debug allocator because it # conflicts with Python.net. See # https://github.com/pythonnet/pythonnet/issues/1977 for details. - if self.platform == "windows": + # marking no cover since a single platform cannot take both branches + if self.platform == "windows": # pragma: no cover env["PYTHONMALLOC"] = "default" return env diff --git a/tests/commands/dev/test_get_environment.py b/tests/commands/dev/test_get_environment.py index 178f16d16..d8a70c9f0 100644 --- a/tests/commands/dev/test_get_environment.py +++ b/tests/commands/dev/test_get_environment.py @@ -1,6 +1,5 @@ import sys from pathlib import Path -from unittest.mock import PropertyMock import pytest @@ -8,22 +7,6 @@ PYTHONMALLOC = "PYTHONMALLOC" -@pytest.mark.parametrize("platform", ["windows", "darwin", "linux", "wonky"]) -def test_pythonmalloc_only_set_for_windows( - dev_command, - first_app, - platform, - monkeypatch, -): - """PYTHONMALLOC env var is only set on Windows.""" - monkeypatch.setattr( - type(dev_command), "platform", PropertyMock(return_value=platform) - ) - env = dev_command.get_environment(first_app, test_mode=False) - expected = "default" if platform == "windows" else "missing" - assert env.get(PYTHONMALLOC, "missing") == expected - - @pytest.mark.skipif(sys.platform != "win32", reason="Relevant only for windows") def test_pythonpath_with_one_source_in_windows(dev_command, first_app): """Test get environment with one source.""" diff --git a/tox.ini b/tox.ini index 69f7d475e..58c60998d 100644 --- a/tox.ini +++ b/tox.ini @@ -58,14 +58,7 @@ setenv = {platform,project}: COVERAGE_EXCLUDE_PYTHON_VERSION=disable # disable coverage exclusions for host platform to test entire project project: COVERAGE_EXCLUDE_PLATFORM=disable -commands_pre = - python --version - python -c 'if 1: \ - import os; \ - print("COVERAGE_FILE", os.environ.get("COVERAGE_FILE", "")); \ - print("COVERAGE_PLATFORM", os.environ.get("COVERAGE_PLATFORM", "")); \ - print("COVERAGE_EXCLUDE_PYTHON_VERSION", os.environ.get("COVERAGE_EXCLUDE_PYTHON_VERSION", "")); \ - print("COVERAGE_EXCLUDE_PLATFORM", os.environ.get("COVERAGE_EXCLUDE_PLATFORM", ""))' +commands_pre = python --version commands = -python -m coverage combine {env:COMBINE_FLAGS} html: python -m coverage html --skip-covered --skip-empty From c2f57a3368bd27ae3bd3e8d789612bb6a33b9b8a Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 8 May 2023 16:52:42 -0400 Subject: [PATCH 06/14] Add coverage rule `no-cover-if-is-py310` back --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 737ec9bab..fd0d33253 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ no-cover-if-is-macos = "'darwin' == os_environ.get('COVERAGE_PLATFORM', sys_plat no-cover-if-not-macos = "'darwin' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" no-cover-if-is-windows = "'win32' == os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" no-cover-if-not-windows = "'win32' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_EXCLUDE_PLATFORM') != 'disable'" +no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" From f5b0d2b200808b46ca780229a3911330eb6ff65c Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 8 May 2023 22:34:41 -0400 Subject: [PATCH 07/14] Remove `no-cover-if-is-py38` conditional coverage rule as it's unused --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd0d33253..b0ce2512e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,6 @@ no-cover-if-is-py310 = "python_version == '3.10' and os_environ.get('COVERAGE_EX no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" -# Python 3.8 -no-cover-if-is-py38 = "python_version == '3.8' and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" - [tool.isort] profile = "black" skip_glob = [ From b020e07e4f29bb14aa55cf0a77d826c5e9c18aa8 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 9 May 2023 12:13:45 -0400 Subject: [PATCH 08/14] Use `no cover` instead of mocks for non-windows code for Android SDK --- src/briefcase/commands/dev.py | 5 +- src/briefcase/integrations/android_sdk.py | 6 ++- .../android_sdk/AndroidSDK/test_verify.py | 50 +++++-------------- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/src/briefcase/commands/dev.py b/src/briefcase/commands/dev.py index 27d5c9e65..2ea947c90 100644 --- a/src/briefcase/commands/dev.py +++ b/src/briefcase/commands/dev.py @@ -147,9 +147,8 @@ def get_environment(self, app, test_mode: bool): # On Windows, we need to disable the debug allocator because it # conflicts with Python.net. See # https://github.com/pythonnet/pythonnet/issues/1977 for details. - # marking no cover since a single platform cannot take both branches - if self.platform == "windows": # pragma: no cover - env["PYTHONMALLOC"] = "default" + if self.platform == "windows": # pragma: no branch + env["PYTHONMALLOC"] = "default" # pragma: no-cover-if-not-windows return env diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index a05d4fc98..671b6a50c 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -58,7 +58,7 @@ def cmdline_tools_url(self): platform_name = self.tools.host_os.lower() if self.tools.host_os.lower() == "darwin": platform_name = "mac" - elif self.tools.host_os.lower() == "windows": + elif self.tools.host_os.lower() == "windows": # pragma: no branch platform_name = "win" return f"https://dl.google.com/android/repository/commandlinetools-{platform_name}-{self.cmdline_tools_version}_latest.zip" # noqa: E501 @@ -307,12 +307,14 @@ def install(self): # Zip file no longer needed once unpacked. cmdline_tools_zip_path.unlink() + # fmt: off # Python zip unpacking ignores permission metadata. # On non-Windows, we manually fix permissions. - if self.tools.host_os != "Windows": + if self.tools.host_os != "Windows": # pragma: no branch no-cover-if-is-windows for binpath in (self.cmdline_tools_path / "bin").glob("*"): if not self.tools.os.access(binpath, self.tools.os.X_OK): binpath.chmod(0o755) + # fmt: on # Licences must be accepted. self.verify_license() diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py index 439e757da..faced453e 100644 --- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py +++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py @@ -10,15 +10,6 @@ from briefcase.integrations.base import ToolCache -def sdk_download_tag(host_platform: str) -> str: - """Maps ``platform.system()`` to the OS tag in the sdk download URL.""" - return { - "Windows": "win", - "Darwin": "mac", - "Linux": "linux", - }[host_platform] - - @pytest.fixture def mock_tools(mock_tools) -> ToolCache: # Mock the os environment, but copy over other key functions. @@ -28,7 +19,11 @@ def mock_tools(mock_tools) -> ToolCache: mock_tools.os.X_OK = os.X_OK # Identify the host platform - mock_tools._test_download_tag = sdk_download_tag(mock_tools.host_os) + mock_tools._test_download_tag = { + "Windows": "win", + "Darwin": "mac", + "Linux": "linux", + }[mock_tools.host_os] # Use the original module rmtree implementation mock_tools.shutil.rmtree = shutil.rmtree @@ -170,22 +165,11 @@ def test_invalid_user_provided_sdk(mock_tools, tmp_path): assert sdk.root_path == android_sdk_root_path -@pytest.mark.parametrize("mock_host_os", ["Darwin", "Linux", "Windows"]) -def test_download_sdk(mock_tools, mock_host_os, tmp_path): +def test_download_sdk(mock_tools, tmp_path): """If an SDK is not available, one will be downloaded.""" android_sdk_root_path = tmp_path / "tools" / "android_sdk" cmdline_tools_base_path = android_sdk_root_path / "cmdline-tools" - # Mock the host os - mock_tools.host_os = mock_host_os - - # Allow calls to `os.access()` on when the actual host is Windows - if platform.system() == "Windows": - mock_tools.os.access = MagicMock( - spec_set=os.access, - side_effect=[True, False, True], - ) - # The download will produce a cached file. cache_file = MagicMock() mock_tools.download.file.return_value = cache_file @@ -202,7 +186,7 @@ def test_download_sdk(mock_tools, mock_host_os, tmp_path): # Validate that the SDK was downloaded and unpacked url = ( "https://dl.google.com/android/repository/" - f"commandlinetools-{sdk_download_tag(mock_host_os)}-8092744_latest.zip" + f"commandlinetools-{mock_tools._test_download_tag}-8092744_latest.zip" ) mock_tools.download.file.assert_called_once_with( url=url, @@ -225,20 +209,12 @@ def test_download_sdk(mock_tools, mock_host_os, tmp_path): assert sdk.cmdline_tools_path.is_dir() assert sdk.cmdline_tools_version_path.is_file() - # Verify binaries marked executable for non-Windows - if mock_host_os != "Windows": - if platform.system() == "Windows": - # When the actual host is Windows, check the mock was called - mock_tools.os.access.assert_called_with( - cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", - os.X_OK, - ) - else: - # When the actual host is non-Windows, ensure the unpacked binary is executable - assert os.access( - cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", - os.X_OK, - ) + if platform.system() == "Windows": + # On non-Windows, ensure the unpacked binary was made executable + assert os.access( + cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", + os.X_OK, + ) # The license has been accepted assert (android_sdk_root_path / "licenses" / "android-sdk-license").exists() From 03dca70d86b404fd4a2fa822fb4a2c26a095d34a Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 9 May 2023 14:17:35 -0400 Subject: [PATCH 09/14] Ensure `tox` environments use color - While `FORCE_COLOR=1` is being set in GitHub CI, it actually isn't safe to run `pytest` while `FORCE_COLOR` is set. This causes `pytest` to capture the ANSI control chars in `capsys`; so, tests that rely on asserting the output fail. - This issue is masked, though, because `tox` doesn't pass environment variables through to commands it runs. - Of note, this issue existed before CI started setting `FORCE_COLOR`. --- tox.ini | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index 58c60998d..8916645a4 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ skip_missing_interpreters = True # 2023-04-22 The virtualenv used by Tox has pip 23.1 pinned into it # This version has a bug on winstore installs of Python, so we # need to force pip to be updated. +# Can be removed once tox bumps virtualenv >= v20.23.0 download = True [testenv:pre-commit] @@ -22,7 +23,6 @@ commands = pre-commit run --all-files --show-diff-on-failure --color=always [testenv:py{,38,39,310,311,312}{,-fast}] depends: pre-commit use_develop = fast: True -skip_sdist = fast: True # Needed on Windows to test data directory creation passenv = LOCALAPPDATA setenv = COVERAGE_FILE = {env:COVERAGE_FILE:.coverage} @@ -30,8 +30,8 @@ extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} commands = - !fast : python -m coverage run -m pytest {posargs:-vv} - fast : python -m pytest {posargs:-vv -n auto} + !fast : python -m coverage run -m pytest {posargs:-vv --color yes} + fast : python -m pytest {posargs:-vv --color yes -n auto} [testenv:coverage{,38,39,310,311,312}{,-platform}{,-project}{,-html}{,-keep}{,-nofail}] depends = py{,38,39,310,311,312} @@ -44,7 +44,6 @@ base_python = coverage310: py310 coverage311: py311 coverage312: py312 -parallel_show_output = True extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} @@ -64,17 +63,12 @@ commands = html: python -m coverage html --skip-covered --skip-empty python -m coverage report {env:REPORT_FLAGS} -[testenv:towncrier-check] -package_env = none -skip_install = True -deps = {[testenv:towncrier]deps} -commands = python -m towncrier.check --compare-with origin/main - -[testenv:towncrier] -package_env = none +[testenv:towncrier{,-check}] skip_install = True deps = towncrier ~= 22.8 -commands = towncrier {posargs} +commands = + check : python -m towncrier.check --compare-with origin/main + !check : python -m towncrier {posargs} [docs] build_dir = _build @@ -107,6 +101,7 @@ commands = [testenv:package] package_env = none skip_install = True +passenv = FORCE_COLOR deps = check_manifest build From 34461be6cb93adf6c5021627d35bdcc2c411147c Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 9 May 2023 14:51:02 -0400 Subject: [PATCH 10/14] Simplify coverage in CI - Only produce and upload an HTML report for final project coverage - Convert `COVERAGE_PLATFORM` env var to individual factors for each platform for `tox -e coverage` - Use `-ci` factor to drive using the platform coverage files created in CI; also useful if the CI coverage files are downloaded manually --- .github/workflows/ci.yml | 69 ++++--------------- src/briefcase/integrations/android_sdk.py | 6 +- .../android_sdk/AndroidSDK/test_verify.py | 2 +- tox.ini | 22 +++--- 4 files changed, 29 insertions(+), 70 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d48dffb83..77ff7ff81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: run: tox -e py --installpkg dist/briefcase-*.whl - name: Store Coverage Data - if: always() && (steps.test.outcome == 'failure' || steps.test.outcome == 'success') + if: always() && contains('success,failure', steps.test.outcome) uses: actions/upload-artifact@v3.1.2 with: name: coverage-data @@ -102,21 +102,15 @@ jobs: - name: Report Platform Coverage id: coverage - if: always() && (steps.test.outcome == 'failure' || steps.test.outcome == 'success') - run: tox -qe coverage$(echo '${{ matrix.python-version }}' | tr -dc '0-9')-html - - - name: Upload HTML Coverage Report - if: always() && steps.coverage.outcome == 'failure' - uses: actions/upload-artifact@v3.1.2 - with: - name: html-coverage-report-${{ matrix.platform }}-${{ matrix.python-version }} - path: htmlcov + if: always() && contains('success,failure', steps.test.outcome) + # coverage reporting must use the same Python version used to produce coverage + run: tox -qe coverage$(echo '${{ matrix.python-version }}' | tr -dc '0-9') coverage: name: Project coverage runs-on: ubuntu-latest needs: unit-tests - if: always() && (needs.unit-tests.result == 'success' || needs.unit-tests.result == 'failure') + if: always() && contains('success,failure', needs.unit-tests.result) steps: - name: Checkout uses: actions/checkout@v3.5.2 @@ -143,55 +137,16 @@ jobs: with: name: coverage-data - - name: Linux Coverage Report - id: linux-coverage - if: success() || failure() - env: - COVERAGE_FILE: ".coverage.ubuntu" - COVERAGE_PLATFORM: "linux" - run: tox -qe coverage-platform-html - - - name: Upload Linux Coverage HTML Report - if: always() && steps.linux-coverage.outcome == 'failure' - uses: actions/upload-artifact@v3.1.2 - with: - name: html-coverage-report-Linux - path: htmlcov - - - name: macOS Coverage Report - id: macos-coverage - if: success() || failure() - env: - COVERAGE_FILE: ".coverage.macos" - COVERAGE_PLATFORM: "darwin" - run: tox -qe coverage-platform-html - - - name: Upload macOS Coverage HTML Report - if: always() && steps.macos-coverage.outcome == 'failure' - uses: actions/upload-artifact@v3.1.2 - with: - name: html-coverage-report-macOS - path: htmlcov - - - name: Windows Coverage Report - id: windows-coverage - if: success() || failure() - env: - COVERAGE_FILE: ".coverage.windows" - COVERAGE_PLATFORM: "win32" - run: tox -qe coverage-platform-html - - - name: Upload Windows Coverage HTML Report - if: always() && steps.windows-coverage.outcome == 'failure' - uses: actions/upload-artifact@v3.1.2 - with: - name: html-coverage-report-Windows - path: htmlcov + - name: Platform Coverage Reports + id: platform-coverage + run: > + tox p --parallel-no-spinner -qe + coverage-ci-platform-linux,coverage-ci-platform-macos,coverage-ci-platform-windows - name: Project Coverage Report id: project-coverage - if: success() || failure() - run: tox -qe coverage-project-html + if: always() || contains('success,failure', needs.platform-coverage.result) + run: tox -qe coverage-ci-project-html - name: Upload Project Coverage HTML Report if: always() && steps.project-coverage.outcome == 'failure' diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index 671b6a50c..79657462f 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -307,14 +307,14 @@ def install(self): # Zip file no longer needed once unpacked. cmdline_tools_zip_path.unlink() - # fmt: off # Python zip unpacking ignores permission metadata. # On non-Windows, we manually fix permissions. - if self.tools.host_os != "Windows": # pragma: no branch no-cover-if-is-windows + if ( # pragma: no branch + self.tools.host_os != "Windows" + ): # pragma: no-cover-if-is-windows for binpath in (self.cmdline_tools_path / "bin").glob("*"): if not self.tools.os.access(binpath, self.tools.os.X_OK): binpath.chmod(0o755) - # fmt: on # Licences must be accepted. self.verify_license() diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py index faced453e..cfddf96f0 100644 --- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py +++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py @@ -209,7 +209,7 @@ def test_download_sdk(mock_tools, tmp_path): assert sdk.cmdline_tools_path.is_dir() assert sdk.cmdline_tools_version_path.is_file() - if platform.system() == "Windows": + if platform.system() != "Windows": # On non-Windows, ensure the unpacked binary was made executable assert os.access( cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", diff --git a/tox.ini b/tox.ini index 8916645a4..4616c0a24 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ commands = !fast : python -m coverage run -m pytest {posargs:-vv --color yes} fast : python -m pytest {posargs:-vv --color yes -n auto} -[testenv:coverage{,38,39,310,311,312}{,-platform}{,-project}{,-html}{,-keep}{,-nofail}] +[testenv:coverage{,38,39,310,311,312}{,-ci}{,-platform,-platform-linux,-platform-macos,-platform-windows,-project}{,-keep}{,-html}] depends = py{,38,39,310,311,312} # by default, coverage should run on oldest supported Python for testing platform coverage. # however, coverage for a particular Python version should match the version used for pytest. @@ -47,21 +47,26 @@ base_python = extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} -passenv = - COVERAGE_FILE - COVERAGE_PLATFORM +passenv = COVERAGE_FILE setenv = keep: COMBINE_FLAGS = --keep - !nofail: REPORT_FLAGS = --fail-under=100 - # disable coverage exclusions for Python version to test entire platform + # spoof platform for conditional coverage exclusions + platform-linux: COVERAGE_PLATFORM = linux + platform-macos: COVERAGE_PLATFORM = darwin + platform-windows: COVERAGE_PLATFORM = win32 + # use the coverage files created in CI for individual platforms + ci-platform-linux: COVERAGE_FILE = .coverage.ubuntu + ci-platform-macos: COVERAGE_FILE = .coverage.macos + ci-platform-windows: COVERAGE_FILE = .coverage.windows + # disable conditional coverage exclusions for Python version to test entire platform {platform,project}: COVERAGE_EXCLUDE_PYTHON_VERSION=disable - # disable coverage exclusions for host platform to test entire project + # disable conditional coverage exclusions for host platform to test entire project project: COVERAGE_EXCLUDE_PLATFORM=disable commands_pre = python --version commands = -python -m coverage combine {env:COMBINE_FLAGS} html: python -m coverage html --skip-covered --skip-empty - python -m coverage report {env:REPORT_FLAGS} + python -m coverage report --fail-under=100 [testenv:towncrier{,-check}] skip_install = True @@ -99,7 +104,6 @@ commands = all : python -m sphinx {[docs]sphinx_args_extra} -b html . {[docs]build_dir}/html [testenv:package] -package_env = none skip_install = True passenv = FORCE_COLOR deps = From ad86f29b98e6eec590e25d62d39baaad70398352 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Wed, 10 May 2023 19:26:44 -0400 Subject: [PATCH 11/14] Add conditional coverage details to contributing docs --- docs/how-to/contribute-code.rst | 320 ++++++++++++++++++++++++------- docs/how-to/internal/release.rst | 2 +- docs/spelling_wordlist | 3 +- tox.ini | 2 +- 4 files changed, 255 insertions(+), 72 deletions(-) diff --git a/docs/how-to/contribute-code.rst b/docs/how-to/contribute-code.rst index 4ecd0bccb..3f65fc86f 100644 --- a/docs/how-to/contribute-code.rst +++ b/docs/how-to/contribute-code.rst @@ -17,6 +17,9 @@ The recommended way of setting up your development environment for Briefcase is to use a `virtual environment `__, and then install the development version of Briefcase and its dependencies: +Clone Briefcase and create virtual environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + .. tabs:: .. group-tab:: macOS @@ -49,6 +52,9 @@ and then install the development version of Briefcase and its dependencies: C:\...>venv\Scripts\activate (venv) C:\...>python3 -m pip install -Ue .[dev] +Install pre-commit +^^^^^^^^^^^^^^^^^^ + Briefcase uses a tool called `pre-commit `__ to identify simple issues and standardize code formatting. It does this by installing a git hook that automatically runs a series of code linters prior to finalizing any @@ -77,9 +83,14 @@ git commit. To enable pre-commit, run: (venv) C:\...>pre-commit install pre-commit installed at .git/hooks/pre-commit -When you commit any change, pre-commit will run automatically. If there are any -issues found with the commit, this will cause your commit to fail. Where possible, -pre-commit will make the changes needed to correct the problems it has found: +Pre-commit automatically runs during the commit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With pre-commit installed as a git hook for verifying commits, the pre-commit +hooks configured in ``.pre-commit-config.yaml`` for Briefcase must all pass +before the commit is successful. If there are any issues found with the commit, +this will cause your commit to fail. Where possible, pre-commit will make the +changes needed to correct the problems it has found: .. tabs:: @@ -89,6 +100,15 @@ pre-commit will make the changes needed to correct the problems it has found: (venv) $ git add some/interesting_file.py (venv) $ git commit -m "Minor change" + check toml...........................................(no files to check)Skipped + check yaml...........................................(no files to check)Skipped + check for case conflicts.................................................Passed + check docstring is first.................................................Passed + fix end of files.........................................................Passed + trim trailing whitespace.................................................Passed + isort....................................................................Passed + pyupgrade................................................................Passed + docformatter.............................................................Passed black....................................................................Failed - hook id: black - files were modified by this hook @@ -99,6 +119,14 @@ pre-commit will make the changes needed to correct the problems it has found: 1 file reformatted. flake8...................................................................Passed + + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ git add some/interesting_file.py + (venv) $ git commit -m "Minor change" check toml...........................................(no files to check)Skipped check yaml...........................................(no files to check)Skipped check for case conflicts.................................................Passed @@ -108,13 +136,6 @@ pre-commit will make the changes needed to correct the problems it has found: isort....................................................................Passed pyupgrade................................................................Passed docformatter.............................................................Passed - - .. group-tab:: Linux - - .. code-block:: console - - (venv) $ git add some/interesting_file.py - (venv) $ git commit -m "Minor change" black....................................................................Failed - hook id: black - files were modified by this hook @@ -125,6 +146,13 @@ pre-commit will make the changes needed to correct the problems it has found: 1 file reformatted. flake8...................................................................Passed + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>git add some/interesting_file.py + (venv) C:\...>git commit -m "Minor change" check toml...........................................(no files to check)Skipped check yaml...........................................(no files to check)Skipped check for case conflicts.................................................Passed @@ -134,32 +162,16 @@ pre-commit will make the changes needed to correct the problems it has found: isort....................................................................Passed pyupgrade................................................................Passed docformatter.............................................................Passed - - .. group-tab:: Windows - - .. code-block:: doscon - - (venv) C:\...>git add some/interesting_file.py - (venv) C:\...>git commit -m "Minor change" black....................................................................Failed - hook id: black - files were modified by this hook - reformatted some\interesting_file.py + reformatted some/interesting_file.py All done! ✨ 🍰 ✨ 1 file reformatted. flake8...................................................................Passed - check toml...........................................(no files to check)Skipped - check yaml...........................................(no files to check)Skipped - check for case conflicts.................................................Passed - check docstring is first.................................................Passed - fix end of files.........................................................Passed - trim trailing whitespace.................................................Passed - isort....................................................................Passed - pyupgrade................................................................Passed - docformatter.............................................................Passed You can then re-add any files that were modified as a result of the pre-commit checks, and re-commit the change. @@ -172,8 +184,6 @@ and re-commit the change. (venv) $ git add some/interesting_file.py (venv) $ git commit -m "Minor change" - black....................................................................Passed - flake8...................................................................Passed check toml...........................................(no files to check)Skipped check yaml...........................................(no files to check)Skipped check for case conflicts.................................................Passed @@ -183,8 +193,11 @@ and re-commit the change. isort....................................................................Passed pyupgrade................................................................Passed docformatter.............................................................Passed - [bugfix e3e0f73] Minor change - 1 file changed, 4 insertions(+), 2 deletions(-) + black....................................................................Passed + flake8...................................................................Passed + [bugfix daedd37a] Minor change + 1 file changed, 2 insertions(+) + create mode 100644 some/interesting_file.py .. group-tab:: Linux @@ -192,8 +205,6 @@ and re-commit the change. (venv) $ git add some/interesting_file.py (venv) $ git commit -m "Minor change" - black....................................................................Passed - flake8...................................................................Passed check toml...........................................(no files to check)Skipped check yaml...........................................(no files to check)Skipped check for case conflicts.................................................Passed @@ -203,8 +214,11 @@ and re-commit the change. isort....................................................................Passed pyupgrade................................................................Passed docformatter.............................................................Passed - [bugfix e3e0f73] Minor change - 1 file changed, 4 insertions(+), 2 deletions(-) + black....................................................................Passed + flake8...................................................................Passed + [bugfix daedd37a] Minor change + 1 file changed, 2 insertions(+) + create mode 100644 some/interesting_file.py .. group-tab:: Windows @@ -212,8 +226,6 @@ and re-commit the change. (venv) C:\...>git add some\interesting_file.py (venv) C:\...>git commit -m "Minor change" - black....................................................................Passed - flake8...................................................................Passed check toml...........................................(no files to check)Skipped check yaml...........................................(no files to check)Skipped check for case conflicts.................................................Passed @@ -223,10 +235,31 @@ and re-commit the change. isort....................................................................Passed pyupgrade................................................................Passed docformatter.............................................................Passed + black....................................................................Passed + flake8...................................................................Passed + [bugfix daedd37a] Minor change + 1 file changed, 2 insertions(+) + create mode 100644 some/interesting_file.py + +Running tests and coverage +-------------------------- + +Briefcase uses `tox `__ to manage the testing +process and `pytest `__ for its own test +suite. + +The default ``tox`` command includes running: + * pre-commit hooks + * towncrier release note check + * documentation linting + * test suite for available Python versions + * code coverage reporting + +.. note:: -Briefcase uses `pytest `__ for its own test -suite. It uses `tox `__ to manage the testing -process. To set up a testing environment and run the full test suite: + The argument of ``p`` for the ``tox`` command is short-hand for + ``run-parallel``. As that implies, tox runs the checks concurrently and + only shows the output/errors from checks that fail. .. tabs:: @@ -248,10 +281,19 @@ process. To set up a testing environment and run the full test suite: (venv) C:\...>tox p -By default this will run the test suite multiple times, once on each Python -version supported by Briefcase, as well as running some pre-commit checks of -code style and validity. This can take a while, so if you want to speed up the -process while developing, you can run the tests on one Python version only: +Run tests for multiple versions of Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, many of the ``tox`` commands will attempt to run the test suite +multiple times, once for each Python version supported by Briefcase. To do +this, though, each of the Python versions must be installed on your machine +and available to tox's Python `discovery +`__ +process. In general, if a version of Python is available via ``PATH``, then +tox should be able to find and use it. + +Run only the test suite +^^^^^^^^^^^^^^^^^^^^^^^ .. tabs:: @@ -273,7 +315,8 @@ process while developing, you can run the tests on one Python version only: (venv) C:\...>tox -e py -Or, you can run a single test file on a single version of Python: +Run the test suite for specific files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. tabs:: @@ -281,21 +324,22 @@ Or, you can run a single test file on a single version of Python: .. code-block:: console - (venv) $ tox -e py -- tests/path_to_test_file/test_some_test.py + (venv) $ tox -e py -- tests/path/to/test_some_test.py .. group-tab:: Linux .. code-block:: console - (venv) $ tox -e py -- tests/path_to_test_file/test_some_test.py + (venv) $ tox -e py -- tests/path/to/test_some_test.py .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>tox -e py -- tests/path_to_test_file/test_some_test.py + (venv) C:\...>tox -e py -- tests/path/to/test_some_test.py -Or, to run using a specific version of Python, e.g. when you want to use Python 3.10: +Run the test suite for a specific Python version +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. tabs:: @@ -317,22 +361,52 @@ Or, to run using a specific version of Python, e.g. when you want to use Python (venv) C:\...>tox -e py310 -substituting the version number that you want to target. +Run the test suite without coverage (fast) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This will run the test suite in multiple processes and can be dramatically +faster. This mode does not produce coverage files due to complexities in +capturing coverage within spawned processes. + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -e py-fast + + .. group-tab:: Linux + + .. code-block:: console -If you just want to run pytest without generating coverage, then use ``py-fast`` -or ``py310-fast`` for the environment instead of ``py`` or ``py310``. + (venv) $ tox -e py-fast -You can also specify the ``towncrier-check``, ``docs`` or ``package`` targets -to check release notes, documentation syntax and packaging metadata, respectively. + .. group-tab:: Windows + + .. code-block:: doscon -Verify test coverage --------------------- + (venv) C:\...>tox -e py-fast -Briefcase maintains 100% branch coverage in its codebase. When you add or modify -code in the project, you must add test code to ensure coverage of any +Understanding conditional coverage +---------------------------------- + +Briefcase maintains 100% branch coverage in its codebase. When you add or +modify code in the project, you must add test code to ensure coverage of any changes you make. -After running the test suite, generate a coverage report by running: +Given, though, that Briefcase targets macOS, Linux, and Windows, as well as +multiple versions of Python, full coverage cannot be verified on a single +platform. To accommodate this, several conditional coverage rules are defined +in ``pyproject.toml``, such as ``no-cover-if-is-windows``, and used in the +project to identify sections of code that are only covered on particular +platforms. + +Coverage report for host platform and Python version +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After running the test suite, you can generate a coverage report that ignores +missing coverage for code for other platforms or version of Python by running: .. tabs:: @@ -354,18 +428,101 @@ After running the test suite, generate a coverage report by running: (venv) C:\...>tox -e coverage -You can run the test suite and coverage together by including the testing -environment to run, e.g. ``py,coverage`` or ``py310,coverage``. +To run the test suite along with this coverage reporting, run: -Additionally, an HTML coverage report can be generated by using -``py,coverage-html`` or ``py310,coverage-html``. +.. tabs:: -Depending on your platform, it's possible that some lines required by other -platforms will be skipped and shown as "missing" in the coverage report. You -can safely ignore those lines. However, make sure that any lines of code that -you added or modified do not appear in the report. If they do, you need to add -new tests that will cover those lines. Otherwise, the coverage check will fail -when you make a PR with your changes. + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox p -m test + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox p -m test + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox p -m test + +Coverage report for host platform +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If all supported versions of Python are available to tox, then coverage for the +host platform can be reported by running: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -e coverage-platform + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -e coverage-platform + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -e coverage-platform + +To run the test suite along with this coverage reporting, run: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox p -m test-platform + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox p -m test-platform + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox p -m test-platform + +Coverage reporting in HTML +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Additionally, an HTML coverage report can be generated by appending ``-html`` +to any of the coverage tox environment names, for instance: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -e coverage-platform-html + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -e coverage-platform-html + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -e coverage-platform-html Add change information for release notes ---------------------------------------- @@ -385,4 +542,29 @@ See `News Fragments for more details on the types of news fragments you can add. You can also see existing examples of news fragments in the ``changes/`` folder. +Simulating GitHub CI checks locally +----------------------------------- + +To run the same checks that run in CI for the platform, run: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox p -m ci + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox p -m ci + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox p -m ci + Now you are ready to start hacking! Have fun! diff --git a/docs/how-to/internal/release.rst b/docs/how-to/internal/release.rst index 7a7e5cb58..ecfbce125 100644 --- a/docs/how-to/internal/release.rst +++ b/docs/how-to/internal/release.rst @@ -106,7 +106,7 @@ The procedure for cutting a new release is as follows: may need to correct the build configuration, roll back and re-tag the release. #. Edit the GitHub release to add release notes. You can use the text generated - by Towncrier, but you'll need to update the format to Markdown, rather than + by towncrier, but you'll need to update the format to Markdown, rather than ReST. If necessary, check the pre-release checkbox. #. Double check everything, then click Publish. This will trigger a diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 61fd689f1..d8d162f35 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -69,8 +69,9 @@ subdirectory submodule subprocesses toml -Towncrier +towncrier TTY +tox tvOS untrusted VisualStudio diff --git a/tox.ini b/tox.ini index 4616c0a24..e383a861e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = towncrier-check,docs-lint,pre-commit,py{38,39,310,311,312},coverage labels = test = py{38,39,310,311,312},coverage test-fast = py{38,39,310,311,312}-fast - test-cov = py{38,39,310,311,312},coverage-platform + test-platform = py{38,39,310,311,312},coverage-platform ci = towncrier-check,docs-lint,pre-commit,py{38,39,310,311,312},coverage-platform skip_missing_interpreters = True From f999ba5c12ec11dd774cd81bcbfca0c0995022e6 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 11 May 2023 10:43:23 +0800 Subject: [PATCH 12/14] Tweaks to working of contribution guide. --- docs/how-to/contribute-code.rst | 76 +++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/docs/how-to/contribute-code.rst b/docs/how-to/contribute-code.rst index 3f65fc86f..62f788c2f 100644 --- a/docs/how-to/contribute-code.rst +++ b/docs/how-to/contribute-code.rst @@ -255,11 +255,7 @@ The default ``tox`` command includes running: * test suite for available Python versions * code coverage reporting -.. note:: - - The argument of ``p`` for the ``tox`` command is short-hand for - ``run-parallel``. As that implies, tox runs the checks concurrently and - only shows the output/errors from checks that fail. +To run the full test suite, run: .. tabs:: @@ -267,19 +263,25 @@ The default ``tox`` command includes running: .. code-block:: console - (venv) $ tox p + (venv) $ tox .. group-tab:: Linux .. code-block:: console - (venv) $ tox p + (venv) $ tox .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>tox p + (venv) C:\...>tox + +The full test suite can take a while to run. You can speed it up considerably by +running tox in parallel, by running ``tox p`` (or ``tox run-parallel``). When +you run the test suite in parallel, you'll get less feedback on the progress of +the test suite as it runs, but you'll still get a summary of any problems found +at the end of the test run. Run tests for multiple versions of Python ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -295,6 +297,9 @@ tox should be able to find and use it. Run only the test suite ^^^^^^^^^^^^^^^^^^^^^^^ +If you're rapidly iterating on a new feature, you don't need to run the full +test suite; you can run *just* the unit tests. To do this, run: + .. tabs:: .. group-tab:: macOS @@ -315,8 +320,16 @@ Run only the test suite (venv) C:\...>tox -e py -Run the test suite for specific files -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _test-subset: + +Run a subset of tests +^^^^^^^^^^^^^^^^^^^^^ + +By default, tox will run all tests in the unit test suite. To restrict the test +run to a subset of tests, you can pass in `any pytest specifier +`__ +as an argument to tox. For example, to run only the tests in a single file, run: .. tabs:: @@ -338,9 +351,16 @@ Run the test suite for specific files (venv) C:\...>tox -e py -- tests/path/to/test_some_test.py +.. _test-py-version: + Run the test suite for a specific Python version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +By default ``tox -e py`` will run using whatever interpreter resolves as +``python3`` on your machine. If you have multiple Python versions installed, and +want to test a specific Python version, you can specify a specific python +version to use. For example, to run the test suite on Python 3.10, run: + .. tabs:: .. group-tab:: macOS @@ -361,12 +381,16 @@ Run the test suite for a specific Python version (venv) C:\...>tox -e py310 +A :ref:`subset of tests ` can be run by adding ``--`` and a test +specification to the command line. + Run the test suite without coverage (fast) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This will run the test suite in multiple processes and can be dramatically -faster. This mode does not produce coverage files due to complexities in -capturing coverage within spawned processes. +By default, tox will run the pytest suite in single threaded mode. You can speed +up the execution of the test suite by running the test suite in parallel. This +mode does not produce coverage files due to complexities in capturing coverage +within spawned processes. To run a single python version in "fast" mode, run: .. tabs:: @@ -388,19 +412,25 @@ capturing coverage within spawned processes. (venv) C:\...>tox -e py-fast -Understanding conditional coverage ----------------------------------- +A :ref:`subset of tests ` can be run by adding ``--`` and a test +specification to the command line; a :ref:`specific Python version +` can be used by adding the version to the test target (e.g., +``py310-fast`` to run fast on Python 3.10). + +Code coverage +------------- Briefcase maintains 100% branch coverage in its codebase. When you add or modify code in the project, you must add test code to ensure coverage of any changes you make. -Given, though, that Briefcase targets macOS, Linux, and Windows, as well as -multiple versions of Python, full coverage cannot be verified on a single -platform. To accommodate this, several conditional coverage rules are defined -in ``pyproject.toml``, such as ``no-cover-if-is-windows``, and used in the -project to identify sections of code that are only covered on particular -platforms. +However, Briefcase targets macOS, Linux, and Windows, as well as multiple +versions of Python, so full coverage cannot be verified on a single platform and +Python version. To accommodate this, several conditional coverage rules are +defined in the ``tool.coverage.coverage_conditional_plugin.rules`` section of +``pyproject.toml`` (e.g., ``no-cover-if-is-windows``). These rules are used to +identify sections of code that are only covered on particular platforms or +Python versions. Coverage report for host platform and Python version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -501,8 +531,8 @@ To run the test suite along with this coverage reporting, run: Coverage reporting in HTML ^^^^^^^^^^^^^^^^^^^^^^^^^^ -Additionally, an HTML coverage report can be generated by appending ``-html`` -to any of the coverage tox environment names, for instance: +A HTML coverage report can be generated by appending ``-html`` to any of the +coverage tox environment names, for instance: .. tabs:: From c1f0f5f0503d273bc607d409a1a7d6c15f6e6789 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 12 May 2023 12:54:58 -0400 Subject: [PATCH 13/14] Update contributing code docs - Add tl;dr section to top - Add tox labels to run tests and coverage for specific versions of Python - Add a section to create a new git branch for changes --- docs/how-to/contribute-code.rst | 153 +++++++++++++++++++++----------- docs/spelling_wordlist | 2 + tox.ini | 5 ++ 3 files changed, 110 insertions(+), 50 deletions(-) diff --git a/docs/how-to/contribute-code.rst b/docs/how-to/contribute-code.rst index 62f788c2f..5f3ba506c 100644 --- a/docs/how-to/contribute-code.rst +++ b/docs/how-to/contribute-code.rst @@ -8,6 +8,68 @@ to contribute code, please `fork the code`_ and `submit a pull request`_. .. _fork the code: https://github.com/beeware/briefcase .. _submit a pull request: https://github.com/beeware/briefcase/pulls +tl;dr +----- + +Set up the dev environment by running: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + $ git clone https://github.com/beeware/briefcase.git + $ cd briefcase + $ python3 -m venv venv + $ . venv/bin/activate + (venv) $ python3 -m pip install -Ue ".[dev]" + (venv) $ pre-commit install + + .. group-tab:: Linux + + .. code-block:: console + + $ git clone https://github.com/beeware/briefcase.git + $ cd briefcase + $ python3 -m venv venv + $ . venv/bin/activate + (venv) $ python3 -m pip install -Ue ".[dev]" + (venv) $ pre-commit install + + .. group-tab:: Windows + + .. code-block:: doscon + + C:\...>git clone https://github.com/beeware/briefcase.git + C:\...>cd briefcase + C:\...>py -m venv venv + C:\...>venv\Scripts\activate + (venv) C:\...>python -m pip install -Ue .[dev] + (venv) C:\...>pre-commit install + +Invoke CI checks and tests by running: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox p -m ci + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox p -m ci + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox p -m ci + .. _setup-dev-environment: Setting up your development environment @@ -50,7 +112,7 @@ Clone Briefcase and create virtual environment C:\...>cd briefcase C:\...>py -m venv venv C:\...>venv\Scripts\activate - (venv) C:\...>python3 -m pip install -Ue .[dev] + (venv) C:\...>python -m pip install -Ue .[dev] Install pre-commit ^^^^^^^^^^^^^^^^^^ @@ -241,6 +303,35 @@ and re-commit the change. 1 file changed, 2 insertions(+) create mode 100644 some/interesting_file.py +Create a new branch in git +-------------------------- + +When you clone Briefcase, it will default to checking out the default branch, +``main``. However, your changes should be committed to a new branch instead of +being committed directly in to ``main``. The branch name should be succinct but +relate to what's being changed; for instance, ``fix-dev-pip-error``. To create +a new branch, run: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ git checkout -b fix-dev-pip-error + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ git checkout -b fix-dev-pip-error + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>git checkout -b fix-dev-pip-error + Running tests and coverage -------------------------- @@ -432,11 +523,17 @@ defined in the ``tool.coverage.coverage_conditional_plugin.rules`` section of identify sections of code that are only covered on particular platforms or Python versions. +Of note, coverage reporting across Python versions can be a bit quirky. For +instance, if coverage files are produced using one version of Python but +coverage reporting is done on another, the report may include false positives +for missed branches. Because of this, coverage reporting should always use the +oldest version Python used to produce the coverage files. + Coverage report for host platform and Python version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -After running the test suite, you can generate a coverage report that ignores -missing coverage for code for other platforms or version of Python by running: +You can generate a coverage report for your platform and version of Python, for +example Python 3.11, by running: .. tabs:: @@ -444,41 +541,19 @@ missing coverage for code for other platforms or version of Python by running: .. code-block:: console - (venv) $ tox -e coverage + (venv) $ tox -m test311 .. group-tab:: Linux .. code-block:: console - (venv) $ tox -e coverage + (venv) $ tox -m test311 .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>tox -e coverage - -To run the test suite along with this coverage reporting, run: - -.. tabs:: - - .. group-tab:: macOS - - .. code-block:: console - - (venv) $ tox p -m test - - .. group-tab:: Linux - - .. code-block:: console - - (venv) $ tox p -m test - - .. group-tab:: Windows - - .. code-block:: doscon - - (venv) C:\...>tox p -m test + (venv) C:\...>tox -m test311 Coverage report for host platform ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -486,28 +561,6 @@ Coverage report for host platform If all supported versions of Python are available to tox, then coverage for the host platform can be reported by running: -.. tabs:: - - .. group-tab:: macOS - - .. code-block:: console - - (venv) $ tox -e coverage-platform - - .. group-tab:: Linux - - .. code-block:: console - - (venv) $ tox -e coverage-platform - - .. group-tab:: Windows - - .. code-block:: doscon - - (venv) C:\...>tox -e coverage-platform - -To run the test suite along with this coverage reporting, run: - .. tabs:: .. group-tab:: macOS diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index d8d162f35..28aaa975d 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -19,6 +19,7 @@ distributable Django dmg Dockerfile +dr embeddable executables Flathub @@ -68,6 +69,7 @@ subdirectories subdirectory submodule subprocesses +tl toml towncrier TTY diff --git a/tox.ini b/tox.ini index e383a861e..88da2c964 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,11 @@ envlist = towncrier-check,docs-lint,pre-commit,py{38,39,310,311,312},coverage labels = test = py{38,39,310,311,312},coverage + test38 = py38,coverage38 + test39 = py39,coverage39 + test310 = py310,coverage310 + test311 = py311,coverage311 + test312 = py312,coverage312 test-fast = py{38,39,310,311,312}-fast test-platform = py{38,39,310,311,312},coverage-platform ci = towncrier-check,docs-lint,pre-commit,py{38,39,310,311,312},coverage-platform From 968ab3d7e6dc6b1d41df6681664c9c5b9836d6de Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 13 May 2023 09:40:04 +0800 Subject: [PATCH 14/14] Final cosmetic tweaks. --- docs/how-to/contribute-code.rst | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/how-to/contribute-code.rst b/docs/how-to/contribute-code.rst index 5f3ba506c..507af9965 100644 --- a/docs/how-to/contribute-code.rst +++ b/docs/how-to/contribute-code.rst @@ -23,7 +23,7 @@ Set up the dev environment by running: $ cd briefcase $ python3 -m venv venv $ . venv/bin/activate - (venv) $ python3 -m pip install -Ue ".[dev]" + (venv) $ python -m pip install -Ue ".[dev]" (venv) $ pre-commit install .. group-tab:: Linux @@ -34,7 +34,7 @@ Set up the dev environment by running: $ cd briefcase $ python3 -m venv venv $ . venv/bin/activate - (venv) $ python3 -m pip install -Ue ".[dev]" + (venv) $ python -m pip install -Ue ".[dev]" (venv) $ pre-commit install .. group-tab:: Windows @@ -92,7 +92,7 @@ Clone Briefcase and create virtual environment $ cd briefcase $ python3 -m venv venv $ . venv/bin/activate - (venv) $ python3 -m pip install -Ue ".[dev]" + (venv) $ python -m pip install -Ue ".[dev]" .. group-tab:: Linux @@ -102,7 +102,7 @@ Clone Briefcase and create virtual environment $ cd briefcase $ python3 -m venv venv $ . venv/bin/activate - (venv) $ python3 -m pip install -Ue ".[dev]" + (venv) $ python -m pip install -Ue ".[dev]" .. group-tab:: Windows @@ -309,8 +309,9 @@ Create a new branch in git When you clone Briefcase, it will default to checking out the default branch, ``main``. However, your changes should be committed to a new branch instead of being committed directly in to ``main``. The branch name should be succinct but -relate to what's being changed; for instance, ``fix-dev-pip-error``. To create -a new branch, run: +relate to what's being changed; for instance, if you're fixing a bug in Windows +code signing, you might use the branch name ``fix-windows-signing``. To create a +new branch, run: .. tabs:: @@ -318,19 +319,19 @@ a new branch, run: .. code-block:: console - (venv) $ git checkout -b fix-dev-pip-error + (venv) $ git checkout -b fix-windows-signing .. group-tab:: Linux .. code-block:: console - (venv) $ git checkout -b fix-dev-pip-error + (venv) $ git checkout -b fix-windows-signing .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>git checkout -b fix-dev-pip-error + (venv) C:\...>git checkout -b fix-windows-signing Running tests and coverage -------------------------- @@ -519,9 +520,10 @@ However, Briefcase targets macOS, Linux, and Windows, as well as multiple versions of Python, so full coverage cannot be verified on a single platform and Python version. To accommodate this, several conditional coverage rules are defined in the ``tool.coverage.coverage_conditional_plugin.rules`` section of -``pyproject.toml`` (e.g., ``no-cover-if-is-windows``). These rules are used to -identify sections of code that are only covered on particular platforms or -Python versions. +``pyproject.toml`` (e.g., ``no-cover-if-is-windows`` can be used to flag a block +of code that won't be executed when running the test suite on Windows). These +rules are used to identify sections of code that are only covered on particular +platforms or Python versions. Of note, coverage reporting across Python versions can be a bit quirky. For instance, if coverage files are produced using one version of Python but @@ -532,8 +534,9 @@ oldest version Python used to produce the coverage files. Coverage report for host platform and Python version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You can generate a coverage report for your platform and version of Python, for -example Python 3.11, by running: +You can generate a coverage report for your platform and version of Python. For +example, to run the test suite and generate a coverage report on Python3.11, +run: .. tabs::