azbshiri azbshiri - 4 months ago 15
C Question

Pass Python's stdout to a C function to which write

I want to unit testing a C function which prints to the stdout but after I searched I reached to

os.dup
,
os.pip
and other stuff which isn't the coolest way to capture the stdout of a C shared library function. so I figured to pass Python's stdout to the C function after it writes to it then I can get the values to test but it doesn't work.
Here's the code:


compile the file as a shared library:
gcc -shared -Wl,-soname,tomat -o tomat.so -fPIC tomat.c



/* filename: tomat.c */
#include <stdio.h>
#include <unistd.h>

int
tomat(int length, FILE *stdout_)
{
int remain = 0;
while ((remain = (length -= 1)) != 0)
{
fprintf(stdout_, "00:%d\n", remain);
sleep(1);
}
return 0;
}





# filename: tomat_test.py
import sys
import ctypes
import unittest

class TomatTestCase(unittest.TestCase):
def setUp(self):
self.lib = ctypes.CDLL('./tomat.so')

def test_prints_every_second(self):
seconds = ['00:1', '00:2', '00:2', '00:3', '00:4', '00:5',
'00:6', '00:7', '00:8', '00:9']
self.lib.tomat(10, sys.stdout.fileno())
self.assertEqual(output, seconds[::-1])

Answer

Hope it'd help others.

import os
import sys
from tempfile import TemporaryFile
from io import TextIOWrapper, SEEK_SET
from contextlib import contextmanager
from ctypes import c_void_p


@contextmanager
def capture(stream, libc):
    osfd = sys.stdout.fileno()
    fd = os.dup(osfd)
    try:
        tfile = TemporaryFile(mode='w+b')
        redirect_stdout(tfile.fileno(), osfd, libc)
        yield
        redirect_stdout(fd, osfd, libc)
        tfile.flush()
        tfile.seek(0, SEEK_SET)
        stream.write(tfile.read())
    finally:
        tfile.close()
        os.close(fd)


def redirect_stdout(fd, osfd, libc):
    libc.fflush(c_void_p.in_dll(libc, 'stdout'))
    sys.stdout.close()
    os.dup2(fd, osfd)
    sys.stdout = TextIOWrapper(os.fdopen(osfd, 'wb'))

How I used it to test the output of the timer funciton:

from io import BytesIO
from ctypes import CDLL
from unittest import TestCase
from helpers import capture


class TomatTestCase(TestCase):
    def setUp(self):
        self.libc = CDLL('./tomat.so')
        self.maxDiff = None

    def test_prints_every_second(self):
        seconds = [
            '01:00', '01:01', '01:02', '01:03', '01:04', '01:05', '01:06',
            '01:07', '01:08', '01:09', '01:10', '01:11', '01:12', '01:13',
            '01:14', '01:15', '01:16', '01:17', '01:18', '01:19', '01:20',
            '01:21', '01:22', '01:23', '01:24', '01:25', '01:26', '01:27',
            '01:28', '01:29', '01:30', '01:31', '01:32', '01:33', '01:34',
            '01:35', '01:36', '01:37', '01:38', '01:39', '01:40', '01:41',
            '01:42', '01:43', '01:44', '01:45', '01:46', '01:47', '01:48',
            '01:49', '01:50', '01:51', '01:52', '01:53', '01:54', '01:55',
            '01:56', '01:57', '01:58', '01:59', '01:60']

        stream = BytesIO()
        with capture(stream, self.libc):
            self.libc.tomat(2)
        stream = stream.getvalue().split()
        output = [byte.decode() for byte in stream]

        self.assertListEqual(output, seconds)

for more information about how it works you can take a look at the Eli Bendersky's post: http://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/

Comments