Pythonを使用したC / C ++プロジェクトのテスト

はじめに



PythonとC / C ++を統合する機能はよく知られています。 通常、この手法は、Pythonプログラムを高速化するため、またはC / C ++プログラムを微調整するために使用されます。 IDEのテスト組織システムをサポートせずに、Pythonを使用してIDEでC / C ++コードをテストする可能性を強調したいと思います。 私の観点からは、マイクロコントローラのソフトウェア開発の分野に適用することをお勧めします。



プロジェクトでのテストの必要性について多くのことを話すことができます。テストはプログラムの機能の開発に役立つと思います。 そして、プロジェクトの完了後、しばらくしてから、彼らはそれを見つけ出し、間違いを防ぐのを助けます。



マイクロコントローラー用のプログラムを開発するとき、標準の入出力の不足に直面しました(もちろん、シミュレーターで入出力関数を再定義し、UARTを介してデータを出力できますが、多くの場合UARTが既に関与しており、シミュレーターが常に正しく動作しない場合があります)、無効にする大きなリスクがありますハードウェアエラーのあるビジネスロジック。 開発段階では、プログラムの一部をテストする個々のプロジェクトを実装し、変更を加えた後にすべてのテストアプリケーションを起動する責任がありました。 もちろん、これはすべて自動化できます。 これは機能しますが、より良い方法を見つけました。



テクニックの説明



Python(つまりctypes)を使用して、C / C ++プロジェクトの個々のモジュールのテストをカバーすることができます。 この手法の本質は、動的にリンクされたライブラリ(dll)の形で機能の一部を実装する分離された部分を作成し、データをフィードし、結果を制御することです。 Pythonは「バインディング」として使用されます。 この手法は、テスト対象のアプリケーションのコードの変更を意味するものではありません。



個々のコードをテストするには、/ c ++-「アダプター」を使用して追加のファイルを作成し、オーバーロードされた関数の名前と戦う必要があります(エクスポートされた関数の名前の質問についてはhabrahabr.ru/post/150327で詳しく説明されています ) 「イデオロギー」dllに実装するのは難しい。



必要なソフトウェア環境



この手法は、コマンドラインからプログラムの個々の部分をコンパイルできることを意味します。 したがって、c / c ++コンパイラとPythonインタプリタが必要です。 たとえば、私はGCCを使用します(Windowsの場合-MinGW (MinGw www.mingw.org )、python( www.python.org )が、Linuxディストリビューションでは、原則として、必要なものはすべてデフォルトでインストールされます)。



使用例



この手法を説明するために、次の例を示します。

ソースプロジェクト:



ファイル構造:



 + ---プロジェクト
     | メイクファイル
     + --- src
         + --- api
         |  Apiclass.cpp
         |  Apiclass.h
         |  ApiFunction.cpp
         |  ApiFunction.h
         |       
         \ ---ユーザー
                 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; }
      
      











メイクファイル
 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)
      
      







テストでカバーするには、テストフォルダーをプロジェクトフォルダーに追加します。 このフォルダには、テストに関連するすべてのものが含まれます。



便宜上、テストフォルダーにヘルパーフォルダーを作成します(pythonパッケージは、__ init__.pyファイルを内部に作成することを忘れないでください)-すべてのテストに共通の補助関数があります。

ヘルパーパッケージのヘルパー関数:



ファイル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に送信できますが、私には複雑に思え、keilまたは他のIDEで作成されたプロジェクトは常にmakefileでビルドされるわけではありません)。



これですべての準備が完了し、テストを開始できます。



簡単なテスト作成



アダプタを使用せずにテストを作成するオプションを検討してください。



ファイルApiFunction.h / ApiFunction.cppから関数をテストします。



作成したdllのApiFunctionTestのテストフォルダーにフォルダーを作成します。 unittestモジュールを使用して、テスト用のPythonファイルを作成します。 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.h / ApiClass.cppファイルからApiClassクラスをテストします。 ご覧のとおり、このクラスには作成のためのオプションがいくつかあり、呼び出し間の状態も保存されます。 テストフォルダーで、作成されたdllのApiClassTestのフォルダーと、「アダプター」-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)、結果を楽しみます。



この手法の欠点



この手法の欠点は次のとおりです。





結論



もちろん、テストサポートが組み込まれたIDEを使用するのは良いことですが、そうでない場合は、この手法を使えば非常に簡単になります。 プロジェクトテストシステムのセットアップに時間をかけるだけで十分です。 また、Pythonの解析機能を使用して「ライブ」ドキュメントを生成することもできます。実際、Pythonの機能を使用してC / C ++のプログラムテキストを操作することもできます。



プロジェクトアーカイブへのリンク



ご清聴ありがとうございました。



All Articles