Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

py.test: how to automatically detect an exception in a child process?

I'm running py.test on Linux in the context of a module that makes heavy usage of multiprocessing. Exceptions in child processes are not detected as an error. Example test file pytest_mp_test.py:

import multiprocessing

def test_mp():
    p = multiprocessing.Process(target=child)
    p.start()
    p.join()

def child():
    assert False

Execution:

$ py.test pytest_mp_test.py 
================================== test session starts ===================================
platform linux2 -- Python 2.7.3 -- pytest-2.3.3
plugins: cov
collected 1 items 

pytest_mp_test.py .

================================ 1 passed in 0.04 seconds ================================

No error detected. The exception is printed when invoked with -s:

$ py.test -s pytest_mp_test.py 
================================== test session starts ===================================
platform linux2 -- Python 2.7.3 -- pytest-2.3.3
plugins: cov
collected 1 items 

pytest_mp_test.py Process Process-1:
Traceback (most recent call last):
  File "/apps11/bioinfp/Python-2.7.3/lib/python2.7/multiprocessing/process.py", line 258, in _bootstrap
    self.run()
  File "/apps11/bioinfp/Python-2.7.3/lib/python2.7/multiprocessing/process.py", line 114, in run
    self._target(*self._args, **self._kwargs)
  File "/home/bioinfp/jang/hobbyproggorn/gevent-messagepipe/gevent-messagepipe/pytest_mp_test.py", line 9, in child
    assert False
AssertionError: assert False
.

================================ 1 passed in 0.04 seconds ================================

When I manually review the test log, I realize when there is a problem. However, I am wondering if there a neat way to automate exception detection in a child with py.test.

Must I verify the child's exit code in the parent? Is this the only way?

like image 573
Dr. Jan-Philip Gehrcke Avatar asked Nov 15 '12 15:11

Dr. Jan-Philip Gehrcke


People also ask

How do you catch exceptions in multiprocessing?

If a task issued asynchronously raises an exception, it will be caught by the process pool and re-raised if you call get() function in the AsyncResult object in order to get the result. It means that you have two options for handling exceptions in tasks, they are: Handle exceptions within the task function.

What is Conftest in pytest?

The conftest.py file serves as a means of providing fixtures for an entire directory. Fixtures defined in a conftest.py can be used by any test in that package without needing to import them (pytest will automatically discover them).

What is flag in pytest?

The -v flag controls the verbosity of pytest output in various aspects: test session progress, assertion details when tests fail, fixtures details with --fixtures , etc.


2 Answers

After some further consideration, I think that just catching the expected exception in the child and checking for the child's exit state is a very clean and reliable solution, that does not add an additional IPC component to the tests. Example code:

import multiprocessing
from py.test import raises

class CustomError(Exception):
    pass

def test_mp_expected_fail():
    p = multiprocessing.Process(target=child_expected_fail)
    p.start()
    p.join()
    assert not p.exitcode

def test_mp_success():
    p = multiprocessing.Process(target=child)
    p.start()
    p.join()
    assert not p.exitcode

def test_mp_unexpected_fail():
    p = multiprocessing.Process(target=child_unexpected_fail)
    p.start()
    p.join()
    assert not p.exitcode


def child_expected_fail():
    with raises(CustomError):
        raise CustomError

def child_unexpected_fail():
    raise TypeError

def child():
    pass

Execution:

$ py.test pytest_mp_test.py 
================================== test session starts ===================================
platform linux2 -- Python 2.7.3 -- pytest-2.3.3
plugins: cov
collected 3 items 

pytest_mp_test.py ..F

======================================== FAILURES ========================================
________________________________ test_mp_unexpected_fail _________________________________

    def test_mp_unexpected_fail():
        p = multiprocessing.Process(target=child_unexpected_fail)
        p.start()
        p.join()
>       assert not p.exitcode
E       assert not 1
E        +  where 1 = <Process(Process-3, stopped[1])>.exitcode

pytest_mp_test.py:23: AssertionError
------------------------------------ Captured stderr -------------------------------------
Process Process-3:
Traceback (most recent call last):
  File "/apps11/bioinfp/Python-2.7.3/lib/python2.7/multiprocessing/process.py", line 258, in _bootstrap
    self.run()
  File "/apps11/bioinfp/Python-2.7.3/lib/python2.7/multiprocessing/process.py", line 114, in run
    self._target(*self._args, **self._kwargs)
  File "/home/bioinfp/jang/hobbyproggorn/gevent-messagepipe/gevent-messagepipe/pytest_mp_test.py", line 31, in child_unexpected_fail
    raise TypeError
TypeError
=========================== 1 failed, 2 passed in 0.07 seconds ===========================
like image 151
Dr. Jan-Philip Gehrcke Avatar answered Oct 06 '22 01:10

Dr. Jan-Philip Gehrcke


import pytest

@pytest.fixture
def runproc(request):
    import multiprocessing
    def Process(target):
        p = multiprocessing.Process(target=target)
        p.start()
        p.join()
        return p
    return Process

class CustomError(Exception):
    pass


def test_mp_expected_fail(runproc):
    p = runproc(child_expected_fail)
    assert not p.exitcode

def test_mp_success(runproc):
    p = runproc(target=child)
    assert not p.exitcode

def test_mp_unexpected_fail(runproc):
    p = runproc(child_unexpected_fail)
    assert not p.exitcode

def child_expected_fail():
    with pytest.raises(CustomError):
        raise CustomError

def child_unexpected_fail():
    raise TypeError

def child():
    pass

If you put this "runproc" fixture into a conftest.py file you can accept "runproc" in any function in your project.

like image 41
hpk42 Avatar answered Oct 06 '22 02:10

hpk42