Skip to content
YMK Must be Kidding
Go back

/dev/null with pybind11 and unittest.mock

嗨嗨,我愛 pybind11 {% fa_inline file-import %} {% fa_inline python fab %}!

Introduction

最近工作上用 pybind11 來接自己包出來會 Link 到 TensorFlow 的 C++ Library。覺得相當好用寫起來又乾淨,還可以傳入傳出 NumPy 的 ndarray, 實在是太方便了啊。

另外也使用 Python 的 unittest 工具來寫許多的測試, 反正有新功能要加就也會先寫些測試項目,另外也會驗證會不會影響到原本的功能。 通常跑 unittest 的時候,除非要看特定跑過哪些項目, 不然其實是希望只要印出跑完測試的結果就好,像是底下這樣,

$ make tests
.......s...........
----------------------------------------------------------------------
Ran 19 tests in 0.002s

OK (skipped=1)

通常在可以控制的範圍下,都是沒問題的,像是有些 Warnings 只要修一下, 就不會影響到上面乾淨的輸出結果,

with warnings.catch_warnings():
    warnings.simplefilter("ignore", category=PendingDeprecationWarning)
    import np.tools.tflite_runner

不過如剛剛有提到,有些其實是呼叫到別人的 Library , 尤其是一些提示錯誤的訊息像是檔案找不到之類的,本來就會想要從 stderr 印出來, 在故意寫這種的測試項目時,其實就會看到錯誤訊息截斷了測試結果。

python tests/mock_stdout_tests.py TestFooBarStdout.test_print_printf TestFooBarStdout.test_print_fprintf_stdout
printf: [test_print_printf]
.fprintf_stdout: [test_print_fprintf_stdout]
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

{% blockquote %} 所以這篇會以 Python unittest 的角度,分別將 Python 的 print,C++ 的 cout/cerr 以及 C 的 printf/fprintf 的結果導走,不讓這些訊息影響到測試的結果。 {% endblockquote %}

print in Python with unittest.mock

unittest.mock 是個可以在 Pythonunittest 環境下, 用假造的物件替換掉原本應該真正使用的部份,其實功能相當多,這邊只用來替換 sys.stdout 就可以將 print() 的訊息截斷並拉回來,

class TestStdout(unittest.TestCase):
    @unittest.skip('skip due to annoying stdout print')
    def test_print_stdout(self):
        print('test_print_stdout')

    @unittest.skip('skip due to annoying stderr print')
    def test_print_stderr(self):
        print('test_print_stderr', file=sys.stderr)


class TestMockStdout(unittest.TestCase):
    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def test_print_mock_stdout(self, mock_stdout):
        print('test_print_mock_stdout')

    @unittest.mock.patch('sys.stderr', new_callable=io.StringIO)
    def test_print_mock_stderr(self, mock_stderr):
        print('test_print_mock_stderr', file=sys.stderr)

如上面的測試項目,test_print_mock_stderr 就不會像 test_print_stderr 一樣印出訊息,因為被 mock_stderr 給接過來了。如果都是純的Python 程式, 這樣就非常足夠且方便使用。

cout/cerr in C++ with py::scoped_estream_redirect in pybind11

python module with cout

不過要是底下有些 pythonmodule 是用 C++ 接的,然後呼叫 std::cout/std::cerr 印出訊息,上面的方法還不夠,為了驗證首先就用 pybind11 寫一個簡單的 module

#include <pybind11/pybind11.h>
#include <pybind11/iostream.h>

namespace py = pybind11;

PYBIND11_MODULE(print, m) {
    m.attr("__name__") = "foo.bar.print";
    m.def("cout", [](const char* str) {
            std::cout << "cout: [" << str << "]" << std::endl; });
}

也寫個 unittest 來測試一下,結果還真的 mock 不起來, 所以把它 skip 掉了。

class TestFooBarMockStdout(unittest.TestCase):
    @unittest.skip('skip due to annoying stdout cout even mocked')
    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def test_print_mock_cout(self, mock_stdout):
        foo.bar.print.cout('test_print_mock_cout')

py::scoped_ostream_redirect in pybind11

其實 pybind11Capturing standard output from ostream, 就有提到這個問題,

Often, a library will use the streams std::cout and std::cerr to print, but this does not play well with Python’s standard sys.stdout and sys.stderr redirection. Replacing a library’s printing with py::print may not be feasible.

所以 pybind11 也有提供 py::scoped_ostream_redirect 的方法, 將 std::cout 重導到 Pythonsys.stdout 去,用 Capturing standard output from ostream,改寫上面的範例,

#include <pybind11/pybind11.h>
#include <pybind11/iostream.h>

namespace py = pybind11;

PYBIND11_MODULE(print, m) {
    m.attr("__name__") = "foo.bar.print";
    m.def("cout", [](const char* str) {
            std::cout << "cout: [" << str << "]" << std::endl; })
     .def("cout_redirect", [](const char* str) {
            std::cout << "cout redirect: [" << str << "]" << std::endl; },
        py::call_guard<py::scoped_ostream_redirect,
                       py::scoped_estream_redirect>());
}
class TestFooBarMockStdout(unittest.TestCase):
    @unittest.skip('skip due to annoying stdout cout even mocked')
    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def test_print_mock_cout(self, mock_stdout):
        foo.bar.print.cout('test_print_mock_cout')

    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def test_print_mock_cout_redirect(self, mock_stdout):
        foo.bar.print.cout_redirect('test_print_mock_cout_redirect')

呼叫有被 guardcout_redirect,就可以順利地被 mock_stdout 接走了, 當然如果沒有使用 unittest.mock,訊息一樣是會顯示在 sys.stdout 上。

printf/fprintf in C with os.dup/os.dup2

但如果不是使用 std::cout/std::cerr ,而是使用 printffprintf 的話,py::scoped_ostream_redirect 也沒有辦法 Capturing standard output from ostream 也有說明,

The above methods will not redirect C-level output to file descriptors, such as fprintf. For those cases, you’ll need to redirect the file descriptors either directly in C or with Python’s os.dup2 function in an operating-system dependent way.

建議可以使用 os.dup2 自己接走。 既然如此就自己來實作 > /dev/null 2> /dev/null 的功能吧, 也可以達到不影響 unittest 結果顯示。首先還是來準備一下用 fprint()Module

#include <pybind11/pybind11.h>
#include <pybind11/iostream.h>

namespace py = pybind11;

PYBIND11_MODULE(print, m) {
    m.attr("__name__") = "foo.bar.print";
    m.def("printf", [](const char* str){
            printf("printf: [%s]\n", str); })
     .def("fprintf_stdout", [](const char* str) {
             fprintf(stdout, "fprintf_stdout: [%s]\n", str); })
     .def("fprintf_stderr", [](const char* str) {
             fprintf(stderr, "fprintf_stderr: [%s]\n", str); })
     .def("fflush_stdout", []() { fflush(stdout); })
     .def("fflush_stderr", []() { fflush(stderr); });
}

果然即便是用了 unittest.mock ,還是沒辦法擷取到,只好 unittest.skip

class TestFooBarMockStdout(unittest.TestCase):
    @unittest.skip('skip due to annoying stdout printf even mocked')
    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def test_print_mock_printf(self, mock_stdout):
        foo.bar.print.printf('test_print_mock_printf')

    @unittest.skip('skip due to annoying stdout fprintf even mocked')
    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def test_print_mock_fprintf_stdout(self, mock_stdout):
        foo.bar.print.fprintf_stdout('test_print_mock_fprintf_stdout')

所以來開個 /dev/nullos.dup/os.dup2 自己做重導吧, 然後跑完之後再恢復回來,該正常顯示的還是要可以顯示,

class TestFooBarStdout(unittest.TestCase):
    @unittest.skip('skip due to annoying stdout cout at the end')
    def test_dup2_print_printf(self):
        with open(os.devnull, 'w') as null:
            _dup = os.dup(1)
            os.dup2(null.fileno(), 1)
            foo.bar.print.printf('test_dup2_print_printf')
            null.flush()
            os.dup2(_dup, 1)

不過這樣還是有點問題,因為導到 /dev/nullstdout , 雖然沒有馬上印在螢怒上,但還是會在程式結束之後再刷出來, 因為這時候為了其他正常的顯示需要,會將 stdout 接回來, 所以最後還是會在程式結束之後印,

$ python tests/mock_stdout_tests.py TestFooBarStdout.test_print_fprintf_stdout
fprintf_stdout: [test_print_fprintf_stdout]
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

$ python tests/mock_stdout_tests.py TestFooBarStdout.test_dup2_print_fprintf_stdout
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
fprintf_stdout: [test_dup2_print_fprintf_stdout]

這時候可以利用之前加的 fflush_stdout()/fflush_stderr(), 在切換 stdout/stderr 的時候強制刷一下,

class TestFooBarStdout(unittest.TestCase):
    def test_dup2_print_printf_and_fflush_stdout(self):
        with open(os.devnull, 'w') as null:
            _dup = os.dup(1)
            os.dup2(null.fileno(), 1)
            foo.bar.print.printf('test_dup2_print_printf_and_fflush_stdout')
            foo.bar.print.fflush_stdout()
            null.flush()
            os.dup2(_dup, 1)

    def test_dup2_print_fprintf_stdout_and_fflush_stdout(self):
        with open(os.devnull, 'w') as null:
            _dup = os.dup(1)
            os.dup2(null.fileno(), 1)
            foo.bar.print.fprintf_stdout('test_dup2_print_fprintf_stdout_and_fflush_stdout')
            foo.bar.print.fflush_stdout()
            null.flush()
            os.dup2(_dup, 1)

    def test_dup2_print_fprintf_stderr_and_fflush_stderr(self):
        with open(os.devnull, 'w') as null:
            _dup = os.dup(2)
            os.dup2(null.fileno(), 2)
            foo.bar.print.fprintf_stderr('test_dup2_print_fprintf_stderr_and_fflush_stderr')
            foo.bar.print.fflush_stderr()
            null.flush()
            os.dup2(_dup, 2)

如此一來就可以達到濾掉 printf()/fprintf() 的訊息,不會影響到結果顯示了。

$ python tests/mock_stdout_tests.py TestFooBarStdout.test_dup2_print_printf_and_fflush_stdout
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

$ python tests/mock_stdout_tests.py TestFooBarStdout.test_dup2_print_fprintf_stdout_and_fflush_stdout
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

$ python tests/mock_stdout_tests.py TestFooBarStdout.test_dup2_print_fprintf_stderr_and_fflush_stderr
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Share this post on:

Previous Post
Docker Symbolic Linked Volumes
Next Post
Makefile of Dot Config Files