Many tests that use the capsys fixture fail on Windows 11. It seems like it's because sys.__stdout__.isatty() evaluates to True when we run the tests with pytest, since the following (horrible) workaround makes everything pass:
Clone Briefcase and install it in a clean virtual environment
Run pytest
See error
Expected behavior
All tests should pass
Screenshots
No response
Environment
Operating System: Windows 11
Python version: 3.12.2
Software versions:
Briefcase: '0.3.19.dev26+g96734be6.d20240520'
Logs
[...]
_______________________________________________________________________ test_variant_with_size ________________________________________________________________________
create_command = <tests.commands.create.conftest.DummyCreateCommand object at 0x000001EBA9515C10>
tmp_path = WindowsPath('C:/Users/yngve/AppData/Local/Temp/pytest-of-yngve/pytest-9/test_variant_with_size0')
capsys = <_pytest.capture.CaptureFixture object at 0x000001EBA9517950>
def test_variant_with_size(create_command, tmp_path, capsys):
"""If the app specifies a variant with a size, the sized variant is used."""
create_command.tools.shutil = mock.MagicMock(spec_set=shutil)
# Create the source image
source_file = tmp_path / "base_path/input/original-3742.png"
source_file.parent.mkdir(parents=True, exist_ok=True)
with source_file.open("w", encoding="utf-8") as f:
f.write("image")
# Try to install the image
out_path = tmp_path / "output.png"
create_command.install_image(
"sample image",
source={
"round": "input/original",
},
variant="round",
size="3742",
target=out_path,
)
# The right message was written to output
expected = (
"Installing input/original-3742.png as 3742px round sample image... started\n"
"Installing input/original-3742.png as 3742px round sample image... done\n\n"
)
> assert capsys.readouterr().out == expected
E AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
E
E Skipping 57 identical leading characters in diff, use -v to show
E + image... done
E - image... started
E - Installing input/original-3742.png as 3742px round sample image... done
tests\commands\create\test_install_image.py:280: AssertionError
________________________________________________________________________ test_unsized_variant _________________________________________________________________________
create_command = <tests.commands.create.conftest.DummyCreateCommand object at 0x000001EBA92A7CE0>
tmp_path = WindowsPath('C:/Users/yngve/AppData/Local/Temp/pytest-of-yngve/pytest-9/test_unsized_variant0')
capsys = <_pytest.capture.CaptureFixture object at 0x000001EBA92A7950>
def test_unsized_variant(create_command, tmp_path, capsys):
"""If the app specifies an unsized variant, it is used."""
create_command.tools.shutil = mock.MagicMock(spec_set=shutil)
# Create the source image
source_file = tmp_path / "base_path/input/original.png"
source_file.parent.mkdir(parents=True, exist_ok=True)
with source_file.open("w", encoding="utf-8") as f:
f.write("image")
# Try to install the image
# Unsized variants are an annoying edge case; they get the *variant*
# as the *size*.
out_path = tmp_path / "output.png"
create_command.install_image(
"sample image",
source={
"round": "input/original",
},
variant=None,
size="round",
target=out_path,
)
# The right message was written to output
expected = (
"Installing input/original.png as round sample image... started\n"
"Installing input/original.png as round sample image... done\n\n"
)
> assert capsys.readouterr().out == expected
E AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
E
E Skipping 45 identical leading characters in diff, use -v to show
E + image... done
E - image... started
E - Installing input/original.png as round sample image... done
tests\commands\create\test_install_image.py:378: AssertionError
_________________________________________________________________________ test_sign_app[True] _________________________________________________________________________
dummy_command = <tests.platforms.macOS.app.test_signing.DummySigningCommand object at 0x000001EBA9BF2C00>
first_app_with_binaries = <com.example.first-app v0.0.1 AppConfig>, verbose = True
tmp_path = WindowsPath('C:/Users/yngve/AppData/Local/Temp/pytest-of-yngve/pytest-9/test_sign_app_True_0')
capsys = <_pytest.capture.CaptureFixture object at 0x000001EBA9C96C30>
@pytest.mark.parametrize("verbose", [True, False])
def test_sign_app(dummy_command, first_app_with_binaries, verbose, tmp_path, capsys):
"""An app bundle can be signed."""
if verbose:
dummy_command.logger.verbosity = LogLevel.VERBOSE
# Sign the app
dummy_command.sign_app(
first_app_with_binaries, identity="Sekrit identity (DEADBEEF)"
)
# A request has been made to sign all the so and dylib files
# This acts as a test of the discovery process:
# * It discovers frameworks
# * It discovers apps
# * It discovers Mach-O binaries in various forms and guises
# * It *doesn't* discover directories
# * It *doesn't* discover non-Mach-O binaries
# * It traverses in "depth first" order
app_path = (
tmp_path
/ "base_path"
/ "build"
/ "first-app"
/ "macos"
/ "app"
/ "First App.app"
)
lib_path = app_path / "Contents/Resources/app_packages"
frameworks_path = app_path / "Contents/Frameworks"
dummy_command.tools.subprocess.run.assert_has_calls(
[
sign_call(tmp_path, lib_path / "subfolder/second_so.so"),
sign_call(tmp_path, lib_path / "subfolder/second_dylib.dylib"),
sign_call(tmp_path, lib_path / "special.binary"),
sign_call(tmp_path, lib_path / "other_binary"),
sign_call(tmp_path, lib_path / "first_so.so"),
sign_call(tmp_path, lib_path / "first_dylib.dylib"),
sign_call(tmp_path, lib_path / "Extras.app/Contents/MacOS/Extras"),
sign_call(tmp_path, lib_path / "Extras.app"),
sign_call(
tmp_path,
frameworks_path / "Extras.framework/Resources/extras.dylib",
),
sign_call(tmp_path, frameworks_path / "Extras.framework"),
sign_call(tmp_path, app_path),
],
any_order=True,
)
# Also check that files are not signed after their parent directory has been
# signed. Reduce the files mentions in the calls to the dummy command
# to a list of path objects, then ensure that the call to sign any given file
# does not occur *after* it's parent directory.
sign_targets = [
Path(call.args[0][1]) for call in dummy_command.tools.subprocess.run.mock_calls
]
parents = set()
for path in sign_targets:
# Check parent of path is not in parents
assert path.parent not in parents
parents.add(path)
# Output only happens if in debug mode.
output = capsys.readouterr().out
if sys.platform == "win32":
# In practice, we won't ever actually run signing on win32; but to ensure test
# coverage we need to. However, win32 doesn't handle executable permissions
# the same as linux/unix, `unknown.binary` is identified as a signing target.
# We ignore this discrepancy for testing purposes.
> assert len(output.strip("\n").split("\n")) == (12 if verbose else 1)
E AssertionError: assert 13 == 12
E + where 13 = len(['Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary', 'Signing bu...er_binary', 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\first_so.so', ...])
E + where ['Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary', 'Signing bu...er_binary', 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\first_so.so', ...] = <built-in method split of str object at 0x000001EBA79D8970>('\n')
E + where <built-in method split of str object at 0x000001EBA79D8970> = 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary\nSigning build...ng build\\first-app\\macos\\app\\First App.app\n -------------------------------------------------- 100.0% • 00:00'.split
E + where 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary\nSigning build...ng build\\first-app\\macos\\app\\First App.app\n -------------------------------------------------- 100.0% • 00:00' = <built-in method strip of str object at 0x000001EBA79DCFE0>('\n')
E + where <built-in method strip of str object at 0x000001EBA79DCFE0> = 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary\nSigning build... build\\first-app\\macos\\app\\First App.app\n -------------------------------------------------- 100.0% • 00:00\n'.strip
tests\platforms\macOS\app\test_signing.py:555: AssertionError
__________________________________________________________________ test_sign_app_with_failure[True] ___________________________________________________________________
dummy_command = <tests.platforms.macOS.app.test_signing.DummySigningCommand object at 0x000001EBA92BA0C0>
first_app_with_binaries = <com.example.first-app v0.0.1 AppConfig>, verbose = True, capsys = <_pytest.capture.CaptureFixture object at 0x000001EBA95409E0>
@pytest.mark.parametrize("verbose", [True, False])
def test_sign_app_with_failure(dummy_command, first_app_with_binaries, verbose, capsys):
"""If signing a single file in the app fails, the error is surfaced."""
if verbose:
dummy_command.logger.verbosity = LogLevel.VERBOSE
# Sign the app. Signing first_dylib.dylib will fail.
def _codesign(args, **kwargs):
if Path(args[1]).name == "first_dylib.dylib":
raise subprocess.CalledProcessError(
returncode=1, cmd=args, stderr=f"{args[1]}: Unknown error"
)
dummy_command.tools.subprocess.run.side_effect = _codesign
# The invocation will raise an error; however, we can't predict exactly which
# file will raise an error.
with pytest.raises(
BriefcaseCommandError, match=r"Unable to code sign .*first_dylib\.dylib"
):
dummy_command.sign_app(
first_app_with_binaries, identity="Sekrit identity (DEADBEEF)"
)
# There has been at least 1 call to sign files. We can't know how many are
# actually signed, as threads are involved.
dummy_command.tools.subprocess.run.call_count > 0
# Output only happens if in debug mode.
output = capsys.readouterr().out
if sys.platform == "win32":
# In practice, we won't ever actually run signing on win32; but to ensure test
# coverage we need to. However, win32 doesn't handle executable permissions
# the same as linux/unix, `unknown.binary` is identified as a signing target.
# We ignore this discrepancy for testing purposes.
> assert len(output.strip("\n").split("\n")) == (7 if verbose else 1)
E AssertionError: assert 8 == 7
E + where 8 = len(['Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary', 'Signing bu...er_binary', 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\first_so.so', ...])
E + where ['Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary', 'Signing bu...er_binary', 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\first_so.so', ...] = <built-in method split of str object at 0x000001EBA62FF0E0>('\n')
E + where <built-in method split of str object at 0x000001EBA62FF0E0> = 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary\nSigning build...nts\\Resources\\app_packages\\first_dylib.dylib\n - ----------------------------- 58.3% • 00:01'.split
E + where 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary\nSigning build...nts\\Resources\\app_packages\\first_dylib.dylib\n - ----------------------------- 58.3% • 00:01' = <built-in method strip of str object at 0x000001EBA62FDDF0>('\n')
E + where <built-in method strip of str object at 0x000001EBA62FDDF0> = 'Signing build\\first-app\\macos\\app\\First App.app\\Contents\\Resources\\app_packages\\unknown.binary\nSigning build...s\\Resources\\app_packages\\first_dylib.dylib\n - ----------------------------- 58.3% • 00:01\n'.strip
tests\platforms\macOS\app\test_signing.py:595: AssertionError
======================================================================= short test summary info =======================================================================
FAILED tests/commands/create/test_cleanup_app_content.py::test_no_cleanup[True] - AssertionError: assert 4 == 5
FAILED tests/commands/create/test_cleanup_app_content.py::test_no_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_dir_cleanup[True] - AssertionError: assert 4 == 5
FAILED tests/commands/create/test_cleanup_app_content.py::test_dir_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_file_cleanup[True] - AssertionError: assert 5 == 6
FAILED tests/commands/create/test_cleanup_app_content.py::test_file_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_all_files_in_dir_cleanup[True] - AssertionError: assert 7 == 8
FAILED tests/commands/create/test_cleanup_app_content.py::test_all_files_in_dir_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_dir_glob_cleanup[True] - AssertionError: assert 5 == 6
FAILED tests/commands/create/test_cleanup_app_content.py::test_dir_glob_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_file_glob_cleanup[True] - AssertionError: assert 6 == 7
FAILED tests/commands/create/test_cleanup_app_content.py::test_file_glob_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_deep_glob_cleanup[True] - AssertionError: assert 7 == 8
FAILED tests/commands/create/test_cleanup_app_content.py::test_deep_glob_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_template_glob_cleanup[True] - AssertionError: assert 7 == 8
FAILED tests/commands/create/test_cleanup_app_content.py::test_template_glob_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_cleanup_app_content.py::test_non_existent_cleanup[True] - AssertionError: assert 5 == 6
FAILED tests/commands/create/test_cleanup_app_content.py::test_non_existent_cleanup[False] - AssertionError: assert 3 == 4
FAILED tests/commands/create/test_install_image.py::test_no_requested_size - AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
FAILED tests/commands/create/test_install_image.py::test_requested_size - AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
FAILED tests/commands/create/test_install_image.py::test_variant_with_no_requested_size - AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
FAILED tests/commands/create/test_install_image.py::test_variant_with_size - AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
FAILED tests/commands/create/test_install_image.py::test_unsized_variant - AssertionError: assert 'Installing i...e... done\n\n' == 'Installing i...e... done\n\n'
FAILED tests/platforms/macOS/app/test_signing.py::test_sign_app[True] - AssertionError: assert 13 == 12
FAILED tests/platforms/macOS/app/test_signing.py::test_sign_app_with_failure[True] - AssertionError: assert 8 == 7
======================================================= 25 failed, 2828 passed, 91 skipped in 131.64s (0:02:11) =======================================================
Describe the bug
Many tests that use the
capsys
fixture fail on Windows 11. It seems like it's becausesys.__stdout__.isatty()
evaluates toTrue
when we run the tests with pytest, since the following (horrible) workaround makes everything pass:Steps to reproduce
Expected behavior
All tests should pass
Screenshots
No response
Environment
Logs
Additional context
No response