Repository: beeware/briefcase
Pull request #1244 (2023-04-28 01:12:56)

Add support for accurate local coverage reports
## Changes
- Explicitly test `os.altsep` for local requirements
- Ensure `os.X_OK` is unmocked for Android SDK download test
- Include test case for linux system not updating file perms
- Explicitly test `project_path()`

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct


rmartin16: @freakboy3742,
Concerning the final piece of coverage for macOS...would you like me to do anything about this? (personally, I only really wanted coverage to work on Linux...but macOS mostly came along for the ride.)

I can easily add `pragma: no cover` or go a little bigger with something like [coverage-conditional-plugin](https://github.com/wemake-services/coverage-conditional-plugin).

Patch:
diff --git a/changes/1244.misc.rst b/changes/1244.misc.rst
new file mode 100644
index 00000000..47a4c82f
--- /dev/null
+++ b/changes/1244.misc.rst
@@ -0,0 +1,1 @@
+Tests were updated for more accurate coverage on a single platform.
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index 76cd9237..f40cfdd5 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -18,9 +18,11 @@ from briefcase.integrations.xcode import (
 
 try:
     import dmgbuild
-except ImportError:
+except ImportError:  # pragma: no cover
     # 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
 
 
@@ -67,9 +69,7 @@ def macOS_log_clean_filter(line):
 class macOSMixin:
     platform = "macOS"
     supported_host_os = {"Darwin"}
-    supported_host_os_reason = (
-        "Building and / or code signing a DMG requires running on macOS."
-    )
+    supported_host_os_reason = "macOS applications can only be built on macOS."
 
 
 class macOSRunMixin:


Test patch:
diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py
index f0c6f601..5b721077 100644
--- a/tests/commands/create/test_install_app_requirements.py
+++ b/tests/commands/create/test_install_app_requirements.py
@@ -6,6 +6,7 @@ from unittest import mock
 import pytest
 import tomli_w
 
+from briefcase.commands.create import _is_local_requirement
 from briefcase.exceptions import BriefcaseCommandError, RequirementsInstallError
 from briefcase.integrations.subprocess import Subprocess
 
@@ -484,6 +485,28 @@ def test_app_requirements_requires(
     assert myapp.test_requires is None
 
 
+@pytest.mark.parametrize(
+    "altsep, requirement, expected",
+    [
+        (None, "asdf/xcvb", True),
+        (None, "asdf>xcvb", False),
+        (">", "asdf/xcvb", True),
+        (">", "asdf>xcvb", True),
+        (">", "asdf+xcvb", False),
+    ],
+)
+def test__is_local_requirement_altsep_respected(
+    altsep,
+    requirement,
+    expected,
+    monkeypatch,
+):
+    """``os.altsep`` is included as a separator when available."""
+    monkeypatch.setattr(os, "sep", "/")
+    monkeypatch.setattr(os, "altsep", altsep)
+    assert _is_local_requirement(requirement) is expected
+
+
 def _test_app_requirements_paths(
     create_command,
     myapp,
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
index 20760595..3237d13a 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
@@ -17,6 +17,7 @@ def mock_tools(mock_tools) -> ToolCache:
     mock_tools.os.environ = {}
     mock_tools.os.fsdecode = os.fsdecode
     mock_tools.os.access = os.access
+    mock_tools.os.X_OK = os.X_OK
 
     # Identify the host platform
     mock_tools._test_download_tag = {
diff --git a/tests/platforms/linux/appimage/test_mixin.py b/tests/platforms/linux/appimage/test_mixin.py
index a3e45d18..2d09a4d1 100644
--- a/tests/platforms/linux/appimage/test_mixin.py
+++ b/tests/platforms/linux/appimage/test_mixin.py
@@ -68,14 +68,16 @@ def test_docker_image_tag(create_command, first_app_config, manylinux, tag):
 
 
 def test_docker_image_tag_uppercase_name(
-    create_command, uppercase_app_config, tmp_path
+    create_command,
+    uppercase_app_config,
+    tmp_path,
 ):
     image_tag = create_command.docker_image_tag(uppercase_app_config)
 
     assert image_tag == "briefcase/com.example.first-app:appimage"
 
 
-def test_verify_linux_no_docker(create_command, tmp_path, first_app_config):
+def test_verify_linux_no_docker(create_command, first_app_config, tmp_path):
     """If Docker is disabled on Linux, the app_context is Subprocess."""
     create_command.tools.host_os = "Linux"
     create_command.use_docker = False
@@ -178,6 +180,6 @@ def test_clone_options(tmp_path):
 
     create_command = build_command.create_command
 
-    # Confirm the use_docker option has been cloned.
+    # Confirm the use_docker option has been cloned
     assert create_command.is_clone
     assert create_command.use_docker
diff --git a/tests/platforms/linux/flatpak/test_mixin.py b/tests/platforms/linux/flatpak/test_mixin.py
index 687f577e..379534c9 100644
--- a/tests/platforms/linux/flatpak/test_mixin.py
+++ b/tests/platforms/linux/flatpak/test_mixin.py
@@ -35,6 +35,15 @@ def test_binary_path(create_command, first_app_config, tmp_path):
     assert binary_path == expected_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" / "flatpak"
+    assert expected_path == project_path == bundle_path
+
+
 def test_distribution_path(create_command, first_app_config, tmp_path):
     """The distribution path is a flatpak bundle."""
     # Force the architecture to something odd for test purposes.
diff --git a/tests/platforms/linux/system/conftest.py b/tests/platforms/linux/system/conftest.py
index e8d6220b..a8f44d8d 100644
--- a/tests/platforms/linux/system/conftest.py
+++ b/tests/platforms/linux/system/conftest.py
@@ -41,10 +41,14 @@ def first_app(first_app_config, tmp_path):
     (lib_dir / "app_packages" / "secondlib").mkdir(parents=True, exist_ok=True)
 
     # Create some .so files
-    # An SO file with 775 permissions
+    # An SO file with different group and world permissions
     (lib_dir / "app" / "support.so").touch()
     (lib_dir / "app" / "support.so").chmod(0o775)
 
+    # An SO file with same group and world permissions
+    (lib_dir / "app" / "support_same_perms.so").touch()
+    (lib_dir / "app" / "support_same_perms.so").chmod(0o744)
+
     # A SO file with both .so and .so.1.0 forms
     (lib_dir / "app_packages" / "firstlib" / "first.so").touch()
     (lib_dir / "app_packages" / "firstlib" / "first.so.1.0").touch()
diff --git a/tests/platforms/linux/system/test_build.py b/tests/platforms/linux/system/test_build.py
index 670f4b6c..f24cd33e 100644
--- a/tests/platforms/linux/system/test_build.py
+++ b/tests/platforms/linux/system/test_build.py
@@ -74,6 +74,8 @@ def test_build_app(build_command, first_app, tmp_path):
         os.stat(lib_dir / "app_packages" / "secondlib" / "second_a.so").st_mode & 0o777
         == 0o644
     )
+    # no perms change
+    assert os.stat(lib_dir / "app" / "support_same_perms.so").st_mode & 0o777 == 0o744
 
     # Strip has been invoked on the binary
     build_command.tools.subprocess.check_output.assert_called_once_with(
diff --git a/tests/platforms/linux/system/test_package__pkg.py b/tests/platforms/linux/system/test_package__pkg.py
index 31a09b72..9954754f 100644
--- a/tests/platforms/linux/system/test_package__pkg.py
+++ b/tests/platforms/linux/system/test_package__pkg.py
@@ -193,6 +193,7 @@ def test_pkg_package(package_command, first_app_pkg, tmp_path):
             "first-app-0.0.1/usr/lib/first-app",
             "first-app-0.0.1/usr/lib/first-app/app",
             "first-app-0.0.1/usr/lib/first-app/app/support.so",
+            "first-app-0.0.1/usr/lib/first-app/app/support_same_perms.so",
             "first-app-0.0.1/usr/lib/first-app/app_packages",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib/first.so",
@@ -289,6 +290,7 @@ def test_pkg_re_package(package_command, first_app_pkg, tmp_path):
             "first-app-0.0.1/usr/lib/first-app",
             "first-app-0.0.1/usr/lib/first-app/app",
             "first-app-0.0.1/usr/lib/first-app/app/support.so",
+            "first-app-0.0.1/usr/lib/first-app/app/support_same_perms.so",
             "first-app-0.0.1/usr/lib/first-app/app_packages",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib/first.so",
diff --git a/tests/platforms/linux/system/test_package__rpm.py b/tests/platforms/linux/system/test_package__rpm.py
index 534b9a26..3a580d8a 100644
--- a/tests/platforms/linux/system/test_package__rpm.py
+++ b/tests/platforms/linux/system/test_package__rpm.py
@@ -208,6 +208,7 @@ def test_rpm_package(package_command, first_app_rpm, tmp_path):
                 '%dir "/usr/lib/first-app"',
                 '%dir "/usr/lib/first-app/app"',
                 '"/usr/lib/first-app/app/support.so"',
+                '"/usr/lib/first-app/app/support_same_perms.so"',
                 '%dir "/usr/lib/first-app/app_packages"',
                 '%dir "/usr/lib/first-app/app_packages/firstlib"',
                 '"/usr/lib/first-app/app_packages/firstlib/first.so"',
@@ -238,6 +239,7 @@ def test_rpm_package(package_command, first_app_rpm, tmp_path):
             "first-app-0.0.1/usr/lib/first-app",
             "first-app-0.0.1/usr/lib/first-app/app",
             "first-app-0.0.1/usr/lib/first-app/app/support.so",
+            "first-app-0.0.1/usr/lib/first-app/app/support_same_perms.so",
             "first-app-0.0.1/usr/lib/first-app/app_packages",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib/first.so",
@@ -356,6 +358,7 @@ def test_rpm_re_package(package_command, first_app_rpm, tmp_path):
                 '%dir "/usr/lib/first-app"',
                 '%dir "/usr/lib/first-app/app"',
                 '"/usr/lib/first-app/app/support.so"',
+                '"/usr/lib/first-app/app/support_same_perms.so"',
                 '%dir "/usr/lib/first-app/app_packages"',
                 '%dir "/usr/lib/first-app/app_packages/firstlib"',
                 '"/usr/lib/first-app/app_packages/firstlib/first.so"',
@@ -386,6 +389,7 @@ def test_rpm_re_package(package_command, first_app_rpm, tmp_path):
             "first-app-0.0.1/usr/lib/first-app",
             "first-app-0.0.1/usr/lib/first-app/app",
             "first-app-0.0.1/usr/lib/first-app/app/support.so",
+            "first-app-0.0.1/usr/lib/first-app/app/support_same_perms.so",
             "first-app-0.0.1/usr/lib/first-app/app_packages",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib",
             "first-app-0.0.1/usr/lib/first-app/app_packages/firstlib/first.so",
@@ -525,6 +529,7 @@ def test_rpm_package_extra_requirements(package_command, first_app_rpm, tmp_path
                 '%dir "/usr/lib/first-app"',
                 '%dir "/usr/lib/first-app/app"',
                 '"/usr/lib/first-app/app/support.so"',
+                '"/usr/lib/first-app/app/support_same_perms.so"',
                 '%dir "/usr/lib/first-app/app_packages"',
                 '%dir "/usr/lib/first-app/app_packages/firstlib"',
                 '"/usr/lib/first-app/app_packages/firstlib/first.so"',
diff --git a/tests/platforms/macOS/app/test_mixin.py b/tests/platforms/macOS/app/test_mixin.py
index 5288ccfe..21cc4ad3 100644
--- a/tests/platforms/macOS/app/test_mixin.py
+++ b/tests/platforms/macOS/app/test_mixin.py
@@ -32,7 +32,7 @@ def test_unsupported_host_os(create_command, host_os):
 
     with pytest.raises(
         UnsupportedHostError,
-        match="Building and / or code signing a DMG requires running on macOS.",
+        match="macOS applications can only be built on macOS.",
     ):
         create_command()
 
@@ -52,6 +52,23 @@ def test_binary_path(create_command, first_app_config, tmp_path):
     assert binary_path == expected_path
 
 
+def test_project_path(create_command, first_app_config, tmp_path):
+    """The project path is the Contents directory."""
+    project_path = create_command.project_path(first_app_config)
+
+    expected_path = (
+        tmp_path
+        / "base_path"
+        / "build"
+        / "first-app"
+        / "macos"
+        / "app"
+        / "First App.app"
+        / "Contents"
+    )
+    assert expected_path == project_path
+
+
 def test_distribution_path_app(package_command, first_app_config, tmp_path):
     first_app_config.packaging_format = "app"
     distribution_path = package_command.distribution_path(first_app_config)
diff --git a/tests/platforms/macOS/app/test_package.py b/tests/platforms/macOS/app/test_package.py
index 70fe8d7b..06f41145 100644
--- a/tests/platforms/macOS/app/test_package.py
+++ b/tests/platforms/macOS/app/test_package.py
@@ -1,6 +1,5 @@
 import os
 import subprocess
-import sys
 from unittest import mock
 from zipfile import ZipFile
 
@@ -835,7 +834,6 @@ def test_dmg_with_missing_installer_background(
     )
 
 
-@pytest.mark.skipif(sys.platform != "darwin", reason="macOS specific test")
 def test_verify(package_command):
     """If you're on macOS, you can verify tools."""
     # Mock the existence of the command line tools
diff --git a/tests/platforms/macOS/xcode/test_mixin.py b/tests/platforms/macOS/xcode/test_mixin.py
index e995d41a..8f2079ef 100644
--- a/tests/platforms/macOS/xcode/test_mixin.py
+++ b/tests/platforms/macOS/xcode/test_mixin.py
@@ -42,3 +42,19 @@ def test_binary_path(create_command, first_app_config, tmp_path):
         / "First App.app"
     )
     assert binary_path == expected_path
+
+
+def test_project_path(create_command, first_app_config, tmp_path):
+    """The project path is the Xcode project."""
+    project_path = create_command.project_path(first_app_config)
+
+    expected_path = (
+        tmp_path
+        / "base_path"
+        / "build"
+        / "first-app"
+        / "macos"
+        / "xcode"
+        / "First App.xcodeproj"
+    )
+    assert expected_path == project_path
diff --git a/tests/platforms/windows/app/test_mixin.py b/tests/platforms/windows/app/test_mixin.py
index 53901201..a2a631f1 100644
--- a/tests/platforms/windows/app/test_mixin.py
+++ b/tests/platforms/windows/app/test_mixin.py
@@ -30,6 +30,15 @@ def test_binary_path(create_command, first_app_config, tmp_path):
     assert binary_path == expected_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" / "windows" / "app"
+    assert expected_path == project_path == bundle_path
+
+
 def test_distribution_path(create_command, first_app_config, tmp_path):
     distribution_path = create_command.distribution_path(first_app_config)
 
diff --git a/tests/platforms/windows/visualstudio/test_mixin.py b/tests/platforms/windows/visualstudio/test_mixin.py
index 6730eff8..679e0adc 100644
--- a/tests/platforms/windows/visualstudio/test_mixin.py
+++ b/tests/platforms/windows/visualstudio/test_mixin.py
@@ -44,6 +44,22 @@ 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 Visual Studio solution."""
+    project_path = create_command.project_path(first_app_config)
+
+    expected_path = (
+        tmp_path
+        / "base_path"
+        / "build"
+        / "first-app"
+        / "windows"
+        / "visualstudio"
+        / "First App.sln"
+    )
+    assert expected_path == project_path
+
+
 def test_distribution_path(create_command, first_app_config, tmp_path):
     distribution_path = create_command.distribution_path(first_app_config)