Skip to content

AssertionError if stderr == subprocess.STDOUT and stdout is a file #143

@tbhartman

Description

@tbhartman

Description

An AssertionError is raised at fake_popen.py:188when calling with argument stderr=subprocess.STDOUT if stdout is a file (stdout is expected to be None:

>           assert self.stdout is not None
E           AssertionError

pyvenv\Lib\site-packages\pytest_subprocess\fake_popen.py:188: AssertionError

subprocess.Popen handles this correctly; stderr is simply written to the file at stdout.

Example

Below is an example demonstrating the error. I have GIT_TRACE set so that git rev-parse gives a small amount of stdout and stderr:

$ git rev-parse HEAD
06:05:28.502294 exec-cmd.c:244          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
06:05:28.537291 git.c:463               trace: built-in: git rev-parse HEAD
47c77698dd8e0e35af00da56806fe98fcb9a1056

I define a function that runs the subprocess (so that I can call the same thing, using either the fake or real Popen:

def run_git_revparse():
    import os
    os.environ['GIT_TRACE'] = '1'
    with NamedTemporaryFile("w+b", delete=False) as f:
        path = pathlib.Path(f.name)
        # NOTE: I pass f.file because for some reason:
        #           isinstance(f, io.BufferedWriter) == False
        #       so pytest_subprocess isinstance checks would fail.  I am
        #       hesitant to change those tests, though you could simply check
        #       for a few key methods:
        #           hasattr(f.mode)
        #           isinstance(f.mode, str)
        #           hasattr(f.write)
        #           isinstance(f.write, types.FunctionType)
        p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=subprocess.STDOUT)
        f.close()
        assert p.wait() == 0
        assert path.exists()
        output = path.read_text()

        # print to sys.stdout so I can see with `pytest --capture=no`
        print('-'*50)
        print(output)
        print('-'*50)

        output = output.splitlines()
        assert len(output) == 3

        # account for stderr coming before or after stdout
        if 'exec-cmd.c' in output[1]:
            output[2], output[0], output[1] = output
        # first two lines should be stderr
        assert 'exec-cmd.c' in output[0]
        assert 'git.c' in output[1]
        # last line should be a hex string
        assert len(set(output[2]) - set('0123456789abcdef')) == 0

Then define my test function, which first runs the real git, then the fake "git":

def test_fp(fp):
    print() # for --capture=no
    fp.allow_unregistered(True)
    run_git_revparse()
    fp.register(
        ["git", 'rev-parse', 'HEAD'],
        stdout=["47c77698dd8e0e35af00da56806fe98fcb9a1056"],
        stderr=[
            '05:17:17.982923 exec-cmd.c:244          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin',
            '05:17:18.000760 git.c:463               trace: built-in: git rev-parse HEAD'
            ])
    run_git_revparse()

This raises the assertion error. I can correct this:

--- fake_popen.py   2024-02-10 06:09:01.897909800 -0500
+++ fake_popen.py       2024-02-10 06:08:49.610943500 -0500
@@ -181,22 +181,16 @@
         stdout = kwargs.get("stdout")
         if stdout == subprocess.PIPE:
             self.stdout = self._prepare_buffer(self.__stdout)
-        elif self.__is_io_writer(stdout):
+        elif isinstance(stdout, (io.BufferedWriter, io.TextIOWrapper)):
             self._write_to_buffer(self.__stdout, stdout)
         stderr = kwargs.get("stderr")
         if stderr == subprocess.STDOUT and self.__stderr:
-            if self.__is_io_writer(stdout):
-                self._write_to_buffer(self.__stderr, stdout)
-            else:
-                assert self.stdout is not None
-                self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
+            assert self.stdout is not None
+            self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
         elif stderr == subprocess.PIPE:
             self.stderr = self._prepare_buffer(self.__stderr)
-        elif self.__is_io_writer(stderr):
+        elif isinstance(stderr, (io.BufferedWriter, io.TextIOWrapper)):
             self._write_to_buffer(self.__stderr, stderr)
-
-    def __is_io_writer(self, o):
-        return isinstance(o, (io.BufferedWriter, io.TextIOWrapper, io.BufferedRandom))

     def _prepare_buffer(
         self,

Note:

  • I pulled out the isinstance testing.
  • I added io.BufferedRandom to the group, as that is what I had, and I would expect it should be included...my test still won't work without it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions