Repository: beeware/briefcase

Issue:
Controlled console output raises encoding errors
In the process of investigating #749, it became clear that the introduction of Rich captured output and wait bars has impacted on console output.

The error message described by #749 is no longer generated, because the output of the linker is captured by a waitbar; when the captured stdout has `readline()` invoked on it, you get encoding errors:

**To Reproduce**
Steps to reproduce the behavior:
1. Using checkout 2127f6e, generate a helloworld app
2. Add a file named `büggy.py` to your project
3. run `briefcase packages`
4. See error
```
(venv3.10) Z:\beeware\toga\local\bugs\bug749>briefcase package

[bug749] Building MSI...
Compiling application manifest...
Compiling... done
Compiling application installer...
bug749.wxs
bug749-manifest.wxs
Compiling... done
Linking application installer...
Exception in thread Wait Bar streamer output streamer:
Traceback (most recent call last):
  File "C:\Users\rkm\AppData\Local\Programs\Python\Python310\lib\threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "C:\Users\rkm\AppData\Local\Programs\Python\Python310\lib\threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "\\mac\home\beeware\briefcase\src\briefcase\integrations\subprocess.py", line 386, in _stream_output_thread
    output_line = ensure_str(popen_process.stdout.readline())
  File "C:\Users\rkm\AppData\Local\Programs\Python\Python310\lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x81 in position 48: character maps to <undefined>
Linking...

Unable to link app bug749.
```
**Expected behavior**

The output described by #749 should still be displayed.

**Environment:**
 - Operating System: Windows 10
 - Python version: Any
 - Software versions:
   - Briefcase: 2127f6e
   - Toga: any

**Additional context**

This appears to be caused by the morass that is Windows encodings. Windows locale default to cp1252, but *stdout* defaults to cp437. The captured output is captured with `text=True`, so it's trying to decode cp437 output using cp1252, which raises an error on the character `ü`.
```
>>> 'ü'.encode('cp1252')
b'\xfc'

>>> 'ü'.encode('cp437')
b'\x81'

>>> b'\x81'.decode('cp1252')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/encodings/cp1252.py", line 15, in decode
    return codecs.charmap_decode(input,errors,decoding_table)
UnicodeDecodeError: 'charmap' codec can't decode byte 0x81 in position 0: character maps to <undefined>
```

I guess this could happen in theory on any other system, but every other system has pretty much converged on UTF-8 encoding for everything, so in practice, this bug isn't seen.

Discussion:

Patch:
diff --git a/changes/770.bugfix.rst b/changes/770.bugfix.rst
new file mode 100644
index 00000000..b942055f
--- /dev/null
+++ b/changes/770.bugfix.rst
@@ -0,0 +1,1 @@
+Output from subprocesses is correctly encoded, avoiding errors on Windows when printing non-ASCII content.
diff --git a/src/briefcase/integrations/subprocess.py b/src/briefcase/integrations/subprocess.py
index db3a4bd5..98823d80 100644
--- a/src/briefcase/integrations/subprocess.py
+++ b/src/briefcase/integrations/subprocess.py
@@ -1,7 +1,9 @@
 import json
 import operator
+import os
 import shlex
 import subprocess
+import sys
 import threading
 import time
 
@@ -141,6 +143,19 @@ class Subprocess:
         if "text" not in kwargs and "universal_newlines" not in kwargs:
             kwargs["text"] = True
 
+        # If we're in text/universal_newlines mode, ensure that there is
+        # an encoding specified. If encoding isn't specified, default to
+        # the system's stdout encoding.
+        # This is for the benefit of Windows, which uses cp437 for console
+        # output when the system encoding is cp1252.
+        # `__stdout__` is used because rich captures and redirects `sys.stdout`.
+        # The fallback to "UTF-8" is needed to catch the case where stdout
+        # is redirected to a non-tty (e.g. during pytest conditions)
+        if kwargs.get("text") or kwargs.get("universal_newlines", False):
+            kwargs.setdefault(
+                "encoding", os.device_encoding(sys.__stdout__.fileno()) or "UTF-8"
+            )
+
         # For Windows, convert start_new_session=True to creation flags
         if self.command.host_os == "Windows":
             try:


Test patch:
diff --git a/tests/integrations/docker/test_Docker__prepare.py b/tests/integrations/docker/test_Docker__prepare.py
index f001bd28..681432f0 100644
--- a/tests/integrations/docker/test_Docker__prepare.py
+++ b/tests/integrations/docker/test_Docker__prepare.py
@@ -1,5 +1,6 @@
 import os
 import subprocess
+from unittest.mock import ANY
 
 import pytest
 
@@ -31,6 +32,7 @@ def test_prepare(mock_docker, tmp_path):
         ],
         check=True,
         text=True,
+        encoding=ANY,
     )
 
 
@@ -64,4 +66,5 @@ def test_prepare_failure(mock_docker, tmp_path):
         ],
         check=True,
         text=True,
+        encoding=ANY,
     )
diff --git a/tests/integrations/docker/test_Docker__run.py b/tests/integrations/docker/test_Docker__run.py
index b066e492..3405acd5 100644
--- a/tests/integrations/docker/test_Docker__run.py
+++ b/tests/integrations/docker/test_Docker__run.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from unittest.mock import ANY
 
 import pytest
 
@@ -25,6 +26,7 @@ def test_simple_call(mock_docker, tmp_path, capsys):
             "world",
         ],
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -48,6 +50,7 @@ def test_simple_call_with_arg(mock_docker, tmp_path, capsys):
             "world",
         ],
         universal_newlines=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -72,6 +75,7 @@ def test_simple_call_with_path_arg(mock_docker, tmp_path, capsys):
         ],
         cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -101,6 +105,7 @@ def test_simple_call_with_sys_executable_arg(
             "python3.X",
         ],
         text=True,
+        encoding=ANY,
     )
 
     assert capsys.readouterr().out == ""
@@ -129,6 +134,7 @@ def test_simple_verbose_call(mock_docker, tmp_path, capsys):
             "world",
         ],
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == (
         "\n"
diff --git a/tests/integrations/subprocess/test_Subprocess__Popen.py b/tests/integrations/subprocess/test_Subprocess__Popen.py
index 3e8e168f..c2a8f231 100644
--- a/tests/integrations/subprocess/test_Subprocess__Popen.py
+++ b/tests/integrations/subprocess/test_Subprocess__Popen.py
@@ -1,4 +1,5 @@
 import os
+from unittest.mock import ANY
 
 import pytest
 
@@ -14,7 +15,11 @@ def test_call(mock_sub, capsys, platform):
     mock_sub.command.host_os = platform
     mock_sub.Popen(["hello", "world"])
 
-    mock_sub._subprocess.Popen.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.Popen.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     assert capsys.readouterr().out == ""
 
 
@@ -24,7 +29,9 @@ def test_call_with_arg(mock_sub, capsys):
     mock_sub.Popen(["hello", "world"], universal_newlines=True)
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], universal_newlines=True
+        ["hello", "world"],
+        universal_newlines=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -38,6 +45,7 @@ def test_call_with_path_arg(mock_sub, capsys, tmp_path):
         ["hello", os.fsdecode(tmp_path / "location")],
         cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -73,6 +81,7 @@ def test_call_with_start_new_session(
         mock_sub._subprocess.Popen.assert_called_with(
             ["hello", "world"],
             text=True,
+            encoding=ANY,
             **popen_kwargs,
         )
         assert capsys.readouterr().out == ""
@@ -81,6 +90,7 @@ def test_call_with_start_new_session(
             ["hello", "world"],
             start_new_session=start_new_session,
             text=True,
+            encoding=ANY,
             **popen_kwargs,
         )
         assert capsys.readouterr().out == ""
@@ -117,7 +127,11 @@ def test_debug_call(mock_sub, capsys):
 
     mock_sub.Popen(["hello", "world"])
 
-    mock_sub._subprocess.Popen.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.Popen.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     assert capsys.readouterr().out == (
         "\n" ">>> Running Command:\n" ">>>     hello world\n"
     )
@@ -134,7 +148,10 @@ def test_debug_call_with_env(mock_sub, capsys):
     merged_env.update(env)
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], env=merged_env, text=True
+        ["hello", "world"],
+        env=merged_env,
+        text=True,
+        encoding=ANY,
     )
 
     expected_output = (
@@ -155,7 +172,11 @@ def test_deep_debug_call(mock_sub, capsys):
 
     mock_sub.Popen(["hello", "world"])
 
-    mock_sub._subprocess.Popen.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.Popen.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
 
     expected_output = (
         "\n"
@@ -185,7 +206,10 @@ def test_deep_debug_call_with_env(mock_sub, capsys):
     merged_env.update(env)
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], env=merged_env, text=True
+        ["hello", "world"],
+        env=merged_env,
+        text=True,
+        encoding=ANY,
     )
 
     expected_output = (
@@ -208,11 +232,11 @@ def test_deep_debug_call_with_env(mock_sub, capsys):
 @pytest.mark.parametrize(
     "in_kwargs, kwargs",
     [
-        ({}, {"text": True}),
-        ({"text": True}, {"text": True}),
+        ({}, {"text": True, "encoding": ANY}),
+        ({"text": True}, {"text": True, "encoding": ANY}),
         ({"text": False}, {"text": False}),
         ({"universal_newlines": False}, {"universal_newlines": False}),
-        ({"universal_newlines": True}, {"universal_newlines": True}),
+        ({"universal_newlines": True}, {"universal_newlines": True, "encoding": ANY}),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
diff --git a/tests/integrations/subprocess/test_Subprocess__check_output.py b/tests/integrations/subprocess/test_Subprocess__check_output.py
index 3233667d..b832b0d2 100644
--- a/tests/integrations/subprocess/test_Subprocess__check_output.py
+++ b/tests/integrations/subprocess/test_Subprocess__check_output.py
@@ -1,5 +1,6 @@
 import os
 from subprocess import CalledProcessError
+from unittest.mock import ANY
 
 import pytest
 
@@ -15,7 +16,11 @@ def test_call(mock_sub, capsys, platform):
     mock_sub.command.host_os = platform
     mock_sub.check_output(["hello", "world"])
 
-    mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.check_output.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     assert capsys.readouterr().out == ""
 
 
@@ -27,6 +32,7 @@ def test_call_with_arg(mock_sub, capsys):
     mock_sub._subprocess.check_output.assert_called_with(
         ["hello", "world"],
         universal_newlines=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -40,6 +46,7 @@ def test_call_with_path_arg(mock_sub, capsys, tmp_path):
         ["hello", os.fsdecode(tmp_path / "location")],
         cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -75,6 +82,7 @@ def test_call_with_start_new_session(
         mock_sub._subprocess.check_output.assert_called_with(
             ["hello", "world"],
             text=True,
+            encoding=ANY,
             **check_output_kwargs,
         )
         assert capsys.readouterr().out == ""
@@ -83,6 +91,7 @@ def test_call_with_start_new_session(
             ["hello", "world"],
             start_new_session=start_new_session,
             text=True,
+            encoding=ANY,
             **check_output_kwargs,
         )
         assert capsys.readouterr().out == ""
@@ -119,7 +128,11 @@ def test_debug_call(mock_sub, capsys):
 
     mock_sub.check_output(["hello", "world"])
 
-    mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.check_output.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
 
     expected_output = (
         "\n"
@@ -145,7 +158,10 @@ def test_debug_call_with_env(mock_sub, capsys):
     merged_env.update(env)
 
     mock_sub._subprocess.check_output.assert_called_with(
-        ["hello", "world"], env=merged_env, text=True
+        ["hello", "world"],
+        env=merged_env,
+        text=True,
+        encoding=ANY,
     )
 
     expected_output = (
@@ -170,7 +186,11 @@ def test_deep_debug_call(mock_sub, capsys):
 
     mock_sub.check_output(["hello", "world"])
 
-    mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.check_output.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
 
     expected_output = (
         "\n"
@@ -204,7 +224,10 @@ def test_deep_debug_call_with_env(mock_sub, capsys):
     merged_env.update(env)
 
     mock_sub._subprocess.check_output.assert_called_with(
-        ["hello", "world"], env=merged_env, text=True
+        ["hello", "world"],
+        env=merged_env,
+        text=True,
+        encoding=ANY,
     )
 
     expected_output = (
@@ -269,11 +292,11 @@ def test_calledprocesserror_exception_logging(mock_sub, capsys):
 @pytest.mark.parametrize(
     "in_kwargs, kwargs",
     [
-        ({}, {"text": True}),
-        ({"text": True}, {"text": True}),
+        ({}, {"text": True, "encoding": ANY}),
+        ({"text": True}, {"text": True, "encoding": ANY}),
         ({"text": False}, {"text": False}),
         ({"universal_newlines": False}, {"universal_newlines": False}),
-        ({"universal_newlines": True}, {"universal_newlines": True}),
+        ({"universal_newlines": True}, {"universal_newlines": True, "encoding": ANY}),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
diff --git a/tests/integrations/subprocess/test_Subprocess__final_kwargs.py b/tests/integrations/subprocess/test_Subprocess__final_kwargs.py
new file mode 100644
index 00000000..76f70211
--- /dev/null
+++ b/tests/integrations/subprocess/test_Subprocess__final_kwargs.py
@@ -0,0 +1,71 @@
+import platform
+
+import pytest
+
+# Default encoding is platform specific.
+if platform.platform() == "Windows":
+    CONSOLE_ENCODING = "cp437"
+else:
+    CONSOLE_ENCODING = "UTF-8"
+
+
+def test_no_overrides(mock_sub):
+    """With no overrides, there are still kwargs."""
+    assert mock_sub.final_kwargs() == {
+        "text": True,
+        "encoding": CONSOLE_ENCODING,
+    }
+
+
+def test_env_overrides(mock_sub):
+    """If environmental overrides are provided, they supercede the default
+    environment."""
+    assert mock_sub.final_kwargs(env={"NEWVAR": "value", "VAR1": "New Value"}) == {
+        "env": {
+            "VAR1": "New Value",
+            "PS1": "\nLine 2\n\nLine 4",
+            "PWD": "/home/user/",
+            "NEWVAR": "value",
+        },
+        "text": True,
+        "encoding": CONSOLE_ENCODING,
+    }
+
+
+def test_cwd_provided(mock_sub):
+    """If a cwd is provided, it is reflected in the environment."""
+
+
+def test_non_str_cwd_provided(mock_sub):
+    """If the cwd isn't a string, it's converted to string."""
+
+
+@pytest.mark.parametrize(
+    "in_kwargs, final_kwargs",
+    [
+        ({}, {"text": True, "encoding": CONSOLE_ENCODING}),
+        ({"text": True}, {"text": True, "encoding": CONSOLE_ENCODING}),
+        ({"text": False}, {"text": False}),
+        ({"universal_newlines": False}, {"universal_newlines": False}),
+        (
+            {"universal_newlines": True},
+            {"universal_newlines": True, "encoding": CONSOLE_ENCODING},
+        ),
+        # Explcit encoding provided
+        ({"encoding": "ibm850"}, {"text": True, "encoding": "ibm850"}),
+        ({"text": True, "encoding": "ibm850"}, {"text": True, "encoding": "ibm850"}),
+        ({"text": False, "encoding": "ibm850"}, {"text": False, "encoding": "ibm850"}),
+        (
+            {"universal_newlines": False, "encoding": "ibm850"},
+            {"universal_newlines": False, "encoding": "ibm850"},
+        ),
+        (
+            {"universal_newlines": True, "encoding": "ibm850"},
+            {"universal_newlines": True, "encoding": "ibm850"},
+        ),
+    ],
+)
+def test_text_conversion(mock_sub, in_kwargs, final_kwargs):
+    """Text/universal_newlines is correctly inserted/overridden, with
+    encoding."""
+    assert mock_sub.final_kwargs(**in_kwargs) == final_kwargs
diff --git a/tests/integrations/subprocess/test_Subprocess__parse_output.py b/tests/integrations/subprocess/test_Subprocess__parse_output.py
index 791014d4..d15f1b36 100644
--- a/tests/integrations/subprocess/test_Subprocess__parse_output.py
+++ b/tests/integrations/subprocess/test_Subprocess__parse_output.py
@@ -1,3 +1,5 @@
+from unittest.mock import ANY
+
 import pytest
 
 from briefcase.integrations.subprocess import CommandOutputParseError, ParseError
@@ -29,7 +31,11 @@ def test_call(mock_sub, capsys):
 
     output = mock_sub.parse_output(splitlines_parser, ["hello", "world"])
 
-    mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.check_output.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     assert capsys.readouterr().out == ""
 
     assert output == ["some output line 1", "more output line 2"]
@@ -46,6 +52,7 @@ def test_call_with_arg(mock_sub, capsys):
         ["hello", "world"],
         extra_arg="asdf",
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -57,7 +64,11 @@ def test_call_with_parser_success(mock_sub, capsys):
 
     output = mock_sub.parse_output(second_line_parser, ["hello", "world"])
 
-    mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.check_output.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     assert output == "more output line 2"
 
 
@@ -70,7 +81,11 @@ def test_call_with_parser_error(mock_sub, capsys):
     ):
         mock_sub.parse_output(third_line_parser, ["hello", "world"])
 
-    mock_sub._subprocess.check_output.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.check_output.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     expected_output = (
         "\n"
         "Command Output Parsing Error:\n"
@@ -87,11 +102,11 @@ def test_call_with_parser_error(mock_sub, capsys):
 @pytest.mark.parametrize(
     "in_kwargs, kwargs",
     [
-        ({}, {"text": True}),
-        ({"text": True}, {"text": True}),
+        ({}, {"text": True, "encoding": ANY}),
+        ({"text": True}, {"text": True, "encoding": ANY}),
         ({"text": False}, {"text": False}),
         ({"universal_newlines": False}, {"universal_newlines": False}),
-        ({"universal_newlines": True}, {"universal_newlines": True}),
+        ({"universal_newlines": True}, {"universal_newlines": True, "encoding": ANY}),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
diff --git a/tests/integrations/subprocess/test_Subprocess__run.py b/tests/integrations/subprocess/test_Subprocess__run.py
index 93b3dd5e..63504b25 100644
--- a/tests/integrations/subprocess/test_Subprocess__run.py
+++ b/tests/integrations/subprocess/test_Subprocess__run.py
@@ -1,5 +1,6 @@
 import os
 from subprocess import CalledProcessError
+from unittest.mock import ANY
 
 import pytest
 
@@ -15,7 +16,11 @@ def test_call(mock_sub, capsys, platform):
     mock_sub.command.sys.platform = platform
     mock_sub.run(["hello", "world"])
 
-    mock_sub._subprocess.run.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.run.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     assert capsys.readouterr().out == ""
 
 
@@ -25,7 +30,9 @@ def test_call_with_arg(mock_sub, capsys):
     mock_sub.run(["hello", "world"], universal_newlines=True)
 
     mock_sub._subprocess.run.assert_called_with(
-        ["hello", "world"], universal_newlines=True
+        ["hello", "world"],
+        universal_newlines=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -39,6 +46,7 @@ def test_call_with_path_arg(mock_sub, capsys, tmp_path):
         ["hello", os.fsdecode(tmp_path / "location")],
         cwd=os.fsdecode(tmp_path / "cwd"),
         text=True,
+        encoding=ANY,
     )
     assert capsys.readouterr().out == ""
 
@@ -74,6 +82,7 @@ def test_call_with_start_new_session(
         mock_sub._subprocess.run.assert_called_with(
             ["hello", "world"],
             text=True,
+            encoding=ANY,
             **run_kwargs,
         )
         assert capsys.readouterr().out == ""
@@ -82,6 +91,7 @@ def test_call_with_start_new_session(
             ["hello", "world"],
             start_new_session=start_new_session,
             text=True,
+            encoding=ANY,
             **run_kwargs,
         )
         assert capsys.readouterr().out == ""
@@ -118,7 +128,11 @@ def test_debug_call(mock_sub, capsys):
 
     mock_sub.run(["hello", "world"])
 
-    mock_sub._subprocess.run.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.run.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     # fmt: off
     expected_output = (
         "\n"
@@ -142,7 +156,10 @@ def test_debug_call_with_env(mock_sub, capsys):
     merged_env.update(env)
 
     mock_sub._subprocess.run.assert_called_with(
-        ["hello", "world"], env=merged_env, text=True
+        ["hello", "world"],
+        env=merged_env,
+        text=True,
+        encoding=ANY,
     )
     expected_output = (
         "\n"
@@ -162,7 +179,11 @@ def test_deep_debug_call(mock_sub, capsys):
 
     mock_sub.run(["hello", "world"])
 
-    mock_sub._subprocess.run.assert_called_with(["hello", "world"], text=True)
+    mock_sub._subprocess.run.assert_called_with(
+        ["hello", "world"],
+        text=True,
+        encoding=ANY,
+    )
     expected_output = (
         "\n"
         ">>> Running Command:\n"
@@ -191,7 +212,10 @@ def test_deep_debug_call_with_env(mock_sub, capsys):
     merged_env.update(env)
 
     mock_sub._subprocess.run.assert_called_with(
-        ["hello", "world"], env=merged_env, text=True
+        ["hello", "world"],
+        env=merged_env,
+        text=True,
+        encoding=ANY,
     )
     expected_output = (
         "\n"
@@ -243,11 +267,11 @@ def test_calledprocesserror_exception_logging(mock_sub, capsys):
 @pytest.mark.parametrize(
     "in_kwargs, kwargs",
     [
-        ({}, {"text": True}),
-        ({"text": True}, {"text": True}),
+        ({}, {"text": True, "encoding": ANY}),
+        ({"text": True}, {"text": True, "encoding": ANY}),
         ({"text": False}, {"text": False}),
         ({"universal_newlines": False}, {"universal_newlines": False}),
-        ({"universal_newlines": True}, {"universal_newlines": True}),
+        ({"universal_newlines": True}, {"universal_newlines": True, "encoding": ANY}),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
diff --git a/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py b/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py
index ee5c190a..1667ad3b 100644
--- a/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py
+++ b/tests/integrations/subprocess/test_Subprocess__run_in_wait_bar.py
@@ -1,4 +1,5 @@
 import subprocess
+from unittest.mock import ANY
 
 import pytest
 
@@ -19,7 +20,11 @@ def test_call(mock_sub, capsys):
         mock_sub.run(["hello", "world"])
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
+        ["hello", "world"],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        text=True,
+        encoding=ANY,
     )
     expected_output = "output line 1\n" "\n" "output line 3\n" "\n"
     assert capsys.readouterr().out == expected_output
@@ -36,6 +41,7 @@ def test_call_with_arg(mock_sub, capsys):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         universal_newlines=True,
+        encoding=ANY,
     )
     expected_output = "output line 1\n" "\n" "output line 3\n" "\n"
     assert capsys.readouterr().out == expected_output
@@ -49,7 +55,11 @@ def test_debug_call(mock_sub, capsys):
         mock_sub.run(["hello", "world"])
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
+        ["hello", "world"],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        text=True,
+        encoding=ANY,
     )
     expected_output = (
         "\n"
@@ -82,6 +92,7 @@ def test_debug_call_with_env(mock_sub, capsys):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         text=True,
+        encoding=ANY,
     )
     expected_output = (
         "\n"
@@ -107,7 +118,11 @@ def test_deep_debug_call(mock_sub, capsys):
         mock_sub.run(["hello", "world"])
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
+        ["hello", "world"],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        text=True,
+        encoding=ANY,
     )
     expected_output = (
         "\n"
@@ -147,6 +162,7 @@ def test_deep_debug_call_with_env(mock_sub, capsys):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         text=True,
+        encoding=ANY,
     )
     expected_output = (
         "\n"
@@ -172,11 +188,11 @@ def test_deep_debug_call_with_env(mock_sub, capsys):
 @pytest.mark.parametrize(
     "in_kwargs, kwargs",
     [
-        ({}, {"text": True}),
-        ({"text": True}, {"text": True}),
+        ({}, {"text": True, "encoding": ANY}),
+        ({"text": True}, {"text": True, "encoding": ANY}),
         ({"text": False}, {"text": False}),
         ({"universal_newlines": False}, {"universal_newlines": False}),
-        ({"universal_newlines": True}, {"universal_newlines": True}),
+        ({"universal_newlines": True}, {"universal_newlines": True, "encoding": ANY}),
     ],
 )
 def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
@@ -185,7 +201,10 @@ def test_text_eq_true_default_overriding(mock_sub, in_kwargs, kwargs):
     with mock_sub.command.input.wait_bar():
         mock_sub.run(["hello", "world"], **in_kwargs)
     mock_sub._subprocess.Popen.assert_called_with(
-        ["hello", "world"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs
+        ["hello", "world"],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        **kwargs,
     )
 
 
@@ -202,6 +221,7 @@ def test_stderr_is_redirected(mock_sub, popen_process, capsys):
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
         text=True,
+        encoding=ANY,
     )
 
     expected_output = "output line 1\n" "\n" "output line 3\n" "\n"
@@ -221,6 +241,7 @@ def test_stderr_dev_null(mock_sub, popen_process, capsys):
         stdout=subprocess.PIPE,
         stderr=subprocess.DEVNULL,
         text=True,
+        encoding=ANY,
     )
 
     expected_output = "output line 1\n" "\n" "output line 3\n" "\n"
diff --git a/tests/platforms/linux/appimage/test_build.py b/tests/platforms/linux/appimage/test_build.py
index 8a6a5e16..b469d16c 100644
--- a/tests/platforms/linux/appimage/test_build.py
+++ b/tests/platforms/linux/appimage/test_build.py
@@ -142,6 +142,7 @@ def test_build_appimage(build_command, first_app, tmp_path):
         },
         cwd=os.fsdecode(tmp_path / "linux"),
         text=True,
+        encoding=mock.ANY,
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
     )
@@ -187,6 +188,7 @@ def test_build_failure(build_command, first_app, tmp_path):
         },
         cwd=os.fsdecode(tmp_path / "linux"),
         text=True,
+        encoding=mock.ANY,
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
     )
@@ -238,6 +240,7 @@ def test_build_appimage_with_docker(build_command, first_app, tmp_path):
         ],
         cwd=os.fsdecode(tmp_path / "linux"),
         text=True,
+        encoding=mock.ANY,
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
     )