Embedded C-Code mit Python testen

Alexander.Steffen@infineon.com
ese19.asdn.eu

Motivation

The C language combines all the power of assembly language with all the ease-of-use of assembly language.

Python-Testwerkzeuge

Tests mit C

  • gleiche Sprache für Tests und Code
  • Tests auf echter Hardware

Kapitel 1

Abnahmetest
Systemtest
Integrationstest
Komponententest

Beispiel 1: Funktionsaufruf

In [3]:
!cat add.h
int add(int a, int b);
In [4]:
!cat add.c
#include "add.h"

int add(int a, int b)
{
	return a + b;
}
In [52]:
class AddTest(unittest.TestCase):
    def test_addition(self):
        module = load('add')
        self.assertEqual(module.add(1, 2), 1 + 2)

run(AddTest)
test_addition (__main__.AddTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.128s

OK
In [7]:
def load(filename):
    # load source code
    source = open(filename + '.c').read()
    includes = open(filename + '.h').read()
    
    # pass source code to CFFI
    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(filename + '_', source)
    ffibuilder.compile()
    
    # import and return resulting module
    module = importlib.import_module(filename + '_')
    return module.lib   

Beispiel 2: interner Zustand

In [8]:
!cat sum.h
int sum(int a);
In [9]:
!cat sum.c
#include "sum.h"

static int _sum = 0;

int sum(int a)
{
	_sum += a;
	return _sum;
}
In [11]:
class SumTest(unittest.TestCase):
    def setUp(self):
        self.module = load('sum')
        
    def test_zero(self):
        self.assertEqual(self.module.sum(0), 0)

    def test_one(self):
        self.assertEqual(self.module.sum(1), 1)

    def test_multiple(self):
        self.assertEqual(self.module.sum(2), 2)
        self.assertEqual(self.module.sum(4), 2 + 4)

run(SumTest)
test_multiple (__main__.SumTest) ... ok
test_one (__main__.SumTest) ... ok
test_zero (__main__.SumTest) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.320s

OK
In [12]:
def load(filename):
    source = open(filename + '.c').read()
    includes = open(filename + '.h').read()
    
    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(filename + '_', source)
    ffibuilder.compile()
    
    module = importlib.import_module(filename + '_')
    return module.lib
In [13]:
def load(filename):
    # generate random name
    name = filename + '_' + uuid.uuid4().hex
    
    source = open(filename + '.c').read()
    includes = open(filename + '.h').read()

    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(name, source)
    ffibuilder.compile()
    
    module = importlib.import_module(name)
    return module.lib

Beispiel 3: Präprozessor

In [14]:
!cat types.h
typedef struct {
	float real;
	float imaginary;
} complex;
In [15]:
!cat complex.h
#include "types.h"

complex add(complex a, complex b);
In [16]:
!cat complex.c
#include "complex.h"

complex add(complex a, complex b)
{
	a.real += b.real;
	a.imaginary += b.imaginary;
	return a;
}
In [53]:
class ComplexTest(unittest.TestCase):
    def setUp(self):
        self.module = load('complex')
        
    def test_addition(self):
        result = self.module.add([0, 1], [2, 3])
        self.assertAlmostEqual(result.real, 2)
        self.assertAlmostEqual(result.imaginary, 4)

run(ComplexTest)
test_addition (__main__.ComplexTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.145s

OK
In [19]:
def load(filename):
    name = filename + '_' + uuid.uuid4().hex
    
    source = open(filename + '.c').read()
    includes = open(filename + '.h').read()

    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(name, source)
    ffibuilder.compile()
    
    module = importlib.import_module(name)
    return module.lib
In [20]:
def load(filename):
    name = filename + '_' + uuid.uuid4().hex

    source = open(filename + '.c').read()
    # handle preprocessor directives
    includes = preprocess(open(filename + '.h').read())
    
    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(name, source)
    ffibuilder.compile()
    
    module = importlib.import_module(name)
    return module.lib
In [21]:
def preprocess(source):
    return subprocess.run(['gcc', '-E', '-P', '-'],
                          input=source, stdout=subprocess.PIPE,
                          universal_newlines=True, check=True).stdout

Beispiel 4: Mocking

In [22]:
!cat gpio_lib.h
int read_gpio0(void);
int read_gpio1(void);
In [23]:
!cat gpio.h
int read_gpio(int number);
In [24]:
!cat gpio.c
#include "gpio.h"
#include "gpio_lib.h"

int read_gpio(int number)
{
	switch (number)
	{
		case 0:
			return read_gpio0();
		case 1:
			return read_gpio1();
		default:
			return -1;
	}
}
In [26]:
class GPIOTest(unittest.TestCase):
    def setUp(self):
        self.module, self.ffi = load2('gpio')
        
    def test_read_gpio0(self):
        @self.ffi.def_extern()
        def read_gpio0():
            return 42
        self.assertEqual(self.module.read_gpio(0), 42)
        
    def test_read_gpio1(self):
        read_gpio1 = unittest.mock.MagicMock(return_value=21)
        self.ffi.def_extern('read_gpio1')(read_gpio1)
        self.assertEqual(self.module.read_gpio(1), 21)
        read_gpio1.assert_called_once_with()
        
run(GPIOTest)
test_read_gpio0 (__main__.GPIOTest) ... ok
test_read_gpio1 (__main__.GPIOTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.277s

OK
In [27]:
def load(filename):
    name = filename + '_' + uuid.uuid4().hex

    source = open(filename + '.c').read()
    includes = preprocess(open(filename + '.h').read())
    
    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(name, source)
    ffibuilder.compile()
    
    module = importlib.import_module(name)
    return module.lib
In [28]:
def load2(filename):
    name = filename + '_' + uuid.uuid4().hex

    source = open(filename + '.c').read()
    # preprocess all header files for CFFI
    includes = preprocess(''.join(re.findall('\s*#include\s+.*', source)))

    # prefix external functions with extern "Python+C"
    local_functions = FunctionList(preprocess(source)).funcs
    includes = convert_function_declarations(includes, local_functions)

    ffibuilder = cffi.FFI()
    ffibuilder.cdef(includes)
    ffibuilder.set_source(name, source)
    ffibuilder.compile()

    module = importlib.import_module(name)
    # return both the library object and the ffi object
    return module.lib, module.ffi
In [29]:
class FunctionList(pycparser.c_ast.NodeVisitor):
    def __init__(self, source):
        self.funcs = set()
        self.visit(pycparser.CParser().parse(source))
        
    def visit_FuncDef(self, node):
        self.funcs.add(node.decl.name)
In [30]:
class CFFIGenerator(pycparser.c_generator.CGenerator):
    def __init__(self, blacklist):
        super().__init__()
        self.blacklist = blacklist
        
    def visit_Decl(self, n, *args, **kwargs):
        result = super().visit_Decl(n, *args, **kwargs)
        if isinstance(n.type, pycparser.c_ast.FuncDecl):
            if n.name not in self.blacklist:
                return 'extern "Python+C" ' + result
        return result

def convert_function_declarations(source, blacklist=set()):
    return CFFIGenerator(blacklist).visit(pycparser.CParser().parse(source))

Kapitel 2

Abnahmetest
Systemtest
Integrationstest
Komponententest

Konzept

Applikation
HAL
Applikation
Python

Beispiel-Projekt

In [31]:
![ -e micropython ] || git clone https://github.com/micropython/micropython -b v1.11
cat application/*.c
CFFI gcc python
cat hal/*.h

Teil 1: Quellcode

In [32]:
source_files = [
    'micropython/ports/minimal/main.c',
    'micropython/ports/minimal/build/_frozen_mpy.c',
    'micropython/lib/utils/pyexec.c',
    'micropython/lib/mp-readline/readline.c',
]
source_files.extend(glob.glob('micropython/py/*.c'))
cat application/*.c
CFFI gcc python
cat hal/*.h

Teil 2: Header

In [33]:
header_files = {os.path.join('micropython', 'py', 'mphal.h')}
In [35]:
header_content = '#define __attribute__(x)\n#define mp_hal_pin_obj_t void*\n'
header_content += ''.join(open(f).read() for f in header_files)
header_content = preprocess(header_content)
In [36]:
sorted(header_content.split('\n'))[-3:]
Out[36]:
['void mp_hal_stdout_tx_str(const char *str);',
 'void mp_hal_stdout_tx_strn(const char *str, size_t len);',
 'void mp_hal_stdout_tx_strn_cooked(const char *str, size_t len);']
In [37]:
class CFFIGenerator(pycparser.c_generator.CGenerator):
    def __init__(self, blacklist):
        super().__init__()
        self.blacklist = blacklist
        
    def visit_Decl(self, n, *args, **kwargs):
        result = super().visit_Decl(n, *args, **kwargs)
        if isinstance(n.type, pycparser.c_ast.FuncDecl):
            if n.name not in self.blacklist:
                return 'extern "Python+C" ' + result
        return result

def convert_function_declarations(source, blacklist=set()):
    return CFFIGenerator(blacklist).visit(pycparser.CParser().parse(source))
In [38]:
class CFFIGenerator2(CFFIGenerator):
    def visit_FuncDef(self, n, *args, **kwargs):
        # remove definitions of inline functions
        return ''

CFFIGenerator = CFFIGenerator2
In [39]:
header_content = convert_function_declarations(header_content)
header_content += 'int mpmain(int argc, char **argv);'
In [40]:
sorted(header_content.split('\n'))[6:9]
Out[40]:
['extern "Python+C" void mp_hal_delay_us(mp_uint_t us);',
 'extern "Python+C" void mp_hal_stdout_tx_str(const char *str);',
 'extern "Python+C" void mp_hal_stdout_tx_strn(const char *str, size_t len);']
cat application/*.c
CFFI gcc python
cat hal/*.h

Teil 3: CFFI

In [42]:
include_dirs = [
    'micropython',
    'micropython/ports/minimal',
    'micropython/ports/minimal/build',
]
In [43]:
ffibuilder = cffi.FFI()
ffibuilder.cdef(header_content)     
ffibuilder.set_source(
    module_name='mpsim',
    source='',
    sources=source_files,
    include_dirs=include_dirs,
    define_macros=[('main', 'mpmain')],
)
ffibuilder.compile();
cat application/*.c
CFFI gcc python
cat hal/*.h

Teil 4: Demo

In [44]:
import mpsim
In [45]:
@mpsim.ffi.def_extern()
def mp_hal_stdin_rx_chr():
    return ord(sys.stdin.read(1))
In [46]:
@mpsim.ffi.def_extern()
def mp_hal_stdout_tx_strn(data, length):
    print(bytes(mpsim.ffi.buffer(data, length)).decode(), end='', flush=True)
In [47]:
@mpsim.ffi.def_extern()
def mp_hal_stdout_tx_str(data):
    print(mpsim.ffi.string(data).decode(), end='', flush=True)
$ ./demo.py 
MicroPython v1.11 on 2019-12-04; minimal with unknown-cpu
>>> 1+1
2
>>> dir()
['__name__']
cat application/*.c
CFFI gcc python
cat hal/*.h

Fallstricke

Dateistruktur

$ ls -l bad/
-rw-r--r-- 1 user user 397789  1. Jan  1970  main.c
$ ls -l good/
drwxr-xr-x 2 user user 4096  1. Jan  1970  application
drwxr-xr-x 2 user user 4096  1. Jan  1970  hal

Plattform-Abhängigkeiten

struct {
    unsigned short major_version;
    unsigned int minor_version;
} data;

data.major_version = 1234;
data.minor_version = 567890;

checksum = sha256(&data, sizeof(data));
struct {
    uint16_t major_version;
    uint32_t minor_version;
} __attribute__((packed)) data;

data.major_version = htons(1234);
data.minor_version = htonl(567890);

checksum = sha256(&data, sizeof(data));

Interrupts

  • Noch nicht getestet :(

Kommunikationsschnittstellen

Applikation Applikation
HAL Python
Hardware Code
Netzwerk

Neue Möglichkeiten

Schnelle Test-Ausführung

In [49]:
matplotlib.rcParams.update({'font.size': 20})
matplotlib.pyplot.figure(figsize=(10, 5))
matplotlib.pyplot.bar([1, 2], [5512/60, 334/60])
matplotlib.pyplot.ylabel('Laufzeit (Minuten)')
matplotlib.pyplot.xticks([1, 2], ('Hardware', 'Simulation'))
matplotlib.pyplot.show()

Dynamische Analyse

AddressSanitizer (ASan)

In [ ]:
ffibuilder.set_source('mpsim', '', extra_compile_args=['-fsanitize-address'], libraries=['asan'])

American Fuzzy Lop (afl)

In [ ]:
os.environ['CC'] = 'afl-gcc'
In [ ]:
import afl
stdin = sys.stdin.detach()
while afl.loop(10000):
    application.lib.run(stdin.read())

Vielen Dank!

Fragen?

Embedded C-Code mit Python testen

Alexander.Steffen@infineon.com
ese19.asdn.eu