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

Введение

Хорошо известна возможность интеграции 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 для работы с текстами программы на С / С++.

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

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

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *