Недавно открыл для себя Python ctypes как весьма удобный способ быстрого тестирования функций из динамических библиотек. ctypes позволяет произвести загрузку динамической библиотеки и реализовать вызов экспортируемых функций, при условии использования C-интерфейсов; при этом, библиотека предоставляет достаточно средств для передачи нетривиальных типов данных, таких как структуры, объединения, массивы и их комбинации.
Решение кроссплатформенное, может помочь как в разработке нового функционала, при остутствии клиентов, так и в последующем выявлении регрессий (если, например, включить вызов соответствующего скрипта в процедуру автоматического санити-теста).
В качестве примера привожу скрипт, которым я тестировал функцию с таким прототипом:
int get_ips(const unsigned short af, struct ipaddress_info ** ppaddrinfo);Структура ipaddress_info выглядит так:
typedef struct ipaddress_info {
unsigned short family;
unsigned char addr[16];
unsigned int scope_id;
} IPADDRESS_INFO;
Следующий скрипт (я использовал Python 3.2 в Windows 7 и Ubuntu 11.10) вызывает функцию get_ips из динамической библиотеки и отображает результат, включая содержимое массива адресов, построенного тестируемой функцией:import os
import sys
from ctypes import *
from socket import AF_INET, AF_INET6, AF_UNSPEC
# Python representation of a C struct defined
# in a shared library being tested
class ipaddress_info(Structure):
_fields_ = [
("family", c_ushort),
("addr", c_ubyte * 16),
("scope_id", c_uint),
]
def bytes2str(v):
s = ""
for x in v:
s += "%.02X " % (x)
return s
class dynlib_bridge:
def __init__(self, dll_path, working_dir="."):
tmp = os.getcwd()
os.chdir(working_dir)
self.invoke = cdll.LoadLibrary(dll_path)
os.chdir(tmp)
class test_case_base:
def __init__(self, real_values):
self.__inout__ = dict()
self.__result__ = None
for arg_name, arg_value in real_values.items():
self.__inout__[arg_name] = arg_value
def actual(self, name):
return self.__inout__[name]
def result(self):
return self.__result__
def setResult(self, v):
self.__result__ = v
# performs a positive test case
def test_pos(testobj):
result = testobj.run()
if result:
print ("[tc+] OK :", testobj.describe())
else:
print ("[tc+] FAIL:", testobj.describe())
# performs a negative test case
def test_neg(testobj):
result = testobj.run()
if not result:
print ("[tc-] OK :", testobj.describe())
else:
print ("[tc-] FAIL:", testobj.describe())
# wraps invocation of a shared library function
class get_ips(test_case_base):
def __init__(self, dll, af):
super(get_ips, self).__init__({
'af': c_ushort(af),
'ppaddrinfo': POINTER(ipaddress_info)(),
})
self.dll = dll
def run(self):
invoke_result = self.dll.invoke.get_ips(
self.actual('af'),
byref(self.actual('ppaddrinfo')))
self.setResult(invoke_result)
return (invoke_result > 0)
def describe(self):
p = self.actual('ppaddrinfo')
ips = []
if self.result() > 0:
for i in range(self.result()):
ips.append("#%d: family = %d, scope = %d, %s" % (
i,
p[i].family,
p[i].scope_id,
bytes2str(p[i].addr)))
s = "result: %.02d\n%s" % (self.result(), "\n".join(ips))
return s
if __name__ == "__main__":
path_to_lib = sys.argv[1]
path_to_env = sys.argv[2]
print ("lib path :", path_to_lib)
print ("env path :", path_to_env)
dll = dynlib_bridge(path_to_lib, working_dir=path_to_env)
test_pos(get_ips(dll, AF_UNSPEC))
test_pos(get_ips(dll, AF_INET))
test_pos(get_ips(dll, AF_INET6))
test_neg(get_ips(dll, 99))
Первый параметр - полный путь к тестируемой библиотеке. Второй параметр - путь из которого будет происходить загрузка (для Linux этого может оказаться недостаточно - нужно также настроить LD_LIBRARY_PATH, но это уже совсем другая тема).
Для упрощения примера из него удалена обработка ошибок и вызовы других функций тестируемой библиотеки (в том числе функции освобождения памяти, выделенной в get_ips). Также следует обратить внимание, что тестируемая библиотека для всех платформ была 32-битной и интересующие функции использовали cdecl соглашение о вызове.
Комментариев нет:
Отправить комментарий