diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc8efa524..77ff7ff81 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 @@ -29,15 +32,16 @@ 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: + fail-fast: false matrix: platform: [ "macos", "ubuntu", "windows" ] python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12-dev" ] @@ -48,7 +52,7 @@ jobs: experimental: true # Run tests against the latest Windows Store Python - platform: "windows" - python-version: "winstore" + python-version: "winstore3.11" experimental: false steps: - name: Checkout @@ -57,52 +61,59 @@ 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 }} - 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 + - 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 + id: 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: always() && contains('success,failure', steps.test.outcome) 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 + - name: Report Platform Coverage + id: coverage + 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: Combine & check coverage. + name: Project coverage runs-on: ubuntu-latest needs: unit-tests + if: always() && contains('success,failure', needs.unit-tests.result) steps: - - uses: actions/checkout@v3.5.2 + - name: Checkout + uses: actions/checkout@v3.5.2 with: fetch-depth: 0 @@ -113,33 +124,39 @@ 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: 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: always() || contains('success,failure', needs.platform-coverage.result) + run: tox -qe coverage-ci-project-html - - name: Upload HTML report if check failed. - 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-coverage-report + name: html-coverage-report-project path: htmlcov verify-apps: - name: Build App + name: Build app needs: unit-tests uses: beeware/.github/.github/workflows/app-build-verify.yml@main with: diff --git a/changes/1262.misc.rst b/changes/1262.misc.rst new file mode 100644 index 000000000..66af39fb3 --- /dev/null +++ b/changes/1262.misc.rst @@ -0,0 +1 @@ +Coverage reporting for a specific versions of Python or a specific platform is now supported. diff --git a/docs/how-to/contribute-code.rst b/docs/how-to/contribute-code.rst index 4ecd0bccb..507af9965 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) $ python -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) $ python -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 @@ -17,6 +79,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 @@ -27,7 +92,7 @@ and then install the development version of Briefcase and its dependencies: $ 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 @@ -37,7 +102,7 @@ and then install the development version of Briefcase and its dependencies: $ 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 @@ -47,7 +112,10 @@ and then install the development version of Briefcase and its dependencies: 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 +^^^^^^^^^^^^^^^^^^ Briefcase uses a tool called `pre-commit `__ to identify simple issues and standardize code formatting. It does this by installing a git @@ -77,9 +145,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 +162,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 +181,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 +198,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 +208,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 +224,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 +246,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 +255,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 +267,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 +276,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 +288,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 +297,21 @@ 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 -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: +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, 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:: @@ -234,24 +319,78 @@ process. To set up a testing environment and run the full test suite: .. code-block:: console - (venv) $ tox p + (venv) $ git checkout -b fix-windows-signing .. group-tab:: Linux .. code-block:: console - (venv) $ tox p + (venv) $ git checkout -b fix-windows-signing .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>tox p + (venv) C:\...>git checkout -b fix-windows-signing + +Running tests and coverage +-------------------------- + +Briefcase uses `tox `__ to manage the testing +process and `pytest `__ for its own test +suite. -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: +The default ``tox`` command includes running: + * pre-commit hooks + * towncrier release note check + * documentation linting + * test suite for available Python versions + * code coverage reporting + +To run the full test suite, run: + +.. tabs:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox + + .. group-tab:: Windows + + .. code-block:: doscon + + (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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 +^^^^^^^^^^^^^^^^^^^^^^^ + +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:: @@ -273,7 +412,16 @@ 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: + +.. _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:: @@ -281,21 +429,29 @@ 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 + +.. _test-py-version: + +Run the test suite for a specific Python version +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Or, to run using a specific version of Python, e.g. when you want to use Python 3.10: +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:: @@ -317,22 +473,96 @@ 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. +A :ref:`subset of tests ` can be run by adding ``--`` and a test +specification to the command line. -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``. +Run the test suite without coverage (fast) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You can also specify the ``towncrier-check``, ``docs`` or ``package`` targets -to check release notes, documentation syntax and packaging metadata, respectively. +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: -Verify test coverage --------------------- +.. tabs:: + + .. group-tab:: macOS -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 + .. code-block:: console + + (venv) $ tox -e py-fast + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -e py-fast + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -e py-fast + +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. -After running the test suite, generate a coverage report by running: +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`` 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 +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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:: + + .. group-tab:: macOS + + .. code-block:: console + + (venv) $ tox -m test311 + + .. group-tab:: Linux + + .. code-block:: console + + (venv) $ tox -m test311 + + .. group-tab:: Windows + + .. code-block:: doscon + + (venv) C:\...>tox -m test311 + +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:: @@ -340,32 +570,45 @@ After running the test suite, generate a coverage report by running: .. code-block:: console - (venv) $ tox -e coverage + (venv) $ tox p -m test-platform .. group-tab:: Linux .. code-block:: console - (venv) $ tox -e coverage + (venv) $ tox p -m test-platform .. group-tab:: Windows .. code-block:: doscon - (venv) C:\...>tox -e coverage + (venv) C:\...>tox p -m test-platform + +Coverage reporting in HTML +^^^^^^^^^^^^^^^^^^^^^^^^^^ -You can run the test suite and coverage together by including the testing -environment to run, e.g. ``py,coverage`` or ``py310,coverage``. +A HTML coverage report can be generated by appending ``-html`` to any of the +coverage tox environment names, for instance: -Additionally, an HTML coverage report can be generated by using -``py,coverage-html`` or ``py310,coverage-html``. +.. tabs:: + + .. group-tab:: macOS -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. + .. 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 +628,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..28aaa975d 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -19,6 +19,7 @@ distributable Django dmg Dockerfile +dr embeddable executables Flathub @@ -68,9 +69,11 @@ subdirectories subdirectory submodule subprocesses +tl toml -Towncrier +towncrier TTY +tox tvOS untrusted VisualStudio diff --git a/pyproject.toml b/pyproject.toml index 9c1d2e449..b0ce2512e 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,17 @@ exclude_lines = [ "if TYPE_CHECKING:", ] +[tool.coverage.coverage_conditional_plugin.rules] +no-cover-if-missing-setuptools_scm = "not is_installed('setuptools_scm')" +no-cover-if-not-linux = "'linux' != os_environ.get('COVERAGE_PLATFORM', sys_platform) and os_environ.get('COVERAGE_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'" +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'" + [tool.isort] profile = "black" skip_glob = [ diff --git a/setup.cfg b/setup.cfg index f54efe81a..c1f396a65 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.3.1 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 08b37d664..d7e250bfc 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/dev.py b/src/briefcase/commands/dev.py index 07d2c1996..2ea947c90 100644 --- a/src/briefcase/commands/dev.py +++ b/src/briefcase/commands/dev.py @@ -147,8 +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": - 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/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/android_sdk.py b/src/briefcase/integrations/android_sdk.py index a05d4fc98..79657462f 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 @@ -309,7 +309,9 @@ def install(self): # Python zip unpacking ignores permission metadata. # On non-Windows, we manually fix permissions. - if self.tools.host_os != "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) 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 e1809e126..a1ded3437 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -211,7 +211,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 13342a4c5..049d7ef06 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -20,11 +20,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 @@ -173,12 +171,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/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py index 3237d13a5..cfddf96f0 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 @@ -213,7 +212,8 @@ def test_download_sdk(mock_tools, tmp_path): 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 + cmdline_tools_base_path / "latest" / "bin" / "sdkmanager", + os.X_OK, ) # The license has been accepted @@ -306,10 +306,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 fd61c8fbb..2aaaf68ee 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 @@ -832,14 +833,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..88da2c964 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,22 @@ [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 + 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 skip_missing_interpreters = True [pkgenv] # 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] @@ -18,41 +28,57 @@ 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} 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{,-html}{,-fail}] +[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} -# coverage should run on oldest supported Python -base_python = py38,py39,py310,py311 -parallel_show_output = True +# 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 extras = dev # 2023-04-22 see pkgenv ↑ download = {[pkgenv]download} +passenv = COVERAGE_FILE +setenv = + keep: COMBINE_FLAGS = --keep + # 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 conditional coverage exclusions for host platform to test entire project + project: COVERAGE_EXCLUDE_PLATFORM=disable +commands_pre = python --version 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 --fail-under=100 -[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 @@ -83,8 +109,8 @@ 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 = check_manifest build