Введение
Хорошо известна возможность интеграции 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
Файлы проекта:
#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;
}
#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
#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;
}
#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
#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;
}
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:
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)
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, загрузка и “настройка” функций. И позднее нам необходимо написать стандартные методы для тестирования.
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.
#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.
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
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