Repository: microsoft/playwright-python

Issue:
Stdout/stderr pipe throws stacktrace if stdout/stderr is mocked by Pytest
I think more an upstream issue (here) instead of for the Pytest plugin.

Stacktrace:

```
pytest project-name/browser_tests --browser chromium --base-url http://localhost:8000/
============================= test session starts ==============================
platform linux -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
baseurl: http://localhost:8000/
django: settings: config.settings.test (from option)
Using --randomly-seed=1
rootdir: /home/runner/work/project-name/project-name, configfile: setup.cfg
plugins: freezegun-0.4.2, base-url-1.4.2, playwright-0.0.5, cov-2.10.0, sugar-0.9.4, django-3.9.0, randomly-3.4.1, celery-4.4.7, factoryboy-2.0.3, Faker-4.1.1, env-0.6.2
collected 15 items

project-name/browser_tests/test_internal_manager_dashboard.py E [  6%]


==================================== ERRORS ====================================
_ ERROR at setup of test123[chromium] _

launch_browser = <function launch_browser.<locals>.launch at 0x7f6bc73de430>

    @pytest.fixture(scope="session")
    def browser(launch_browser: Callable[[], Browser]) -> Generator[Browser, None, None]:
>       browser = launch_browser()

/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/pytest_playwright/pytest_playwright.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/pytest_playwright/pytest_playwright.py:97: in launch
    pw_context = sync_playwright()
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/playwright/__init__.py:27: in sync_playwright
    return SyncPlaywrightContextManager()
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/playwright/main.py:72: in __init__
    self._connection = run_driver()
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/playwright/main.py:67: in run_driver
    return loop.run_until_complete(run_driver_async())
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/asyncio/base_events.py:616: in run_until_complete
    return future.result()
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/playwright/main.py:48: in run_driver_async
    proc = await asyncio.create_subprocess_exec(
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/asyncio/subprocess.py:236: in create_subprocess_exec
    transport, protocol = await loop.subprocess_exec(
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/asyncio/base_events.py:1630: in subprocess_exec
    transport = await self._make_subprocess_transport(
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/asyncio/unix_events.py:197: in _make_subprocess_transport
    transp = _UnixSubprocessTransport(self, protocol, args, shell,
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/asyncio/base_subprocess.py:36: in __init__
    self._start(args=args, shell=shell, stdin=stdin, stdout=stdout,
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/asyncio/unix_events.py:789: in _start
    self._proc = subprocess.Popen(
/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/subprocess.py:804: in __init__
    errread, errwrite) = self._get_handles(stdin, stdout, stderr)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <subprocess.Popen object at 0x7f6bc73dfa60>
stdin = <socket.socket [closed] fd=-1, family=AddressFamily.AF_UNIX, type=SocketKind.SOCK_STREAM, proto=0>
stdout = -1, stderr = <_io.TextIOWrapper encoding='UTF-8'>

    def _get_handles(self, stdin, stdout, stderr):
        """Construct and return tuple with IO objects:
        p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
        """
        p2cread, p2cwrite = -1, -1
        c2pread, c2pwrite = -1, -1
        errread, errwrite = -1, -1
    
        if stdin is None:
            pass
        elif stdin == PIPE:
            p2cread, p2cwrite = os.pipe()
        elif stdin == DEVNULL:
            p2cread = self._get_devnull()
        elif isinstance(stdin, int):
            p2cread = stdin
        else:
            # Assuming file-like object
            p2cread = stdin.fileno()
    
        if stdout is None:
            pass
        elif stdout == PIPE:
            c2pread, c2pwrite = os.pipe()
        elif stdout == DEVNULL:
            c2pwrite = self._get_devnull()
        elif isinstance(stdout, int):
            c2pwrite = stdout
        else:
            # Assuming file-like object
            c2pwrite = stdout.fileno()
    
        if stderr is None:
            pass
        elif stderr == PIPE:
            errread, errwrite = os.pipe()
        elif stderr == STDOUT:
            if c2pwrite != -1:
                errwrite = c2pwrite
            else: # child's stdout is not set, use parent's stdout
                errwrite = sys.__stdout__.fileno()
        elif stderr == DEVNULL:
            errwrite = self._get_devnull()
        elif isinstance(stderr, int):
            errwrite = stderr
        else:
            # Assuming file-like object
>           errwrite = stderr.fileno()
E           io.UnsupportedOperation: fileno

/opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/subprocess.py:1504: UnsupportedOperation
---------------------------- Captured stdout setup -----------------------------
kektus <_io.TextIOWrapper encoding='UTF-8'>
=========================== short test summary info ============================
ERROR project-name/browser_tests/test123.py::test123[chromium]
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.67s ===============================
Makefile:23: recipe for target 'browser_test' failed

```

Using the `-s` parameter is a workaround for that. Issue is persistent since https://github.com/microsoft/playwright-python/pull/145.


Discussion:

Resolving patch:
diff --git a/playwright/main.py b/playwright/main.py
--- a/playwright/main.py
+++ b/playwright/main.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 import asyncio
+import io
 import subprocess
 import sys
 from typing import Any
@@ -45,11 +46,21 @@ async def run_driver_async() -> Connection:
     driver_name = compute_driver_name()
     driver_executable = package_path / "drivers" / driver_name
 
+    # Sourced from: https://github.com/pytest-dev/pytest/blob/49827adcb9256c9c9c06a25729421dcc3c385edc/src/_pytest/faulthandler.py#L73-L80
+    def _get_stderr_fileno() -> int:
+        try:
+            return sys.stderr.fileno()
+        except io.UnsupportedOperation:
+            # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
+            # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
+            # This is potentially dangerous, but the best we can do.
+            return sys.__stderr__.fileno()
+
     proc = await asyncio.create_subprocess_exec(
         str(driver_executable),
         stdin=asyncio.subprocess.PIPE,
         stdout=asyncio.subprocess.PIPE,
-        stderr=sys.stderr,
+        stderr=_get_stderr_fileno(),
         limit=32768,
     )
     assert proc.stdout


Test patch: