Repository: beeware/briefcase
Pull request #1694 (2024-03-13 18:55:25)

Remove `fsdecode()` calls for `subprocess` commands
## Changes
- Removes calls to `os.fsdecode()` for command arguments for `subprocess` calls
- `subprocess` will call `fsdecode()` for `subprocess` command arguments for us
  - Windows: `subprocess` always [calls](https://github.com/python/cpython/blob/25684e71310642ffd20b45eea9b5226a1fa809a5/Lib/subprocess.py#L1460) `list2cmdlist` for Windows processes
  - POSIX: for `posix_spawn` [path](https://github.com/python/cpython/blob/7f418fb111dec325b5c9fe6f6e96076049322f02/Modules/posixmodule.c#L7489) or the `fork_exec` [path](https://github.com/python/cpython/blob/7f418fb111dec325b5c9fe6f6e96076049322f02/Modules/_posixsubprocess.c#L1066), everything has `fsdecode` called

## 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


Patch:
diff --git a/changes/1694.misc.rst b/changes/1694.misc.rst
new file mode 100644
index 00000000..db93fe0a
--- /dev/null
+++ b/changes/1694.misc.rst
@@ -0,0 +1,1 @@
+Since ``subprocess`` already calls ``fsdecode()`` for command arguments, it was removed for calls to ``subprocess`` in Briefcase.
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index 11a9a7d9..796c8a80 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -451,7 +451,7 @@ Delete {cmdline_tools_zip_path} and run briefcase again.
             # Using subprocess.run() with no I/O redirection so the user sees
             # the full output and can send input.
             self.tools.subprocess.run(
-                [os.fsdecode(self.sdkmanager_path), "--update"],
+                [self.sdkmanager_path, "--update"],
                 env=self.env,
                 check=True,
                 stream_output=False,
@@ -557,7 +557,7 @@ its output for errors.
         try:
             # check_output always writes its output to debug
             self.tools.subprocess.check_output(
-                [os.fsdecode(self.sdkmanager_path), "--list_installed"],
+                [self.sdkmanager_path, "--list_installed"],
                 env=self.env,
             )
         except subprocess.CalledProcessError as e:
@@ -593,7 +593,7 @@ before you may use those tools.
             # Using subprocess.run() with no I/O redirection so the user sees
             # the full output and can send input.
             self.tools.subprocess.run(
-                [os.fsdecode(self.sdkmanager_path), "--licenses"],
+                [self.sdkmanager_path, "--licenses"],
                 env=self.env,
                 check=True,
                 stream_output=False,
@@ -641,11 +641,7 @@ connection.
         self.tools.logger.info("Downloading the Android emulator...")
         try:
             self.tools.subprocess.run(
-                [
-                    os.fsdecode(self.sdkmanager_path),
-                    "platform-tools",
-                    "emulator",
-                ],
+                [self.sdkmanager_path, "platform-tools", "emulator"],
                 env=self.env,
                 check=True,
                 stream_output=False,
@@ -771,10 +767,7 @@ connection.
         )
         try:
             self.tools.subprocess.run(
-                [
-                    os.fsdecode(self.sdkmanager_path),
-                    system_image,
-                ],
+                [self.sdkmanager_path, system_image],
                 env=self.env,
                 check=True,
                 stream_output=False,
@@ -835,7 +828,7 @@ connection.
             # Capture `stderr` so that if the process exits with failure, the
             # stderr data is in `e.output`.
             output = self.tools.subprocess.check_output(
-                [os.fsdecode(self.emulator_path), "-list-avds"]
+                [self.emulator_path, "-list-avds"]
             ).strip()
             # AVD names are returned one per line.
             if len(output) == 0:
@@ -848,7 +841,7 @@ connection.
         """Find the devices that are attached and available to ADB."""
         try:
             output = self.tools.subprocess.check_output(
-                [os.fsdecode(self.adb_path), "devices", "-l"]
+                [self.adb_path, "devices", "-l"]
             ).strip()
             # Process the output of `adb devices -l`.
             # The first line is header information.
@@ -1197,7 +1190,7 @@ In future, you can specify this device by running:
             try:
                 self.tools.subprocess.check_output(
                     [
-                        os.fsdecode(self.avdmanager_path),
+                        self.avdmanager_path,
                         "--verbose",
                         "create",
                         "avd",
@@ -1289,13 +1282,7 @@ In future, you can specify this device by running:
             extra_args = []
 
         emulator_popen = self.tools.subprocess.Popen(
-            [
-                os.fsdecode(self.emulator_path),
-                f"@{avd}",
-                "-dns-server",
-                "8.8.8.8",
-            ]
-            + extra_args,
+            [self.emulator_path, f"@{avd}", "-dns-server", "8.8.8.8"] + extra_args,
             env=self.env,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
@@ -1479,15 +1466,7 @@ class ADB:
         # This keeps performance good in the success case.
         try:
             output = self.tools.subprocess.check_output(
-                [
-                    os.fsdecode(self.tools.android_sdk.adb_path),
-                    "-s",
-                    self.device,
-                ]
-                + [
-                    (os.fsdecode(arg) if isinstance(arg, Path) else arg)
-                    for arg in arguments
-                ],
+                [self.tools.android_sdk.adb_path, "-s", self.device] + list(arguments),
                 quiet=quiet,
             )
             # add returns status code 0 in the case of failure. The only tangible evidence
@@ -1594,7 +1573,7 @@ Activity class not found while starting app.
         # See #1425 for details.
         return self.tools.subprocess.Popen(
             [
-                os.fsdecode(self.tools.android_sdk.adb_path),
+                self.tools.android_sdk.adb_path,
                 "-s",
                 self.device,
                 "logcat",
@@ -1623,7 +1602,7 @@ Activity class not found while starting app.
             # See #1425 for details.
             self.tools.subprocess.run(
                 [
-                    os.fsdecode(self.tools.android_sdk.adb_path),
+                    self.tools.android_sdk.adb_path,
                     "-s",
                     self.device,
                     "logcat",
diff --git a/src/briefcase/integrations/java.py b/src/briefcase/integrations/java.py
index c81b652f..a12b95f7 100644
--- a/src/briefcase/integrations/java.py
+++ b/src/briefcase/integrations/java.py
@@ -77,10 +77,7 @@ class JDK(ManagedTool):
         :return: JDK release version; e.g. "17.0.X"
         """
         output = tools.subprocess.check_output(
-            [
-                os.fsdecode(Path(java_path) / "bin/javac"),
-                "-version",
-            ],
+            [Path(java_path) / "bin/javac", "-version"]
         )
         # javac's output should look like "javac 17.0.X\n"
         return output.strip("\n").split(" ")[1]
diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py
index b8118149..939cef37 100644
--- a/src/briefcase/platforms/linux/appimage.py
+++ b/src/briefcase/platforms/linux/appimage.py
@@ -1,6 +1,7 @@
+from __future__ import annotations
+
 import os
 import subprocess
-from typing import List
 
 from briefcase.commands import (
     BuildCommand,
@@ -338,11 +339,9 @@ class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
                         self.tools.linuxdeploy.file_path
                         / self.tools.linuxdeploy.file_name,
                         "--appdir",
-                        os.fsdecode(self.appdir_path(app)),
+                        self.appdir_path(app),
                         "--desktop-file",
-                        os.fsdecode(
-                            self.appdir_path(app) / f"{app.bundle_identifier}.desktop"
-                        ),
+                        self.appdir_path(app) / f"{app.bundle_identifier}.desktop",
                         "--output",
                         "appimage",
                         "-v0" if self.logger.is_deep_debug else "-v1",
@@ -370,7 +369,7 @@ class LinuxAppImageRunCommand(LinuxAppImagePassiveMixin, RunCommand):
         self,
         app: AppConfig,
         test_mode: bool,
-        passthrough: List[str],
+        passthrough: list[str],
         **kwargs,
     ):
         """Start the application.
@@ -384,7 +383,7 @@ class LinuxAppImageRunCommand(LinuxAppImagePassiveMixin, RunCommand):
 
         # Start the app in a way that lets us stream the logs
         app_popen = self.tools.subprocess.Popen(
-            [os.fsdecode(self.binary_path(app))] + passthrough,
+            [self.binary_path(app)] + passthrough,
             cwd=self.tools.home_path,
             **kwargs,
             stdout=subprocess.PIPE,
diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py
index b529a3bc..b05ca0da 100644
--- a/src/briefcase/platforms/linux/system.py
+++ b/src/briefcase/platforms/linux/system.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import gzip
-import os
 import re
 import subprocess
 import sys
@@ -807,7 +806,7 @@ class LinuxSystemRunCommand(LinuxSystemMixin, RunCommand):
         with self.tools[app].app_context.run_app_context(kwargs) as kwargs:
             # Start the app in a way that lets us stream the logs
             app_popen = self.tools[app].app_context.Popen(
-                [os.fsdecode(self.binary_path(app))] + passthrough,
+                [self.binary_path(app)] + passthrough,
                 cwd=self.tools.home_path,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.STDOUT,
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index 3bdd19c0..d012ce50 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -251,11 +251,8 @@ class macOSRunMixin:
 
             # Start the app in a way that lets us stream the logs
             self.tools.subprocess.run(
-                [
-                    "open",
-                    "-n",  # Force a new app to be launched
-                    os.fsdecode(self.binary_path(app)),
-                ]
+                # Force a new app to be launched
+                ["open", "-n", self.binary_path(app)]
                 + ((["--args"] + passthrough) if passthrough else []),
                 cwd=self.tools.home_path,
                 check=True,
@@ -394,16 +391,11 @@ or
         :param entitlements: The path to the entitlements file to use.
         """
         options = "runtime" if identity != "-" else None
-        process_command = [
-            "codesign",
-            os.fsdecode(path),
-            "--sign",
-            identity,
-            "--force",
-        ]
+        process_command = ["codesign", path, "--sign", identity, "--force"]
+
         if entitlements:
             process_command.append("--entitlements")
-            process_command.append(os.fsdecode(entitlements))
+            process_command.append(entitlements)
         if options:
             process_command.append("--options")
             process_command.append(options)
@@ -676,7 +668,7 @@ password:
                             "xcrun",
                             "notarytool",
                             "submit",
-                            os.fsdecode(archive_filename),
+                            archive_filename,
                             "--keychain-profile",
                             profile,
                             "--wait",
@@ -707,12 +699,7 @@ password:
                 f"Stapling notarization onto {filename.relative_to(self.base_path)}..."
             )
             self.tools.subprocess.run(
-                [
-                    "xcrun",
-                    "stapler",
-                    "staple",
-                    os.fsdecode(filename),
-                ],
+                ["xcrun", "stapler", "staple", filename],
                 check=True,
             )
         except subprocess.CalledProcessError:
diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py
index 173b5306..c4ca1374 100644
--- a/src/briefcase/platforms/windows/__init__.py
+++ b/src/briefcase/platforms/windows/__init__.py
@@ -1,4 +1,3 @@
-import os
 import re
 import subprocess
 import uuid
@@ -141,7 +140,7 @@ class WindowsRunCommand(RunCommand):
 
         # Start the app in a way that lets us stream the logs
         app_popen = self.tools.subprocess.Popen(
-            [os.fsdecode(self.binary_path(app))] + passthrough,
+            [self.binary_path(app)] + passthrough,
             cwd=self.tools.home_path,
             encoding="UTF-8",
             stdout=subprocess.PIPE,
@@ -367,7 +366,7 @@ class WindowsPackageCommand(PackageCommand):
                     [
                         self.tools.wix.heat_exe,
                         "dir",
-                        os.fsdecode(self.packaging_root),
+                        self.packaging_root,
                         "-nologo",  # Don't display startup text
                         "-gg",  # Generate GUIDs
                         "-sfrag",  # Suppress fragment generation for directories


Test patch:
diff --git a/tests/commands/build/conftest.py b/tests/commands/build/conftest.py
index 43be1ccd..af40d3f7 100644
--- a/tests/commands/build/conftest.py
+++ b/tests/commands/build/conftest.py
@@ -4,7 +4,6 @@ from briefcase.commands import BuildCommand
 from briefcase.commands.base import full_options
 from briefcase.config import AppConfig
 from briefcase.console import Console, Log
-
 from ...utils import create_file
 
 
diff --git a/tests/integrations/android_sdk/ADB/test_kill.py b/tests/integrations/android_sdk/ADB/test_kill.py
index 81b15b83..b718b595 100644
--- a/tests/integrations/android_sdk/ADB/test_kill.py
+++ b/tests/integrations/android_sdk/ADB/test_kill.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from unittest.mock import MagicMock
 
@@ -15,7 +14,7 @@ def test_kill(mock_tools, adb):
     # Validate call parameters.
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(mock_tools.android_sdk.adb_path),
+            mock_tools.android_sdk.adb_path,
             "-s",
             "exampleDevice",
             "emu",
diff --git a/tests/integrations/android_sdk/ADB/test_logcat.py b/tests/integrations/android_sdk/ADB/test_logcat.py
index 9992e24e..7870dbaf 100644
--- a/tests/integrations/android_sdk/ADB/test_logcat.py
+++ b/tests/integrations/android_sdk/ADB/test_logcat.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from unittest import mock
 
@@ -25,7 +24,7 @@ def test_logcat(mock_tools, adb, is_color_enabled, monkeypatch):
     # Validate call parameters.
     mock_tools.subprocess.Popen.assert_called_once_with(
         [
-            os.fsdecode(mock_tools.android_sdk.adb_path),
+            mock_tools.android_sdk.adb_path,
             "-s",
             "exampleDevice",
             "logcat",
diff --git a/tests/integrations/android_sdk/ADB/test_logcat_tail.py b/tests/integrations/android_sdk/ADB/test_logcat_tail.py
index 07eacd6d..51ad2e12 100644
--- a/tests/integrations/android_sdk/ADB/test_logcat_tail.py
+++ b/tests/integrations/android_sdk/ADB/test_logcat_tail.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from datetime import datetime
 from unittest.mock import MagicMock, PropertyMock
@@ -24,7 +23,7 @@ def test_logcat_tail(mock_tools, adb, is_color_enabled, monkeypatch):
     # Validate call parameters.
     mock_tools.subprocess.run.assert_called_once_with(
         [
-            os.fsdecode(mock_tools.android_sdk.adb_path),
+            mock_tools.android_sdk.adb_path,
             "-s",
             "exampleDevice",
             "logcat",
diff --git a/tests/integrations/android_sdk/ADB/test_run.py b/tests/integrations/android_sdk/ADB/test_run.py
index c3cf7106..06333368 100644
--- a/tests/integrations/android_sdk/ADB/test_run.py
+++ b/tests/integrations/android_sdk/ADB/test_run.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 import sys
 from pathlib import Path
@@ -15,12 +14,8 @@ def test_simple_command(mock_tools, adb, tmp_path):
     # Check that adb was invoked with the expected commands
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "sdk"
-                / "platform-tools"
-                / f"adb{'.exe' if sys.platform == 'win32' else ''}"
-            ),
+            tmp_path
+            / f"sdk/platform-tools/adb{'.exe' if sys.platform == 'win32' else ''}",
             "-s",
             "exampleDevice",
             "example",
@@ -37,12 +32,8 @@ def test_quiet_command(mock_tools, adb, tmp_path):
     # Check that adb was invoked with the expected commands
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "sdk"
-                / "platform-tools"
-                / f"adb{'.exe' if sys.platform == 'win32' else ''}"
-            ),
+            tmp_path
+            / f"sdk/platform-tools/adb{'.exe' if sys.platform == 'win32' else ''}",
             "-s",
             "exampleDevice",
             "example",
@@ -86,12 +77,8 @@ def test_error_handling(mock_tools, adb, name, exception, tmp_path):
     # Check that adb was invoked as expected
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "sdk"
-                / "platform-tools"
-                / f"adb{'.exe' if sys.platform == 'win32' else ''}"
-            ),
+            tmp_path
+            / f"sdk/platform-tools/adb{'.exe' if sys.platform == 'win32' else ''}",
             "-s",
             "exampleDevice",
             "example",
diff --git a/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py
index 4c864be5..9125dda6 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from unittest.mock import ANY, MagicMock
 
@@ -88,7 +87,7 @@ def test_create_emulator(
     # avdmanager was invoked
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.avdmanager_path),
+            android_sdk.avdmanager_path,
             "--verbose",
             "create",
             "avd",
@@ -161,7 +160,7 @@ def test_create_emulator_with_defaults(
     # avdmanager was invoked
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.avdmanager_path),
+            android_sdk.avdmanager_path,
             "--verbose",
             "create",
             "avd",
@@ -208,7 +207,7 @@ def test_create_failure(mock_tools, android_sdk):
     # avdmanager was invoked
     mock_tools.subprocess.check_output.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.avdmanager_path),
+            android_sdk.avdmanager_path,
             "--verbose",
             "create",
             "avd",
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_list_packages.py b/tests/integrations/android_sdk/AndroidSDK/test_list_packages.py
index dbeb5734..731a756b 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_list_packages.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_list_packages.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 
 import pytest
@@ -11,7 +10,7 @@ def test_list_packages(mock_tools, android_sdk):
     android_sdk.list_packages()
 
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--list_installed"],
+        [android_sdk.sdkmanager_path, "--list_installed"],
         env=android_sdk.env,
     )
 
@@ -25,6 +24,6 @@ def test_list_packages_failure(mock_tools, android_sdk):
         android_sdk.list_packages()
 
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--list_installed"],
+        [android_sdk.sdkmanager_path, "--list_installed"],
         env=android_sdk.env,
     )
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
index a0339daf..3dacb259 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from unittest.mock import MagicMock, call
 
@@ -109,7 +108,7 @@ def test_start_emulator(mock_tools, android_sdk):
     # The process was started.
     mock_tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(android_sdk.emulator_path),
+            android_sdk.emulator_path,
             "@idleEmulator",
             "-dns-server",
             "8.8.8.8",
@@ -190,7 +189,7 @@ def test_start_emulator_fast_start(mock_tools, android_sdk):
     # The process was started.
     mock_tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(android_sdk.emulator_path),
+            android_sdk.emulator_path,
             "@idleEmulator",
             "-dns-server",
             "8.8.8.8",
@@ -285,7 +284,7 @@ def test_emulator_fail_to_start(mock_tools, android_sdk):
     # The process was started.
     mock_tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(android_sdk.emulator_path),
+            android_sdk.emulator_path,
             "@idleEmulator",
             "-dns-server",
             "8.8.8.8",
@@ -389,7 +388,7 @@ def test_emulator_fail_to_boot(mock_tools, android_sdk):
     # The process was started.
     mock_tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(android_sdk.emulator_path),
+            android_sdk.emulator_path,
             "@idleEmulator",
             "-dns-server",
             "8.8.8.8",
@@ -458,7 +457,7 @@ def test_emulator_ctrl_c(mock_tools, android_sdk, emulator_comm, expected_log, c
     # The process was started.
     mock_tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(android_sdk.emulator_path),
+            android_sdk.emulator_path,
             "@idleEmulator",
             "-dns-server",
             "8.8.8.8",
@@ -524,7 +523,7 @@ def test_start_emulator_extra_args(mock_tools, android_sdk):
     # The process was started with the headless test options.
     mock_tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(android_sdk.emulator_path),
+            android_sdk.emulator_path,
             "@idleEmulator",
             "-dns-server",
             "8.8.8.8",
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py b/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py
index ddb59759..678f8c6a 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 
 import pytest
@@ -11,7 +10,7 @@ def test_upgrade(mock_tools, android_sdk):
     android_sdk.upgrade()
 
     mock_tools.subprocess.run.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--update"],
+        [android_sdk.sdkmanager_path, "--update"],
         env=android_sdk.env,
         check=True,
         stream_output=False,
@@ -25,7 +24,7 @@ def test_upgrade_failure(mock_tools, android_sdk):
         android_sdk.upgrade()
 
     mock_tools.subprocess.run.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--update"],
+        [android_sdk.sdkmanager_path, "--update"],
         env=android_sdk.env,
         check=True,
         stream_output=False,
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
index 27c6f9f4..c65cfa55 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 import sys
 
@@ -62,7 +61,7 @@ def test_installs_android_emulator(mock_tools, android_sdk):
 
     mock_tools.subprocess.run.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.sdkmanager_path),
+            android_sdk.sdkmanager_path,
             "platform-tools",
             "emulator",
         ],
@@ -84,7 +83,7 @@ def test_partial_android_emulator_install(mock_tools, android_sdk):
 
     mock_tools.subprocess.run.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.sdkmanager_path),
+            android_sdk.sdkmanager_path,
             "platform-tools",
             "emulator",
         ],
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py b/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py
index 60e97f3a..0fd4eb73 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 
 import pytest
@@ -35,7 +34,7 @@ def test_verify_license_prompts_for_licenses_and_exits_if_you_agree(
     mock_tools.subprocess.run.side_effect = accept_license
     android_sdk.verify_license()
     mock_tools.subprocess.run.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--licenses"],
+        [android_sdk.sdkmanager_path, "--licenses"],
         env=android_sdk.env,
         check=True,
         stream_output=False,
@@ -50,7 +49,7 @@ def test_verify_license_handles_sdkmanager_crash(mock_tools, android_sdk):
         android_sdk.verify_license()
 
     mock_tools.subprocess.run.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--licenses"],
+        [android_sdk.sdkmanager_path, "--licenses"],
         env=android_sdk.env,
         check=True,
         stream_output=False,
@@ -66,7 +65,7 @@ def test_verify_license_insists_on_agreement(mock_tools, android_sdk):
         android_sdk.verify_license()
 
     mock_tools.subprocess.run.assert_called_once_with(
-        [os.fsdecode(android_sdk.sdkmanager_path), "--licenses"],
+        [android_sdk.sdkmanager_path, "--licenses"],
         env=android_sdk.env,
         check=True,
         stream_output=False,
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify_system_image.py b/tests/integrations/android_sdk/AndroidSDK/test_verify_system_image.py
index b661e6e9..22b351d1 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify_system_image.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify_system_image.py
@@ -1,4 +1,3 @@
-import os
 import platform
 from subprocess import CalledProcessError
 
@@ -68,7 +67,7 @@ def test_incompatible_abi(mock_tools, android_sdk, capsys):
     # The system image was installed.
     mock_tools.subprocess.run.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.sdkmanager_path),
+            android_sdk.sdkmanager_path,
             "system-images;android-31;default;anything",
         ],
         env=android_sdk.env,
@@ -105,7 +104,7 @@ def test_new_system_image(mock_tools, android_sdk):
     # The system image was installed.
     mock_tools.subprocess.run.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.sdkmanager_path),
+            android_sdk.sdkmanager_path,
             "system-images;android-31;default;x86_64",
         ],
         env=android_sdk.env,
@@ -135,7 +134,7 @@ def test_problem_downloading_system_image(mock_tools, android_sdk):
     # An attempt to install the system image was made
     mock_tools.subprocess.run.assert_called_once_with(
         [
-            os.fsdecode(android_sdk.sdkmanager_path),
+            android_sdk.sdkmanager_path,
             "system-images;android-31;default;x86_64",
         ],
         env=android_sdk.env,
diff --git a/tests/integrations/java/test_JDK__verify.py b/tests/integrations/java/test_JDK__verify.py
index 8d06e451..de1a7594 100644
--- a/tests/integrations/java/test_JDK__verify.py
+++ b/tests/integrations/java/test_JDK__verify.py
@@ -73,7 +73,7 @@ def test_macos_tool_java_home(mock_tools, capsys):
     assert mock_tools.subprocess.check_output.mock_calls == [
         CALL_JAVA_HOME,
         # Second is a call to verify a valid Java version
-        mock.call([os.fsdecode(Path("/path/to/java/bin/javac")), "-version"]),
+        mock.call([Path("/path/to/java/bin/javac"), "-version"]),
     ]
 
     # No console output
@@ -132,7 +132,7 @@ def test_macos_wrong_jdk_version(mock_tools, tmp_path, capsys):
 
     assert mock_tools.subprocess.check_output.mock_calls == [
         CALL_JAVA_HOME,
-        mock.call([os.fsdecode(Path("/path/to/java/bin/javac")), "-version"]),
+        mock.call([Path("/path/to/java/bin/javac"), "-version"]),
     ]
 
     # No console output
@@ -164,7 +164,7 @@ def test_macos_invalid_jdk_path(mock_tools, tmp_path, capsys):
 
     assert mock_tools.subprocess.check_output.mock_calls == [
         CALL_JAVA_HOME,
-        mock.call([os.fsdecode(Path("/path/to/java/bin/javac")), "-version"]),
+        mock.call([Path("/path/to/java/bin/javac"), "-version"]),
     ]
 
     # No console output
@@ -192,7 +192,7 @@ def test_macos_provided_overrides_tool_java_home(mock_tools, capsys):
 
     # A single call to check output
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(Path("/path/to/java/bin/javac")), "-version"],
+        [Path("/path/to/java/bin/javac"), "-version"],
     ),
 
     # No console output
@@ -218,7 +218,7 @@ def test_valid_provided_java_home(mock_tools, capsys):
 
     # A single call to check output
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(Path("/path/to/java/bin/javac")), "-version"],
+        [Path("/path/to/java/bin/javac"), "-version"],
     ),
 
     # No console output
@@ -258,7 +258,7 @@ def test_invalid_jdk_version(mock_tools, host_os, java_home, tmp_path, capsys):
 
     # A single call was made to check javac
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(Path("/path/to/java/bin/javac")), "-version"],
+        [Path("/path/to/java/bin/javac"), "-version"],
     )
 
     # Warning is shown for invalid JAVA_HOME
@@ -305,7 +305,7 @@ def test_no_javac(mock_tools, host_os, java_home, error_type, tmp_path, capsys):
 
     # A single call was made to check javac
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(Path("/path/to/nowhere/bin/javac")), "-version"],
+        [Path("/path/to/nowhere/bin/javac"), "-version"],
     ),
 
     # Warning is shown for invalid JAVA_HOME
@@ -347,7 +347,7 @@ def test_javac_error(mock_tools, host_os, java_home, tmp_path, capsys):
 
     # A single call was made to check javac
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(Path("/path/to/nowhere/bin/javac")), "-version"],
+        [Path("/path/to/nowhere/bin/javac"), "-version"],
     ),
 
     # Warning is shown for invalid JAVA_HOME
@@ -387,7 +387,7 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
 
     # A single call was made to check javac
     mock_tools.subprocess.check_output.assert_called_once_with(
-        [os.fsdecode(Path("/path/to/nowhere/bin/javac")), "-version"],
+        [Path("/path/to/nowhere/bin/javac"), "-version"],
     ),
 
     # No console output (because Briefcase JDK exists)
diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py
index 3db1db2e..8f854a16 100644
--- a/tests/platforms/android/gradle/test_run.py
+++ b/tests/platforms/android/gradle/test_run.py
@@ -3,7 +3,6 @@ import os
 import platform
 import sys
 import time
-from os.path import normpath
 from pathlib import Path
 from unittest import mock
 
@@ -545,11 +544,12 @@ def test_log_file_extra(run_command, monkeypatch):
     run_command.tools.logger.save_log = True
     run_command.tools.logger.save_log_to_file(run_command)
 
-    sdk_manager = f"/path/to/android_sdk/cmdline-tools/{AndroidSDK.SDK_MANAGER_VER}/bin/sdkmanager"
-    if platform.system() == "Windows":
-        sdk_manager += ".bat"
+    sdk_manager = Path(
+        f"/path/to/android_sdk/cmdline-tools/{AndroidSDK.SDK_MANAGER_VER}"
+        f"/bin/sdkmanager{'.bat' if platform.system() == 'Windows' else ''}"
+    )
     run_command.tools.subprocess.check_output.assert_called_once_with(
-        [normpath(sdk_manager), "--list_installed"],
+        [sdk_manager, "--list_installed"],
         env={
             "ANDROID_HOME": str(run_command.tools.android_sdk.root_path),
             "ANDROID_SDK_ROOT": str(run_command.tools.android_sdk.root_path),
diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py
index 294c9ea8..a174f8ff 100644
--- a/tests/platforms/linux/appimage/test_run.py
+++ b/tests/platforms/linux/appimage/test_run.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from unittest import mock
 
@@ -56,10 +55,8 @@ def test_run_app(run_command, first_app_config, tmp_path):
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
-            )
+            tmp_path
+            / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
         ],
         cwd=tmp_path / "home",
         stdout=subprocess.PIPE,
@@ -92,10 +89,8 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path):
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
-            ),
+            tmp_path
+            / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
             "foo",
             "--bar",
         ],
@@ -124,10 +119,8 @@ def test_run_app_failed(run_command, first_app_config, tmp_path):
     # The run command was still invoked
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
-            )
+            tmp_path
+            / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
         ],
         cwd=tmp_path / "home",
         stdout=subprocess.PIPE,
@@ -151,10 +144,8 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
-            )
+            tmp_path
+            / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
         ],
         cwd=tmp_path / "home",
         stdout=subprocess.PIPE,
@@ -188,10 +179,8 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path):
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage"
-            ),
+            tmp_path
+            / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage",
             "foo",
             "--bar",
         ],
diff --git a/tests/platforms/macOS/app/test_package__notarize.py b/tests/platforms/macOS/app/test_package__notarize.py
index 6ca1bb28..794b83e6 100644
--- a/tests/platforms/macOS/app/test_package__notarize.py
+++ b/tests/platforms/macOS/app/test_package__notarize.py
@@ -93,7 +93,7 @@ def test_notarize_app(package_command, first_app_with_binaries, tmp_path):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(archive_path),
+                    archive_path,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -106,7 +106,7 @@ def test_notarize_app(package_command, first_app_with_binaries, tmp_path):
                     "xcrun",
                     "stapler",
                     "staple",
-                    os.fsdecode(app_path),
+                    app_path,
                 ],
                 check=True,
             ),
@@ -131,7 +131,7 @@ def test_notarize_dmg(package_command, first_app_dmg):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -144,7 +144,7 @@ def test_notarize_dmg(package_command, first_app_dmg):
                     "xcrun",
                     "stapler",
                     "staple",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                 ],
                 check=True,
             ),
@@ -192,7 +192,7 @@ def test_notarize_dmg_unknown_credentials(package_command, first_app_dmg):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -218,7 +218,7 @@ def test_notarize_dmg_unknown_credentials(package_command, first_app_dmg):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -231,7 +231,7 @@ def test_notarize_dmg_unknown_credentials(package_command, first_app_dmg):
                     "xcrun",
                     "stapler",
                     "staple",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                 ],
                 check=True,
             ),
@@ -292,7 +292,7 @@ def test_credential_storage_failure_app(
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(archive_path),
+                    archive_path,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -350,7 +350,7 @@ def test_credential_storage_failure_dmg(package_command, first_app_dmg):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -422,7 +422,7 @@ def test_credential_storage_disabled_input_app(
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(archive_path),
+                    archive_path,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -465,7 +465,7 @@ def test_credential_storage_disabled_input_dmg(package_command, first_app_dmg):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -511,7 +511,7 @@ def test_notarize_unknown_credentials_after_storage(package_command, first_app_d
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -537,7 +537,7 @@ def test_notarize_unknown_credentials_after_storage(package_command, first_app_d
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -597,7 +597,7 @@ def test_app_notarization_failure_with_credentials(
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(archive_path),
+                    archive_path,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -639,7 +639,7 @@ def test_dmg_notarization_failure_with_credentials(package_command, first_app_dm
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -679,7 +679,7 @@ def test_stapling_failure(package_command, first_app_dmg):
                     "xcrun",
                     "notarytool",
                     "submit",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                     "--keychain-profile",
                     "briefcase-macOS-DEADBEEF",
                     "--wait",
@@ -692,7 +692,7 @@ def test_stapling_failure(package_command, first_app_dmg):
                     "xcrun",
                     "stapler",
                     "staple",
-                    os.fsdecode(first_app_dmg),
+                    first_app_dmg,
                 ],
                 check=True,
             ),
diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py
index 8f6b2e16..0fe77611 100644
--- a/tests/platforms/macOS/app/test_run.py
+++ b/tests/platforms/macOS/app/test_run.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from signal import SIGTERM
 from unittest import mock
@@ -67,7 +66,7 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)],
+        ["open", "-n", bin_path],
         cwd=tmp_path / "home",
         check=True,
     )
@@ -130,7 +129,7 @@ def test_run_app_with_passthrough(
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path), "--args", "foo", "--bar"],
+        ["open", "-n", bin_path, "--args", "foo", "--bar"],
         cwd=tmp_path / "home",
         check=True,
     )
@@ -154,7 +153,7 @@ def test_run_app_failed(run_command, first_app_config, sleep_zero, tmp_path):
     """If there's a problem started the app, an exception is raised."""
     # Mock a failure opening the app
     run_command.tools.subprocess.run.side_effect = subprocess.CalledProcessError(
-        cmd=["open", "-n", os.fsdecode(run_command.binary_path(first_app_config))],
+        cmd=["open", "-n", run_command.binary_path(first_app_config)],
         returncode=1,
     )
 
@@ -180,7 +179,7 @@ def test_run_app_failed(run_command, first_app_config, sleep_zero, tmp_path):
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)],
+        ["open", "-n", bin_path],
         cwd=tmp_path / "home",
         check=True,
     )
@@ -227,7 +226,7 @@ def test_run_app_find_pid_failed(
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)],
+        ["open", "-n", bin_path],
         cwd=tmp_path / "home",
         check=True,
     )
@@ -278,7 +277,7 @@ def test_run_app_test_mode(
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)],
+        ["open", "-n", bin_path],
         cwd=tmp_path / "home",
         check=True,
         env={"BRIEFCASE_MAIN_MODULE": "tests.first_app"},
diff --git a/tests/platforms/macOS/app/test_signing.py b/tests/platforms/macOS/app/test_signing.py
index a00e2521..79e2f7c3 100644
--- a/tests/platforms/macOS/app/test_signing.py
+++ b/tests/platforms/macOS/app/test_signing.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 import sys
 from pathlib import Path
@@ -56,7 +55,7 @@ def sign_call(
     on a file."""
     args = [
         "codesign",
-        os.fsdecode(filepath),
+        filepath,
         "--sign",
         identity,
         "--force",
@@ -65,15 +64,7 @@ def sign_call(
         args.extend(
             [
                 "--entitlements",
-                os.fsdecode(
-                    tmp_path
-                    / "base_path"
-                    / "build"
-                    / "first-app"
-                    / "macos"
-                    / "app"
-                    / "Entitlements.plist"
-                ),
+                tmp_path / "base_path/build/first-app/macos/app/Entitlements.plist",
             ]
         )
     if runtime:
diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py
index 5e6649a1..dcf5fb2c 100644
--- a/tests/platforms/macOS/xcode/test_run.py
+++ b/tests/platforms/macOS/xcode/test_run.py
@@ -1,7 +1,6 @@
 # Xcode uses the same run implementation as the base app;
 # Run a basic test to ensure coverage, but fall back to
 # the app backend for exhaustive tests.
-import os
 import subprocess
 from signal import SIGTERM
 from unittest import mock
@@ -69,7 +68,7 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)],
+        ["open", "-n", bin_path],
         cwd=tmp_path / "home",
         check=True,
     )
@@ -132,7 +131,7 @@ def test_run_app_with_passthrough(
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path), "--args", "foo", "--bar"],
+        ["open", "-n", bin_path, "--args", "foo", "--bar"],
         cwd=tmp_path / "home",
         check=True,
     )
@@ -190,7 +189,7 @@ def test_run_app_test_mode(
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path)],
+        ["open", "-n", bin_path],
         cwd=tmp_path / "home",
         check=True,
         env={"BRIEFCASE_MAIN_MODULE": "tests.first_app"},
@@ -254,7 +253,7 @@ def test_run_app_test_mode_with_passthrough(
         bufsize=1,
     )
     run_command.tools.subprocess.run.assert_called_with(
-        ["open", "-n", os.fsdecode(bin_path), "--args", "foo", "--bar"],
+        ["open", "-n", bin_path, "--args", "foo", "--bar"],
         cwd=tmp_path / "home",
         check=True,
         env={"BRIEFCASE_MAIN_MODULE": "tests.first_app"},
diff --git a/tests/platforms/windows/app/test_package.py b/tests/platforms/windows/app/test_package.py
index e8b493a4..dd1c56a5 100644
--- a/tests/platforms/windows/app/test_package.py
+++ b/tests/platforms/windows/app/test_package.py
@@ -1,3 +1,4 @@
+from pathlib import Path
 from subprocess import CalledProcessError
 from unittest import mock
 from zipfile import ZipFile
@@ -213,7 +214,7 @@ def test_package_msi(package_command, first_app_config, kwargs, tmp_path):
             [
                 tmp_path / "wix/bin/heat.exe",
                 "dir",
-                "src",
+                Path("src"),
                 "-nologo",
                 "-gg",
                 "-sfrag",
@@ -389,7 +390,7 @@ def test_package_msi_with_codesigning(
             [
                 tmp_path / "wix/bin/heat.exe",
                 "dir",
-                "src",
+                Path("src"),
                 "-nologo",
                 "-gg",
                 "-sfrag",
@@ -530,14 +531,7 @@ def test_package_zip_with_codesigning(
             ]
             + additional_args
             + [
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe"
+                tmp_path / "base_path/build/first-app/windows/app/src/First App.exe",
             ],
             check=True,
         ),
@@ -595,14 +589,7 @@ def test_package_msi_failed_sign_app(package_command, first_app_config, tmp_path
                 "http://freetimestamps.com",
                 "-td",
                 "sha56",
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe",
+                tmp_path / "base_path/build/first-app/windows/app/src/First App.exe",
             ],
             check=True,
         )
@@ -628,7 +615,7 @@ def test_package_msi_failed_manifest(package_command, first_app_config, tmp_path
             [
                 tmp_path / "wix/bin/heat.exe",
                 "dir",
-                "src",
+                Path("src"),
                 "-nologo",
                 "-gg",
                 "-sfrag",
@@ -670,7 +657,7 @@ def test_package_msi_failed_compile(package_command, first_app_config, tmp_path)
             [
                 tmp_path / "wix/bin/heat.exe",
                 "dir",
-                "src",
+                Path("src"),
                 "-nologo",
                 "-gg",
                 "-sfrag",
@@ -731,7 +718,7 @@ def test_package_msi_failed_link(package_command, first_app_config, tmp_path):
             [
                 tmp_path / "wix/bin/heat.exe",
                 "dir",
-                "src",
+                Path("src"),
                 "-nologo",
                 "-gg",
                 "-sfrag",
@@ -836,14 +823,7 @@ def test_package_msi_failed_signing_msi(package_command, first_app_config, tmp_p
                 "http://freetimestamps.com",
                 "-td",
                 "sha56",
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe",
+                tmp_path / "base_path/build/first-app/windows/app/src/First App.exe",
             ],
             check=True,
         ),
@@ -852,7 +832,7 @@ def test_package_msi_failed_signing_msi(package_command, first_app_config, tmp_p
             [
                 tmp_path / "wix/bin/heat.exe",
                 "dir",
-                "src",
+                Path("src"),
                 "-nologo",
                 "-gg",
                 "-sfrag",
diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py
index b098861c..f4b97943 100644
--- a/tests/platforms/windows/app/test_run.py
+++ b/tests/platforms/windows/app/test_run.py
@@ -1,4 +1,3 @@
-import os
 import subprocess
 from unittest import mock
 
@@ -36,18 +35,7 @@ def test_run_app(run_command, first_app_config, tmp_path):
 
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
-        [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe"
-            ),
-        ],
+        [tmp_path / "base_path/build/first-app/windows/app/src/First App.exe"],
         cwd=tmp_path / "home",
         encoding="UTF-8",
         stdout=subprocess.PIPE,
@@ -80,16 +68,7 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path):
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe"
-            ),
+            tmp_path / "base_path/build/first-app/windows/app/src/First App.exe",
             "foo",
             "--bar",
         ],
@@ -119,18 +98,7 @@ def test_run_app_failed(run_command, first_app_config, tmp_path):
 
     # Popen was still invoked, though
     run_command.tools.subprocess.Popen.assert_called_with(
-        [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe"
-            ),
-        ],
+        [tmp_path / "base_path/build/first-app/windows/app/src/First App.exe"],
         cwd=tmp_path / "home",
         encoding="UTF-8",
         stdout=subprocess.PIPE,
@@ -153,18 +121,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
 
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
-        [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe"
-            ),
-        ],
+        [tmp_path / "base_path/build/first-app/windows/app/src/First App.exe"],
         cwd=tmp_path / "home",
         encoding="UTF-8",
         stdout=subprocess.PIPE,
@@ -198,16 +155,7 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p
     # The process was started
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "app"
-                / "src"
-                / "First App.exe"
-            ),
+            tmp_path / "base_path/build/first-app/windows/app/src/First App.exe",
             "foo",
             "--bar",
         ],
diff --git a/tests/platforms/windows/visualstudio/test_package.py b/tests/platforms/windows/visualstudio/test_package.py
index 9d1f01c3..65528f21 100644
--- a/tests/platforms/windows/visualstudio/test_package.py
+++ b/tests/platforms/windows/visualstudio/test_package.py
@@ -38,7 +38,7 @@ def test_package_msi(package_command, first_app_config, tmp_path):
                 [
                     tmp_path / "wix/bin/heat.exe",
                     "dir",
-                    os.fsdecode(Path("x64/Release")),
+                    Path("x64/Release"),
                     "-nologo",
                     "-gg",
                     "-sfrag",
diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py
index b9f69c16..2f18b177 100644
--- a/tests/platforms/windows/visualstudio/test_run.py
+++ b/tests/platforms/windows/visualstudio/test_run.py
@@ -1,7 +1,6 @@
 # The run command inherits most of its behavior from the common base
 # implementation. Do a surface-level verification here, but the app
 # tests provide the actual test coverage.
-import os
 import subprocess
 from unittest import mock
 
@@ -41,17 +40,8 @@ def test_run_app(run_command, first_app_config, tmp_path):
     # Popen was called
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "visualstudio"
-                / "x64"
-                / "Release"
-                / "First App.exe"
-            ),
+            tmp_path
+            / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe"
         ],
         cwd=tmp_path / "home",
         encoding="UTF-8",
@@ -86,17 +76,8 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path):
     # Popen was called
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "visualstudio"
-                / "x64"
-                / "Release"
-                / "First App.exe"
-            ),
+            tmp_path
+            / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe",
             "foo",
             "--bar",
         ],
@@ -129,17 +110,8 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
     # Popen was called
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "visualstudio"
-                / "x64"
-                / "Release"
-                / "First App.exe"
-            ),
+            tmp_path
+            / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe"
         ],
         cwd=tmp_path / "home",
         encoding="UTF-8",
@@ -175,17 +147,8 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path):
     # Popen was called
     run_command.tools.subprocess.Popen.assert_called_with(
         [
-            os.fsdecode(
-                tmp_path
-                / "base_path"
-                / "build"
-                / "first-app"
-                / "windows"
-                / "visualstudio"
-                / "x64"
-                / "Release"
-                / "First App.exe"
-            ),
+            tmp_path
+            / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe",
             "foo",
             "--bar",
         ],