在嵌入式软件产品开发过程中,通常会预留测试接口以便测试。由于产品所使用的语言可能是编译型的静态语言,如果测试程序也用一样的语言编写,虽然数据结构的一致性上没有问题,但是测试程序的灵活性上则受限于产品所用语言。
举例说明,在产品中有一个测试程序,使用了VC,有一些基础模块用于构造对应于消息的结构体,有跟主控通讯的模块,以及一个简单的逻辑判断模块做字段的比较,在产品开发前期该工具起到了很大作用,但是到后期则由于灵活性问题而使用较少,测试部较多的利用robot类的工具从界面上做测试。分析原因可以看出,由于缺乏通用语言的一些特性,导致该工具使用起来有一些缺陷,完成的任务也相对较简单。如果利用该工具的思路,但是添加上语言特性,则是本文的重点。
本文介绍一种通过swig做接口,利用脚本语言的动态性以及其他便利的语言特性简化测试程序的编写,可以克服上面提到的问题。
如果还是用c语言编程,要引入逻辑判断和分支比较的功能非常麻烦,等于是自己做一个脚本语言,难度非常大,并且会非常耗费时间。如何利用已有脚本语言的灵活性,并且能与目标语言c自动结合起来,把c语言中的函数和数据结构等封装为脚本语言的扩展,就可以达到我们的目的了。
下面就主要描述把c语言中的函数和数据结构描述为脚本扩展语言的流程:
1) 写一个接口文件
接口文件ter.i,是一个swig处理的输入文件其中包括了c中的数据结构声明和函数声明,以及用typemaps技术对c语言和脚本语言之间转换的一些控制。由于其中牵涉到一些细节,所以会详细讲解:
%module ter
%{
#include <stdio.h>
#include <stdlib.h>
#include "msg.h"
#include "udp.h"
%}
上面是按部就班的,包括模块名,以及需要引用的头文件,msg.h中包含了所有需要的数据结构。而udp.h则是对udp socket的简单封装,本来ruby中也有udp模块,不过发现按照例子代码,总是无法收发udp包,所以自己封装了一下(补充注释:其实ruby的udp模块很好用的,不需要封装,但是 Socket.do_not_reverse_lookup=true 这句话是不能少的,否则你就等着接收超时吧)。
由于很多数据结构中有unsigned char name[32]类似的数组,其内容可能是字符串,也可能是数组,由于这种字段默认的被swig封装为用户字段,从ruby脚本中读写比较麻烦,所以采用下面的代码,当写字段的时候,用ruby的string对其赋值,当读的时候,会封装为字符串输出(ruby的字符串可以包含/0字符,不过程序中添加了一个判断,如果在非结尾处遇到0,后面也全部是0,就把0前面的作为字符串输出,否则把整个数组内容作为ruby字符串)。
%typemap(memberin,noblock=1) unsigned char [ANY]{
}
%fragment("SetArr","header") {
void SetArr(VALUE str,unsigned char buf[],int len)
{
int minlen=min(len,RSTRING_LEN(str));
memset(buf,0,len);
memcpy(buf,RSTRING_PTR(str),minlen);
}
}
%typemap(in,noblock=1,fragment="SetArr") unsigned char [ANY] {
SetArr($input,arg1->$1_name,$1_dim0);
}
%fragment("GetArr","header") {
VALUE GetArr(unsigned char buf[],int len)
{
int i=0,next=0;
int isStrZ=0;
VALUE str;
for(i=0;i<len;i++)
{
if(buf[i]==0)
{
isStrZ=1;
break;
}
}
for(next=i+1;next<len;next++)
{
if(buf[next]!=0)
{
isStrZ=0;
}
}
if(isStrZ==0)
{
str=rb_str_new(buf,len);
}
else
{
str=rb_str_new2(buf);
}
return str;
}
}
%typemap(out,noblock=1,fragment="GetArr") unsigned char [ANY]{
$result=GetArr($1,$1_dim0);
}
%include "msg.h"
注意上面的fragment主要是为了节省代码空间,否则会到处都是一样的代码。而%typemap(memberin,noblock=1) unsigned char [ANY]这句则是取消swig预订的操作,使用我们的SetArr操作。
下面的代码就比较平凡了,主要是针对每个数据结构,添加一些方便的操作。
%define MAKE_STRUCT(T)
%extend T {
T() {
T *pT;
pT=(T *)malloc(sizeof(T));
memset((void *)pT,0,sizeof(*pT));
return pT;
}
~T(){
free($self);
}
int length(){
return sizeof(*$self);
}
T* shape(VALUE str){
char *buf=RSTRING_PTR(str);
int len=RSTRING_LEN(str);
int slen=sizeof(*$self);
if(len!=slen)
{
rb_raise(rb_eTypeError,"the struct %s has %d size, but %d size supplyed,not equal/n",#T,slen,len);
}
memcpy($self,buf,len);
return $self;
}
VALUE to_a(){
int i=0;
int len=sizeof(*$self);
char *ptr=(char *)$self;
VALUE arr = rb_ary_new2(len);
for(i=0;i<len;i++)
{
rb_ary_push(arr, INT2NUM(ptr[i]));
}
return arr;
}
VALUE to_binstr(){
char *buf=(char *)$self;
int len=sizeof(*$self);
return rb_str_new(buf,len);
}
VALUE to_s(){
VALUE s;
int i=0;
unsigned char buf[1024]={0};
int len=sizeof(T);
int sLen=0,sIdx=0;
unsigned char *ptr=(unsigned char *)$self;
for(i=0;i<len;i++)
{
sLen=sprintf(buf+sIdx,"//0x%02X",ptr[i]);
sIdx+=sLen;
if(sIdx>=1023)
break;
}
buf[sIdx>1023?1023:sIdx]=0;
s=rb_str_new2(buf);
return s;
}
};
%enddef
其中比较关键的shape就是将一个string对象转换成用户定义的数据结构对象,方便后面操作。在接口文件中添加MAKE_STRUCT(T_GeneralConfig) 就会把上面的函数功能添加到T_ GeneralConfig结构中。
Udp 的c代码如下:
int Sock_Open(T_Sock *sock,char *ip,unsigned short port,char *localip,unsigned short localport,int timeout);
int Sock_Send(T_Sock *sock,char *buf,int len);
int Sock_Recv(T_Sock *sock,char *buf,int len, struct sockaddr* from,int* fromlen);
int Sock_Close(T_Sock *sock);
这些代码如果不封装,使用起来非常麻烦,所以封装如下:
%extend T_Sock {
...
VALUE Recv()
{
char buf[2048]={0};
int len=sizeof(buf);
int ret=0;
struct sockaddr from={0};
struct sockaddr_in *pAddr=NULL;
int fromlen=sizeof(from);
VALUE data;
VALUE host;
VALUE port;
VALUE arr;
VALUE subarr;
T_Sock *tSock=(T_Sock *)$self;
ret=Sock_Recv(tSock,buf,len,&from,&fromlen);
if(ret>0)
{
data=rb_str_new(buf,ret);
pAddr=(struct sockaddr_in *)&from;
host=rb_str_new2(inet_ntoa(pAddr->sin_addr));
port=INT2NUM(ntohs(pAddr->sin_port));
subarr=rb_ary_new2(2);
rb_ary_push(subarr,host);
rb_ary_push(subarr,port);
arr=rb_ary_new2(3);
rb_ary_push(arr,data);
rb_ary_push(arr,INT2NUM(SOCK_OK));
rb_ary_push(arr,subarr);
return arr;
}
else if(ret==0)
{
arr=rb_ary_new2(2);
rb_ary_push(arr,Qnil);
rb_ary_push(arr,INT2NUM(SOCK_EXIT));
return arr;
}
else if(ret<0)
{
int error=GetLastError();
if(tSock->timeout>0 && error==WSAETIMEDOUT)
{
arr=rb_ary_new2(2);
rb_ary_push(arr,Qnil);
rb_ary_push(arr,INT2NUM(SOCK_TIMEOUT));
return arr;
}
else
{
arr=rb_ary_new2(3);
rb_ary_push(arr,Qnil);
rb_ary_push(arr,INT2NUM(SOCK_ERROR));
rb_ary_push(arr,INT2NUM(error));
return arr;
}
}
return Qnil;
}
...
}
其中其他的函数已经省略,recv之后,会根据情况返回不同的值,正常读取后,会返回接收到的数据,状态码,以及对端地址,如果对端优雅关闭,则返回Qnil和对端socket退出,如果超时返回Qnil,和超时状态,否则返回Qnil和错误号。其中超时采用了setsockopt来设置接收超时,在socket创建的时候就设置好了,无法中途修改。
2) 写脚本文件
写好接口文件后,写一个extconf.rb,然后用ruby处理后生成一个makefile文件,用nmake调用后就会生成.so的dll文件。只要用require 'ter' 就可以使用封装的功能了。下面按照代码实例说明:
代码中封装了一个TerSock来处理socket以及与终端测试接口的连接。
TerSock初始化的时候打开socket
@sock=Ter::T_Sock.new
@sock.Open(serverip,port,"0",localport,timeout)
用户想要跟终端连接的时候调用:
msg=["user","pass",@localport].pack("a8a8S")
send(TEST_CONNECT,msg)
send(TEST_BEGIN)
其中send是
conn=[2,0,code,msg.length].pack("VVVS")
if msg!=""
conn<<msg
end
ret=@sock.Send(conn);
return ret
前面的pack用来完成T_Message头部的封装,msg部分则是用户提供的,合起来后调用udp socket的send发送到终端
Recv则是:
while true
data=@sock.Recv()
if data[1]==Ter::SOCK_OK
_,_,respcode,len=data[0].unpack("VVVS");
data[0]=data[0][14..14+len-1]
return data if respcode==code
elsif data[1]==Ter::SOCK_TIMEOUT
puts "socket timeout";
break
elsif data[1]==Ter::SOCK_EXIT
puts "socket exit"
break
elsif data[1]==Ter::SOCK_ERROR
puts "error #{data[2]}"
break
end
end
接收之后判断状态,如果是有效数据并且是我们期望的消息号,把数据头剥离后剩下的数据发送给用户。
对于所有的数据结构,一开始都创建了一个对象,
$struct=%w{ T_GeneralConfig T_CallTerMsg T_MccConfState}
$h={}
$struct.each {|name| eval(" $h[/"#{name}/"]=Ter::#{name}.new") }
def get_struct(name)
return $h[name]
end
这样用户就可以用get_struct得到相应的对象了。
下面以获取和设置一般设置的终端名为例:
name="xxx"
code=0x10090 #general query
msg=[0x10010].pack("L")
@ter.send(code,msg)
data=@ter.recv(0x11010)
这段代码构造了通用查询,查询一般配置
if data[1]==Ter::SOCK_OK
ret=check("T_GeneralConfig",data[0])
elsif data[1]==Ter::SOCK_TIMEOUT
puts "socket timeout";
elsif data[1]==Ter::SOCK_EXIT
puts "socket exit"
elsif data[1]==Ter::SOCK_ERROR
puts "error #{data[2]}"
end
这里判断返回的代码,其中check函数是关键,
def check(msg,data)
$h[msg].shape(data)
if block_given?
return false if not yield $h[msg]
end
return true
end
如果没有给定块,则只是把收到的数据导入到全局的hash表中的数据结构中,否则会把判断权交给后面的块,并且把执行结果返回,用这个执行结果,我们就可以判断数据结构的值是否符合我们在块中设置的条件了。
t=get_struct("T_GeneralConfig")
t.acTerminalName=name
str=t.to_binstr()
@ter.send(0x10010,str)
这里是设置终端名并发送到终端
code=0x10090 #general query
msg=[0x10010].pack("L")
@ter.send(code,msg)
sleep(1)
data=@ter.recv(0x11010)
这里再取到终端一般设置数据结构。
if data[1]==Ter::SOCK_OK
ret=check("T_GeneralConfig",data[0]) { |t| t.acTerminalName==name }
assert(ret)
elsif data[1]==Ter::SOCK_TIMEOUT
puts "socket timeout";
elsif data[1]==Ter::SOCK_EXIT
puts "socket exit"
elsif data[1]==Ter::SOCK_ERROR
puts "error #{data[2]}"
end
在 check 函数后面附加了块对一般设置的名字字段做了判断,如果名字和我们设置的名字不一样,就会被 testunit 的 assert 记录。在执行多次测试后,看一下 testunit 的结果,就知道测试的情况了。