Тестирование проектов C-C++ с помощью Python

в 8:36, , рубрики: c/c++, c++, python, tdd, программирование микроконтроллеров

Введение

Хорошо известна возможность интеграции Python и C / C++. Как правило, этот прием используется для ускорения программ на Python или с целью подстройки программ на C / C++. Я хотел бы осветить возможность использование python для тестирования кода на C/C++ в IDE без поддержки системы организации тестов в IDE. С моей точки зрения это целесообразно применять в сфере разработки программного обеспечения для микроконтроллеров.

Можно много рассуждать на тему необходимости тестов в проектах, я исхожу из того что тесты помогают мне разрабатывать функционал программы. И после завершения проекта, по прошествии некоторого времени, помогают в нем разобраться и уберегают от ошибок.

При разработке программ для микроконтроллеров, я сталкивался с отсутствием стандартного ввода / вывода (конечно можно переопределить функции ввода вывода и в симуляторе, выводить данные через UART — но часто UART уже задействован, да и симулятор работает не всегда корректно) и большими рисками вывести из строя аппаратное обеспечение ошибочной бизнес логикой. На стадии разработки, я реализовывал отдельные проекты, тестирующие части программы и далее на меня ложилась ответственность за запуск всех тестовых приложений после внесения изменений. Конечно, это все можно автоматизировать. Так можно работать, но я нашел способ лучше.

Описание методики

Для покрытия тестами отдельных модулей проекта на С / С++ возможно использовать python (а именно ctypes). Суть методики сводится к созданию изолированных частей, реализующих часть функциональности в виде динамически линкуемых библиотек (dll), подаче на вход данных и контроле результата. Python используется в качестве «обвязки». Данная методика не подразумевает внесение изменений в код тестируемого приложения.

Для тестирования отдельных кусков кода возможно понадобится создать дополнительный файл с / с++ — “адаптер”, для борьбы с именованием перегруженных функций (вопросы по именованию экспортируемых функций подробно освещены в habrahabr.ru/post/150327) или с функциональностью имеющей сложные зависимости и тяжело реализуемой в «идеологии» dll.

Необходимое программное окружение

Данная методика подразумевает возможность компиляции отдельных частей программы из командной строки. Так что нам понадобится компилятор c / c++, и интерпретатор python. Я например использую GCC ( для форточек — MinGW (MinGw www.mingw.org ), python ( www.python.org ), ну а в дистрибутивах linux как правило все что нужно установлено по умолчанию).

Пример использования

Для иллюстрации данной методики приведу следующий пример:
исходный проект:

структура файлов:

+---Project
    |   Makefile
    +---src
        +---api
        |       ApiClass.cpp
        |       ApiClass.h
        |       ApiFunction.cpp
        |       ApiFunction.h
        |       
        ---user
                main.cpp

Файлы проекта:

Файл ApiFunction.cpp

#include "ApiFunction.h"

#include <cstring>

int apiFunction(int v1, int v2){
	return v1*v2;
}
void apiFunctionMutablePointer(double * value){


	* value = *value * 100;
}


Data apiFunctionGetData(){

	Data dt;

	dt.intValue = 1;
	dt.doubleValue = 3.1415;
	dt.ucharValue = 0xff;

	return dt;
}

Data GLOBAL_DATA;


Data * apiFunctionGetPointerData(){

	GLOBAL_DATA.intValue = 1*2;
	GLOBAL_DATA.doubleValue = 3.1415*2;
	GLOBAL_DATA.ucharValue = 0xAA;

	return &GLOBAL_DATA;
}

void apiFunctionMutablePointerData(Data * data){
	data->intValue = data->intValue * 3;
	data->doubleValue = data->doubleValue *3;
	data->ucharValue = data->ucharValue * 3;
}


BigData apiFunctionGetBigData(){
	BigData bd;

	bd.iv = 1;
	bd.v1 = 2;
	bd.v2 = 3;
	bd.v3 = 4;
	bd.v4 = 5;

	std::memset(bd.st,0,12);
	std::memmove(bd.st,"hello world",12);

	return bd;
}
Файл ApiFunction.h

#ifndef SRC_API_APIFUNCTION_H_
#define SRC_API_APIFUNCTION_H_


#ifdef __cplusplus
extern "C" {
#endif

int apiFunction(int v1, int v2);

void apiFunctionMutablePointer(double * value);

struct Data{
	int intValue;
	double doubleValue;
	unsigned char ucharValue;
};


struct BigData{
	int iv;
	int v1:4;
	int v2:4;
	int v3:8;
	int v4:16;

	char st[12];

};


Data apiFunctionGetData();

Data * apiFunctionGetPointerData();

void apiFunctionMutablePointerData(Data * data);

BigData apiFunctionGetBigData();


#ifdef __cplusplus
}
#endif

#endif
Файл ApiClass.cpp

#include "ApiClass.h"
#include <iostream>

ApiClass::ApiClass():value(0) {
	std::cout<<std::endl<<"create ApiClass value = "<<value<<std::endl;
}
ApiClass::ApiClass(int startValue):
		value(startValue){
	std::cout<<std::endl<<"create ApiClass value = "<<value<<std::endl;
}

ApiClass::~ApiClass() {
	std::cout<<std::endl<<"delete ApiClass"<<std::endl;
}

int ApiClass::method(int vl){
	value +=vl;
	return value;
}
Файл ApiClass.h

#ifndef SRC_API_APICLASS_H_
#define SRC_API_APICLASS_H_

class ApiClass {
public:
	ApiClass();
	ApiClass(int startValue);
	virtual ~ApiClass();

	int method(int vl);


private:
		int value;
};

#endif
Файл main.cpp

#include <iostream>

#include "ApiFunction.h"
#include "ApiClass.h"

int main(){
	std::cout<<"start work"<<std::endl;
	std::cout<<"=============================================="<<std::endl;
	std::cout<<"call apiFunction(10,20) = "<<apiFunction(10,20)<<std::endl;
	std::cout<<"call apiFunction(30,40) = "<<apiFunction(30,40)<<std::endl;

	std::cout<<"=============================================="<<std::endl;
	ApiClass ac01;
	std::cout<<"call ac01.method(30) = "<<ac01.method(30)<<std::endl;
	std::cout<<"call ac01.method(40) = "<<ac01.method(40)<<std::endl;

	std::cout<<"=============================================="<<std::endl;
	ApiClass ac02(10);
	std::cout<<"call ac02.method(30) = "<<ac02.method(30)<<std::endl;
	std::cout<<"call ac02.method(40) = "<<ac02.method(40)<<std::endl;
}
файл makefile
FOLDER_EXECUTABLE = bin/
EXECUTABLE_NAME = Project.exe

EXECUTABLE = $(FOLDER_EXECUTABLE)$(EXECUTABLE_NAME)
FOLDERS = bin bin/src bin/src/api bin/src/user
SOURSES = src/user/main.cpp src/api/ApiClass.cpp src/api/ApiFunction.cpp

CC = g++
CFLAGS = -c -Wall -Isrc/helper -Isrc/api 
LDFLAGS = 
OBJECTS = $(SOURSES:.cpp=.o)
OBJECTS_PATH = $(addprefix $(FOLDER_EXECUTABLE),$(OBJECTS))

all: $(SOURSES) $(EXECUTABLE)

$(EXECUTABLE): $(OBJECTS)	
	$(CC) $(LDLAGS) $(OBJECTS_PATH) -o $@
	
.cpp.o:
	mkdir -p $(FOLDERS)
	$(CC) $(CFLAGS) $< -o $(FOLDER_EXECUTABLE)$@

clean:
	rm -rf $(OBJECTS) $(EXECUTABLE) 

Для покрытия тестами в папку проекта добавляем папку test. В данной папке у нас будет все, что связано с тестированием.

Для удобства создадим в папке test папку helpers (python package не забываем создать внутри файл __init__.py) – в ней будут общие для всех тестов вспомогательные функции.
Вспомогательные функции из пакета helpers:

Файл callCommandHelper.py

import subprocess

class CallCommandHelperException(Exception):
    pass    

def CallCommandHelper(cmd):
    with subprocess.Popen(cmd, stdout=subprocess.PIPE,shell=True) as proc:
        if proc.wait() != 0:            
            raise CallCommandHelperException("error :" +cmd)
Файл creteDll.py

import os
from helpers import callCommandHelper

def CreateDll(folderTargetName, fileTargetName,fileSO):
    
    templateCompill = "g++  {flags}  {fileSourse} -o {fileTarget}"
    templateLinc    = "g++  -shared {objectfile} -o {fileTarget}"


    if os.path.exists(folderTargetName) == False:
        os.makedirs(folderTargetName)
    
#---------------delete old version-----------------------------------
    if os.path.exists(fileTargetName):
        os.remove(fileTargetName)        
    for fso in fileSO:
        if os.path.exists(fso["rezultName"]):
            os.remove(fso["rezultName"])            
#---------------compil -----------------------------------------------    
    for filePair in fileSO:
        fileSourseName  =  filePair["sourseName"]
        fileObjecteName = filePair["rezultName"]
        flagCompil = filePair["flagsCompil"]
        cmd = templateCompill.format(
            fileSourse = fileSourseName,
            flags      = flagCompil, 
            fileTarget = fileObjecteName)        
        
        callCommandHelper.CallCommandHelper(cmd)   
#---------------linck-----------------------------------------------
    fileObjectName = " "
    for filePair in fileSO:
        fileObjectName = fileObjectName + filePair["rezultName"]+" "
    
    
    cmd = templateLinc.format(
        objectfile = fileObjectName,
        fileTarget = fileTargetName)    
    
    callCommandHelper.CallCommandHelper(cmd)    
#====================================================== 

Примечание: Если вы используете компилятор, отличный от gcc, то необходимо исправить название программ в переменных templateCompill и templateLinc.

В файле creteDll.py происходит все волшебство создания тестовой dll. Я просто создаю для используемой операционной системы команды для компиляции и линковки (сборки) dll. Как вариант возможно создать шаблон makefile и подставлять туда названия файлов, но мне так показалось проще. (вообще как я понимаю всю работу по тестированию можно вынести в makefile но мне это кажется сложным, да и проекты создаваемые в keil или в других IDE не всегда строятся на makefile).

На этом завершена вся подготовка, теперь можем приступать к тестированию.

Простое создание теста

Рассмотрим вариант создание теста без использования адаптера.

Протестируем функции из файлов АpiFunction.h / АpiFunction.cpp.

Создаем в папке test папку для ApiFunctionTest для создаваемой dll. Создадим пайтоновский файл для выполнения теста, с использованием модуля unittest. В методе setUpClass происходит создание dll, загрузка и “настройка” функций. И позднее нам необходимо написать стандартные методы для тестирования.

Файл apiFunctionTest.py

import os
import ctypes

from helpers import creteDll

import unittest

class Data(ctypes.Structure):
    _fields_ = [("intValue",ctypes.c_int),("doubleValue",ctypes.c_double),("ucharValue",ctypes.c_ubyte)]

class BigData(ctypes.Structure):
    _fields_ = [("iv",ctypes.c_int),
                ("v1",ctypes.c_int,4),
                ("v2",ctypes.c_int,4),
                ("v3",ctypes.c_int,8),
                ("v4",ctypes.c_int,16),
                ("st",ctypes.c_char*12)]

class ApiFunctionTest(unittest.TestCase):
    @classmethod
    def setUpClass(self):
       

        folderTargetName = os.path.join(os.path.dirname(__file__),"ApiFunctionTest")
       
        fileSO =  [
                    {"sourseName":"../src/api/ApiFunction.cpp",
                    "flagsCompil":"-Wall -c -fPIC",
                    "rezultName" :os.path.join(folderTargetName,"ApiFunction.o")}
                  ]
                    
        fileTargetName = os.path.join(folderTargetName,"ApiFunction.dll")
                
        #=============================================================
        creteDll.CreateDll(folderTargetName, fileTargetName, fileSO)
        
        
        lib = ctypes.cdll.LoadLibrary(fileTargetName)
    
        self.apiFunction = lib.apiFunction
        self.apiFunction.restype = ctypes.c_int

        self.apiFunctionMutablePointer = lib.apiFunctionMutablePointer
        self.apiFunctionMutablePointer.argtype  = ctypes.POINTER(ctypes.c_double)
        
        
        self.apiFunctionGetData = lib.apiFunctionGetData
        self.apiFunctionGetData.restype = Data
        
        
        self.apiFunctionGetPointerData = lib.apiFunctionGetPointerData
        self.apiFunctionGetPointerData.restype = ctypes.POINTER(Data)
         
        self.apiFunctionMutablePointerData = lib.apiFunctionMutablePointerData
        self.apiFunctionMutablePointerData.argtype  = ctypes.POINTER(Data)
       
        
        self.apiFunctionGetBigData = lib.apiFunctionGetBigData
        self.apiFunctionGetBigData.restype = BigData
        
       
        
    def test_var1(self):
        self.assertEqual(self.apiFunction(10,20), 200,'10*20 = 200')


    def test_var2(self):
        self.assertEqual(self.apiFunction(30,40), 1200,'30*40 = 1200')


    def test_var3(self):
        vl = ctypes.c_double(1.1)        
        self.apiFunctionMutablePointer(ctypes.pointer(vl) )
        self.assertEqual(vl.value, 110.00000000000001,'vl != 110')
        
    def test_var4(self):
        data = self.apiFunctionGetData()
        self.assertEqual(data.intValue, 1,'data.intValue != 1')
        self.assertEqual(data.doubleValue, 3.1415,'data.doubleValue != 3.1415')
        self.assertEqual(data.ucharValue, 0xff,'data.ucharValue != 0xff')
               
    def test_var5(self):
        pointerData = self.apiFunctionGetPointerData()    
        
        self.assertEqual(pointerData.contents.intValue, 1*2,'data.intValue != 1*2')
        self.assertEqual(pointerData.contents.doubleValue, 3.1415*2,'data.doubleValue != 3.1415 * 2')
        self.assertEqual(pointerData.contents.ucharValue, 0xAA,'data.ucharValue != 0xAA')
       

        
    def test_var5(self):
        pointerData = ctypes.pointer(Data())
        pointerData.contents.intValue = ctypes.c_int(10)
        pointerData.contents.doubleValue = ctypes.c_double(20)
        pointerData.contents.ucharValue = ctypes.c_ubyte(85)
       
        self.apiFunctionMutablePointerData(pointerData)
                 
        self.assertEqual(pointerData.contents.intValue, 30,'data.intValue != 30')
        self.assertEqual(pointerData.contents.doubleValue, 60,'data.doubleValue != 60')
        self.assertEqual(pointerData.contents.ucharValue, 0xff,'data.ucharValue != 0xff')
             
    def test_var6(self):
        
        bigData = self.apiFunctionGetBigData()
        st = ctypes.c_char_p(bigData.st).value
                
        self.assertEqual(bigData.iv, 1,'1')
        self.assertEqual(bigData.v1, 2,'2')
        self.assertEqual(bigData.v2, 3,'3')
        self.assertEqual(bigData.v3, 4,'4')
        self.assertEqual(bigData.v4, 5,'5')
        
        
        self.assertEqual(st in b"hello world",True,'getting string')

Примечание: Если вы используете компилятор, отличный от gcc, то необходимо исправить строку с ключом flagsCompil.

Как видите для тестирования нет необходимости в каких либо дополнительных действиях. Мы ограничены только фантазией создания тестовых сценариев. В данном примере продемонстрированы возможности передачи в сишные функции и получения из них различных типов данных (более подробно это описано в документации ctypes).

Создание теста с использованием «адаптера»

Рассмотрим вариант создание теста с использованием «адаптера».

Протестируем класс ApiClassиз файлов ApiClass.h / ApiClass.cpp. Как видите у данного класса есть несколько вариантов создания, также он сохраняет состояние между вызовами. Создаем в папке test папку для ApiClassTest для создаваемой dll, и «адаптера» — ApiClassAdapter.cpp.

Файл ApiClassAdapter.cpp

#include "ApiClass.h"

#ifdef __cplusplus
extern "C" {
#endif

ApiClass * pEmptyApiClass = 0;
ApiClass * pApiClass = 0;

void createEmptyApiClass(){
	if(pEmptyApiClass != 0){
		delete pEmptyApiClass;
	}
	pEmptyApiClass = new ApiClass;
}
void deleteEmptyApiClass(){
	if(pEmptyApiClass != 0){
		delete pEmptyApiClass;
		pEmptyApiClass=0;
	}
}

void createApiClass(int value){
	if(pApiClass != 0){
		delete pApiClass;
	}
	pApiClass = new ApiClass(value);
}
void deleteApiClass(){
	if(pApiClass != 0){
		delete pApiClass;
		pApiClass=0;
	}
}

int callEmptyApiClassMethod(int vl){
	return pEmptyApiClass->method(vl);
}

int callApiClassMethod(int vl){
	return pApiClass->method(vl);
}


#ifdef __cplusplus
}
#endif

Как видите «адаптер» просто оборачивает вызовы класса ApiClass для удобства вызовов из python.

Для тестирования данного класса создадим файл apiClassTest.py.

Файл apiClassTest.py

import os
import ctypes

from helpers import creteDll

import unittest

class ApiClassTest(unittest.TestCase):
    @classmethod
    def setUpClass(self):

                
        folderTargetName = os.path.join(os.path.dirname(__file__),"ApiClassTest")
        
        
        fileSO =  [
                    {
                    "sourseName":os.path.abspath("../src/api/ApiClass.cpp"),
                    "flagsCompil":"-Wall -c -fPIC",
                    "rezultName" :os.path.join(folderTargetName,"ApiClass.o")
                    },
                    {
                    "sourseName":os.path.join(folderTargetName,"ApiClassAdapter.cpp"),
                    "flagsCompil":"-Wall -c -fPIC -I../src/api",
                    "rezultName" :os.path.join(folderTargetName,"ApiClassAdapter.o")
                    }
                   ]
                   
        fileTargetName = os.path.join(folderTargetName,"ApiClass.dll")
        #======================================================
        creteDll.CreateDll(folderTargetName, fileTargetName, fileSO)    
#======================================================
        lib = ctypes.cdll.LoadLibrary(fileTargetName)
    
        self.createEmptyApiClass = lib.createEmptyApiClass        
        self.deleteEmptyApiClass = lib.deleteEmptyApiClass        
    
        self.callEmptyApiClassMethod = lib.callEmptyApiClassMethod
        self.callEmptyApiClassMethod.restype = ctypes.c_int
        
        self.createApiClass = lib.createApiClass        
        self.deleteApiClass = lib.deleteApiClass        
    
        self.callApiClassMethod = lib.callApiClassMethod
        self.callApiClassMethod.restype = ctypes.c_int
        
    
    def tearDown(self):
        self.deleteEmptyApiClass()
        self.deleteApiClass()
    
        
    def test_var1(self):
        self.createEmptyApiClass()
        self.assertEqual(self.callEmptyApiClassMethod(10), 10,'10+0 = 10')
        self.assertEqual(self.callEmptyApiClassMethod(20), 30,'20+10 = 30')


    def test_var2(self):
        self.createApiClass(100)
        self.assertEqual(self.callApiClassMethod(10), 110,'10+100 = 110')
        self.assertEqual(self.callApiClassMethod(20), 130,'20+110 = 130')

Тут следует обратить внимание на метод tearDown, в нем после каждого тестового метода удаляются создаваемые в dll объекты, для предотвращения утечек памяти (в данном контексте это не имеет особого значения).

Ну и объединение всех тестов в файле TestRun.py

файл TestRun.py

import unittest
loader = unittest.TestLoader()
suite = loader.discover(start_dir='.', pattern='*Test.py')
runner = unittest.TextTestRunner(verbosity=5)
result = runner.run(suite)

Запуск всех тестов

В командной строке набираем:

python TestRun.py

(или запускаем отдельные тесты, например так: python -m unittest apiFunctionTest.py) и радуемся результатам.

Недостатки данной методики

К недостаткам данной методики следует отнести:

  • относительную сложность алгоритма создания dll.
  • Возможные проблемы, связанные с согласованностью типов и вопросы выравнивания в структурах.
  • Отладка возможных ошибок в файле «адаптера».
  • Большое время компиляции отдельных dll.
  • Необходимо правильно и вручную выбирать ключи компиляции.
  • Необходимость установки дополнительного ПО.

Выводы

Конечно, хорошо использовать IDE со встроенной поддержкой тестов, но если таковой нет, то данная методика позволяет намного облегчить жизнь. Достаточно один раз потратить время на настройку систему тестирования проекта. Также следует отметить что возможно использовать возможности синтаксического анализа python для генерации «живой» документации да и вообще возможности python для работы с текстами программы на С / С++.

Ссылка на архив проекта.

Спасибо за внимание.

Автор: Alexey00007

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js