• 上午在做一个Solaris 10 on x86代码移植测试过程中,发现一个Gcc编译问题,这里记录下来以作备忘。

    我们的代码在一台安装了Solaris 10 for x86平台的机器A上进行64位编译(gcc -m64)时报错,错误信息如下:
    "xx.c:1: sorry, unimplemented: 64-bit mode not compiled in"。

    而奇怪的是在另外一台同为Solaris 10 for x86的机器B(与上面的机器A硬件配置相同)上则顺利编译通过。最初猜测可能是因为系统设置或环境变量设置不同导致的问题,经过对比检查后发现以上设置都一致,最后将问题定位在Gcc编译器版本上了。

    机器A上使用的是Gcc 3.4.6 for Solaris 10 on x86版本;而可以通过编译的那台机器B上使用的是Gcc 3.4.3 (csl-sol210-3_4-branch+sol_rpath) for Solaris 10 x86版本。尝试在机器A上使用Gcc 3.4.3进行编译,错误未再出现,看来的确是Gcc编译器版本问题。

    遂到Sunfreeware网站上一查究竟。在Gcc 3.4.6 for Solaris 10 on x86的软件说明中,有这样一段话:
    “If you need to do 64-bit compiles, you should use the gcc-3.4.3 that comes with Solaris 10 in /usr/sfw/bin.”

    而Gcc 3.4.6 for Solaris 10 on sparc的版本说明中,则明确表示:“When needed and the source code supports it, this C compiler can create 64-bit executables via the -m64 flag as well as the usual 32-bit ones.”

    注:以上提到的Solaris软件均来自于Sunfreeware站点。

  • 前不久某南方省份的客户反馈说我们的产品对某些生僻字(如“赟”)的转码支持的不好,终端收到后无法显示这个字。

    经分析,发现类似“赟”这样的字在GB2312编码标准中并未收录,要想支持这样的生僻字的内码转换需要产品支持目前最新的中文编码标准GB18030。而我们的产品在诞生到现在就一直只支持GB2312,这就是导致这一问题的直接原因。

    产品以前的代码库中内码转换的接口都是自己实现的,仅支持GB2312和UCS-2(即UNICODE16)之间的内码互转,如果要扩展就要更换码表。与其耗费力气找码表还不如挖掘一下开源世界最常用的内码转换工具iconv呢。iconv既提供了命令行转换工具(iconv),也提供一系列函数库接口供开发人员在代码里调用。很多知名的开源软件包(如vim等)都依赖iconv包。而iconv也几乎遍布所有unix和linux平台,iconv提供的转码支持也基本涵盖了世界范围内绝大多数主流字符集,其中支持的中文字符集就包括GBK, CP936, GB18030, BIG5等主流内码标准。

    iconv的函数接口很简单,我迫不及待的想写一个例子测试一下了(不料,就在写下的这个简单的例子里我犯下了一个低级错误^_^)。

    下面例子代码目的是将"赟"从UTF-8编码转换为GB18030编码(环境:GCC 3.4.6 on Solaris 10 for X86)。
    #include <iconv.h>

    int main() {
            char    in[8];
            char    out[255];

            memset(in, 0, sizeof(in));
            memset(out, 0, sizeof(out));

            in[0]   = 0xe8; /* "赟"的UTF-8编码: E8B59F */
            in[1]   = 0xb5;
            in[2]   = 0x9f;

            size_t  inlen = strlen(in);
            size_t  outlen = sizeof(out);

            iconv_t cd;
            cd = iconv_open("gb18030","utf-8"); /* from utf-8->gb18030 */
            if (cd < 0) {
                    printf("iconv_open failed!\n");
                    return -1;
            }

            if (iconv(cd, &in, &inlen, &out, &outlen) < 0) {
                    printf("iconv failed!\n");
                    iconv_close(cd);
                    return -1;
            }

        printf("out = %s\n", out);
        iconv_close(cd);
        return 0;
    }
    以上代码通过iconv_open获取一个转换描述符,这个描述符包含了转换信息(如从UTF-8转换到GB18030),然后调用iconv接口对传入的字符串进行转换,转换后的结果存储在OUT缓冲区中。

    编译执行执行上面代码:
    gcc -g testiconv.c -liconv
    testiconv.c: In function `main':
    testiconv.c:26: warning: passing arg 2 of `libiconv' from incompatible pointer type
    testiconv.c:26: warning: passing arg 4 of `libiconv' from incompatible pointer type

    ./a.out
    段错误 (core dumped)

    为什么会dump core呢?回顾一下编译时的Warning信息,再对比一下iconv接口的原型:
    size_t iconv (iconv_t cd, const char* * inbuf, size_t * inbytesleft,
                  char* * outbuf, size_t * outbytesleft);
    似乎没什么问题,但又仔细分析了一下Core的栈上信息,发现了一个低级失误:
    问题就出在iconv的第二个和第四个参数上,我在栈上分配了数据in和out,并简单的将&in和&out作为参数传给了iconv。iconv要得是char **类型的参数。看起来&in和&out类型也是char **,但实则不然,这也是C语言的一个陷阱。以in为例,in本身就是栈上那个数组的首地址,&in的含义与in相同,同样是数组的首地址,所以&in = in,也就是说实际上传给iconv的是一个char*而不是char**,iconv在内部对一个char*执行*操作,并以为这是一个地址,显然会导致内存错误。

    修改一下代码:
        char    *p_in    = in;
        char    *p_out    = out;
        if (iconv(cd, &p_in, &inlen, &p_out, &outlen) < 0) {
                    printf("iconv failed!\n");
                    iconv_close(cd);
                    return -1;
        }
    p_in变量在栈上分配,其本身的地址是&p_in,其值指向in这个数组的首地址,这样将&p_in传给iconv就万无一失了。
    再编译执行,我们就得到了正确结果:
    out = 赟

    unix上有很多iconv实现,由于版本不同可能支持的字符集范围不同,所以为了保证代码行为一致,你可下载最新iconv包,并生成静态库(./configure --enable-static=yes),并让你的代码链接静态库。

    午饭时从电视中得知:中国航天之父钱学森今天上午在北京离世。钱老可谓是中国科学家的楷模,对钱老的离世感到甚为惋惜。这里也道一句:“钱老,一路走好!”

  • 安装完中文语言包支持后,Ubuntu的默认locale是zh_CN.UTF-8(即简体中文语言环境,字符集内码UTF-8)。这与我们日常开发环境中Unix设定的环境有所区别,我们日常使用的环境一般为zh_CN.GBK或zh。我们的源代码文件的字符编码也都是GBK的编码,直接在Ubuntu下用默认设置的VIM打开后,中文的注释会显示乱码。如果你直接编辑这个文件并提交,那么其他在Unix下开发的同事Checkout这份源码后打开也将显示乱码(你新增的中文内容会是乱码)。

    解决这个问题至少有两种方法:一种是为Ubuntu新增加一个zh_CN.GBK的locale的支持;另外一种就是通过设置VIM,在不变换Ubuntu所支持的locale的情况下支持对GBK内码文件的读写。

    第一种方法简单说一下,总共分四步走:
    第一步:sudo vi /var/lib/locales/supported.d/local,该文件原始状态只有一行记录:zh_CN.UTF-8 UTF-8;为了增加zh_CN.GBK的locale,我们在这个文件尾添加一行:zh_CN.GBK GBK,保存退出。

    第二步:执行:sudo locale-gen,生成zh_CN.GBK对应的locale
    第三步:编辑:/etc/environment,在文件尾添加如下内容:
    LANGUAGE="zh_CN:zh:en_US:en"
    LANG=zh_CN.GBK
    LC_CTYLE=zh_CN.GBK
    LC_ALL="zh_CN.GBK"

    第四步:重启Ubuntu系统。重启后用VIM再打开以前GBK编码的源代码文件,就不再会有乱码了,而且默认情况下编辑文件采用的依然是GBK编码。不会影响他人在其他平台上读写文件。

    第二种方法是本文重点要谈的内容。即在zh_CN.UTF-8的环境下保证正确读写GBK编码的文件。问题主要集中在:如何读出并正确显示已有的特定字符编码的文件和如何按照特定字符编码写新文件。

    这里有两个数据文件:data1和data2,内容都是“祝祖国六十年生日快乐”,但是data1采用UTF-8编码,而data2采用GBK编码,可以用od -x查看文件实际存储数据是不同的。
    od -x data1
    0000000 a5e7 e79d 96a5 9be5 e5bd ad85 8de5 e581
    0000020 b4b9 94e7 e69f a597 bfe5 e4ab 90b9 000a
    0000037

    od -x data2
    0000000 a3d7 e6d7 fab9 f9c1 aeca eac4 fac9 d5c8
    0000020 ecbf d6c0 000a
    0000025

    在终端UTF-8编码,LC_ALL=zh_CN.UTF-8,VIM默认配置的前提下,尝试用VIM分别打开data1和data2,发现data1正常显示,data2显示乱码;为什么呢?这里VIM当打开一个已存在的文件时会有一系列的处理过程:

    用VIM打开一个已存在的文件时,VIM首先要查看fileencodings(或fencs)这个option。fileencodings是一系列字符编码格式的列表,例如:set fileencodings = GBK,UTF-8,gb18030,ucs-bom,cp936。这个option仅在打开一个已存在的文件时起作用。如果你没有在.vimrc中显式set这个option,那fileencodings的默认值是'ucs-bom,UTF-8,default,latin1',其中default的值是用户环境的默认编码格式。

    当你打开一个已存在的文件时,VIM会用fileencodings值列表中的编码格式逐一去探测该文件的编码方式,直到两者匹配一致。探测成功后,VIM会用匹配到的编码格式去设置此文件session的fileencoding选项值。fileencoding选项指示该session的VIM BUFFER里的数据写入文件或从文件读出时文件中的数据的编码格式。同样该session中VIM BUFFER中数据的编码格式则由另外一个选项指示,那就是encoding option。这里有多个"encoding-like"字样的options,极易混淆。但实际上真正对VIM文件操作时数据显示和保存起作用的只有两个选项:fileencoding和encoding。而fileencodings只是在打开已有文件时用来探测并设置fileencoding字段的一个外围option。VIM的编码转换也是围绕fileencoding和encoding这两个options展开的。无论读写文件,当某个VIM session中fileencoding和encoding的值不一致时,VIM就会自动做编码转换。例如:当读取一个文件时,session的fileencoding为UTF-8,而encoding为GBK时,VIM将文件中的数据读出来后会自动做一个UTF-8到GBK的转换,并将转换后的数据存储在VIM针对该session的BUFFER里;同样当创建一个新文件时,如果该session的vim BUFFER中数据的编码格式(encoding指示)和fileencoding指示的文件编码格式不一致时,save file时,VIM会自动将BUFFER中的数据按照fileencoding指示的编码格式进行一次转换后再存入新文件中。

    每个option都有三种状态:显式设置、空(encoding除外)和默认值。其中显式设置是指在.vimrc或在session中利用set指令对选项进行赋值设置;空:比较特殊,表示该选项的值为empty;默认值则是未通过set在.vimrc或在session对选项进行赋值的状态。

    fileencodings为空时,即在.vimrc中set fileencodings="";VIM将无法进行文件编码探测,将直接根据fileenoding和encoding的值来确定文件编码和BUFFER编码以及是否需要自动做编码转换;当fileencodings不为空,但探测文件编码均告失败时,VIM会将该session的fileencoding置为空,之后将根据encoding的值来设置文件编码和VIM BUFFER编码。

    fileencoding的默认值就是空(""),打开已有文件时通过fileencodings来设置其值,新建文件时如果fileencoding为默认值或空,那么encoding将决定一切。其显式设置的值只有在新建文件的session中才会其作用。

    encoding是核心,是VIM session中BUFFER数据的编码,也可以理解为VIM核心的内码;VIM会根据它与fileencoding、termencoding(term的编码格式)的不同由VIM做自动转码。encoding默认值为$LANG。

    下面用一些例子来说明一下VIM的行为模式,测试环境Ubuntu 9.04, LANG=zh_CN.UTF-8, data1和data2如上所述。
    (1) 三个Option均采用默认值,没有在.vimrc下显式设置
    此时在vim session未建立之前,fileencodings的默认值为“ucs-bom,UTF-8,default,latin1”,fileencoding为空,encoding=UTF-8($LANG).打开data1,VIM通过fileencodings做探测,顺利匹配到UTF-8的编码格式,将fileencoding设置为UTF-8,此时encoding也为UTF-8,两者一致,VIM不做编码转换,屏幕正确显示“祝祖国六十年生日快乐”。打开data2,VIM通过fileencodings做探测,未能匹配到GBK的编码,将fileencoding置为空,encoding发挥作用,VIM不做任何编码转换,将GBK编码的数据以UTF-8格式显示,屏幕显示乱码。

    (2) fileencodings显式被设置为"UTF-8,GBK",其他option采用默认值
    此时在vim session未建立之前,fileencodings的值为“UTF-8,GBK”,fileencoding为空,encoding=UTF-8($LANG).打开data1,VIM通过fileencodings做探测,顺利匹配到UTF-8的编码格式,将fileencoding设置为UTF-8,此时encoding也为UTF-8,两者一致,VIM不做编码转换,屏幕正确显示“祝祖国六十年生日快乐”。打开data2,VIM通过fileencodings做探测,顺利匹配到GBK的编码,将fileencoding置为GBK,此时encoding为UTF-8,两者不一致,VIM做自动编码转换,将GBK编码的数据转换为UTF-8格式后放入BUFFER并显示,屏幕正确显示“祝祖国六十年生日快乐”,VIM在状态条提示“已转换”。

    (3) fileencoding显式设置为"GBK",encoding显式设置为“UTF-8”或采用默认值
    新建一个文件data3,输入:“祝祖国六十年生日快乐”,保存,此时fileencoding和encoding值不一致,VIM做自动编码转换,将BUFFER中的UTF-8编码的数据转换为GBK编码后存储到文件中,VIM状态栏提示“已转换”。退出VIM。od -x data3,输出的是GBK编码。

  • 部门服务器资源向来都比较紧张,每当忙碌季节到来,服务器资源消耗都较大,开发人员总是抱怨编辑代码慢、Build慢以及磁盘空间不足等问题,严重时甚至无法工作。部门也一直在尝试改善这个问题,无非加服务器、加磁盘等,但是这些措施似乎都难以满足开发和测试人员日益增长的对服务器资源的索求。

    为了尽量在组内杜绝上述现象的发生,决定搭建多台PC Server给组内开发人员使用,让大家工作的更有效率,更独立自由,不受共享服务器的约束。因负责部门内部服务器的系统工程师出差在外,无奈委托一个热心同事尝试去安装一下Solaris 10 for x86版本。这位热心同事很积极也很快的将Solaris 10安到了那台空闲PC Server上。但是上午我发现系统的网络仍然未配置,决定亲自手工给这个Server配置网络参数。

    对于Solaris系统的配置和管理,我就是一菜鸟级选手,一切都要从头来-到网络上查找资料。找了半天仍是一头雾水。又想到利用Solaris 10提供图形化界面去配置,但是居然没有找到对应的工具或程序的位置。只能向家中另外一位系统工程师同事求助。这位同事也是热心肠,还亲自过来为我配置网络。在他配置的过程中,我也学到了网络配置的一些皮毛。

    首先查看网口是否激活,如果没有,则找到网口设备名称,并激活网口服务:
    在这台Server上,执行ifconfig -a发现,只有lo0这一个本机LOOPBACK虚拟网口,显然该主机物理网口没有被激活。

    寻找这个网口设备名称:
    cd /dev
    ls -l|more
    一般网口设备名称都类似:bge0,hme0等。发现我的这台主机网口为bge0。

    激活该网口设备:
    ifconfig bge0 plumb up
    这回你再执行ifconfig -a,你将会看到bge0网口,但是该网口尚未分配IP地址和掩码。

    如果你要临时设置该网口IP和掩码的话,可直接使用ifconfig命令(ifconfig bge0 HOST_IP netmask 255.255.255.XXX)进行,但是这样的设置在主机重启后将无法保留下来。那我们就说说永久保留设置的方法。

    设置静态IP:
    vi /etc/hosts,在结尾添加一行:HOST_IP   主机名  loghost
    vi /etc/hostname.bge0,该文件可能需要你手工创建,只有一行:主机名

    设置子网掩码:
    vi /etc/netmasks,增加一行格式诸如:"network-number  netmask"。如果主机IP为10.10.12.77,掩码为255.255.255.0,则你可添加"10.10.12.0 255.255.255.0"。

    设置网关/默认路由
    vi /etc/defaultrouter,直接将你的网关的IP写入即可。

    重启系统后,网络算是通了。无论是从本主机访问其他主机,还是从其他主机访问这个主机都没有问题了。但是还有一个问题:打开Firefox无法打开网页?应该是DNS没有配置,配置方法如下:

    vi /etc/nsswitch.conf,在hosts:   files后面加上一个"dns",即该行变成:"hosts:   files dns",保存退出。
    vi /etc/resolv.conf,每一行是一个DNS服务器,格式如:nameserver xxx.xxx.xxx.xx

    配置完,firefox顺利打开了外部网页。

    配置完网络本以为该主机可以投入正式使用了,但无意间却发现'/'分区下空闲空间仅剩下20%多了,70%的空间已经被使用,再细致一看,发现'/'分区分配的空间太小了,不仅如此swap交换分区仅仅分配了500M的空间。经沟通得知,首次安装采用的是默认安装,才有了此结果。由于无法动态扩展'/'和swap分区大小,无奈只能重装,否则日后问题更多。

    Solaris10的图形化安装果真比不了Ubuntu,更无法与Windows相比了,不过我还能应付,这次我选择了自定义安装,并在安装阶段就将网络配置好了。一个小时左右,安装过程结束,进入桌面,需重新按上面步骤配置DNS,其他就无需配置了。

    从其他机器Telnet访问该主机,居然提示:"telnet: Unable to connect to remote host: Connection refused",是我的网络配置错了?ping和traceroute都正常,而且从这台主机Telnet访问其他主机都没有问题,估计是Telnet服务没有启动,通过“netstat -an|grep LISTEN”并未看到在监听23端口,但是如何启动Telnet服务到不是很清楚,在询问了系统工程师后,执行了一下:svcadm enable telnet,Telnet服务瞬间启动了。同理,Ftp服务也是如此。svcadm应该是Solaris 10新增的系统管理工具,低版本的OS可能都不具备这个命令。

    再次从别的机器telnet这台服务器,并用root用户登录,提示:"Not on system console, Connection to xxx.xxx.xxx.xxx closed by foreign host",这又是怎么回事?从系统工程师那得到的答案是:默认不允许root用户远程登录。可打开/etc/default/login这个文件,并将“CONSOLE=/dev/console”这行注释掉就可以了。

    下班前终于将该主机安装配置完毕,可正式投入使用了。但是在下班路上与另一位同事探讨这个安装配置问题时,他提示我还有一处遗漏:那就是/var没有单独分区,而是与'/'分区共享,这样给以后的使用带来了一些隐患,在测试和运行一些大程序时/var很容易被占满,导致程序无法正常运行。在不再重装系统的前提下,只能考虑定期清理/var下的文件了。

  • 在Unix平台工作的人都使用过Shell的重定向功能,多数人接触较多的是简单的重定向,比如:
    cmd > some_file 将cmd命令的标准输出重定向到some_file中
    cmd < some_file 将some_file的内容作为cmd命令执行的标准输入,或者简单的说cmd命令从some_file读取输入

    等等诸如此类的简单重定向还比较好理解的,起码从大于号或者小于号的箭头方向也可以感性的理解出来。但是类似Bash Shell中还有一些带有复杂符号的重定向功能,看起来就不那么直观了。

    强记是不好的学习方式,加上个人理解的记忆才更牢固,使用起来才更为熟练。昨天晚上为了琢磨一个shell重定向命令,翻看相关bash shell重定向的资料,突然脑子里蹦出一个很容易理解的记忆shell文件描述符重定向的方法。

    以“make 2>&1 1>&build.log”为例,看起来挺头疼,符号增多了,加了一个'&'这个符号,有些晕。不能看表面,我们要看原理:打开“Unix环境高级编程(APUE)”中关于文件内核数据结构的说明,回顾一下,再对应上面的重定向命令。文件描述符重定向是什么?按照书中描述重定向就是进程文件描述符表项改变所指向的文件表项的操作。当make启动后,进程内部文件描述符表中元素1-> 文件表项1, 元素2->文件表项2,元素3 -> 文件表项3,三个文件表项又分别对应v节点表中的不同v节点。但是做了重定向后,"2>&1"关闭文件表项2(文件表项引用计数归0),将进程内部文件描述符表中的元素2指向文件表项1,与元素1指向相同,这时文件表项1有两个文件描述符指向它了; "1>&build.log"将文件表项1关闭,将进程内部文件描述符表中的元素1和元素2都指向build.log对应的文件表项3。这样make执行过程中的标准输出和标准错误都会源源不断的写入build.log中。

    好了回顾完原理,再看看“make 2>&1 1>&build.log”这个命令,'&'在C语言里是取地址的操作符,对应上面原理的描述,把&1看作是取1对应的文件表项;2>&1 则理解为将进程文件描述符表中元素2指向到元素1所对应文件表项上去。1>&build.log理解为:将进程文件描述符表中元素1指向到元素3(build.log对应的文件描述符)所对应文件表项上去。这样理解起来就轻松多了,根本不用记'>&'或'<&'是个什么意思!前面简单重定向命令cmd > some_file同样可以改写为cmd 1>&some_file了。

    '<&'似乎略有不同,如果直接用cmd 0<&some_file,则提示:“ambiguous redirect”,这可能与bash如何解释和执行这一命令有关系。可使用cmd < some_file 0<&3来理解。

    Shell平时用的不多,研究的也不多,所以用了这么多年才有这样粗浅的理解(这个理解也不一定通用,bash的重定向符号有太多,含义也有不一致),呵呵。

  • 近两天一有空就会去看看项目代码,思考一下如何利用cmockery对项目里已有的代码进行测试。项目代码中很多被调用的接口都带有输出参数,而且在这些接口中多利用返回值指示执行成败也否,而利用输出参数返回一些关键结果,这些结果值甚至影响着后续的函数执行流程。前期研究cmockery时没有注意到cmockery是否可以设置被mock接口的输出参数的值,不过回顾了一下cmockery实现的原理,觉得cmockery是应该可以支持的。遂重新翻看了一下cmockery的manual,发现在mock_query_database中确有对输出参数的mock调用,代码如下:

    // Mock query database function.
    unsigned int mock_query_database( DatabaseConnection* const connection,
                                     const char * const query_string, void *** const results)
    {
     *results = (void**)mock();
     return (unsigned int)mock();
    }

    void test_get_customer_id_by_name(void **state) {
     DatabaseConnection connection = { "somedatabase.somewhere.com", 12345678, mock_query_database };
     int customer_ids = 543;
     will_return(mock_query_database, &customer_ids);
     will_return(mock_query_database, 1);
     assert_int_equal(get_customer_id_by_name(&connection, "john doe"), 543);
    }


    上面代码在test_get_customer_id_by_name中两次针对被mock的接口mock_query_database调用will_return,实际上是在符号“mock_query_database”对应的value list里插入了两个item,第一个item的值为&customer_ids,第二个为1。当测试执行到mock_query_database的第一个mock时,返回第一个item:&customer_ids,执行到第二个mock时返回第二个item的值1。这样在测试过程中设置输出参数值的目的就达到了。

    在使用cmockery时唯一需要你关注的就是will_return设置的顺序和在被mock接口中调用mock的顺序,切记不要弄反了。

    这里再举一个例子,再直观感受一下:

    /* message_handler.c */

    #include <stdio.h>

    extern int dispatch_message(void *msg);
    extern int get_next_message(void **msg);

    int handle_next_message() {
            void *  temp_msg        = NULL;
            int     rv              = 0;           

            rv = get_next_message(&temp_msg);
            if (!rv) {                     
                    if (!temp_msg) {                               
                            return -1;
                    } else {                                                               
                            return dispatch_message(temp_msg);
                    }                                                                                              

            }                                                              

            return rv;
    }


    /* test_message_handler.c */
    #include <stdarg.h>
    #include <stddef.h>
    #include <setjmp.h>
    #include "cmockery.h"
    #include <stdio.h>

    int dispatch_message(void *msg) {
            return 0;
    }

    int get_next_message(void **msg) {
            (*msg) = (void*)mock();
            return (int)mock();
    }

    extern int handle_next_message();

    void test_handle_next_message_success(void **state) {
            will_return(get_next_message, 0x1234);
            will_return(get_next_message, 0);
            assert_true(handle_next_message() == 0);
    }

    void test_handle_next_message_fail(void **state) {
            will_return(get_next_message, NULL);
            will_return(get_next_message, 0);
            assert_true(handle_next_message() == -1);
    }

    int main() {
            const UnitTest tests[] = {
                    unit_test(test_handle_next_message_success),
                    unit_test(test_handle_next_message_fail)
            };
            return run_tests(tests);
    }

    执行结果:
    test_handle_next_message_success: Starting test
    test_handle_next_message_success: Test completed successfully.
    test_handle_next_message_fail: Starting test
    test_handle_next_message_fail: Test completed successfully.
    All 2 tests passed

  • 这么久以来一直没有找到一款很好的支持mock测试的C语言单元测试工具包,但前不久在一网友的评论中得知:去年Google曾发布了一款c语言的轻量级单元测试framework -- “cmockery”,cmcokery很小巧,对其他开源包没有依赖,对被测试代码侵入性小;它支持mock test,同样也可以支持常规的单元测试。

    之前在博客中曾描述过C语言实现mock的一个思路,不过和cmockery对比起来,当时我的思路显然还处于初级阶段,而cmockery则走到了更高级,使用起来也更为简便。

    还是以我上一篇文章中的代码来举例,利用cmockery来对biz.c进行mock test。

    应用层被测试代码不变:
    /* biz.h */
    #ifndef BIZ_H
    #define BIZ_H

    #include <stdio.h>

    int biz_operation(char *fname);
    #endif

    /* biz.c */
    #include "biz.h"

    int biz_operation(char *fname) {
            FILE *fp = NULL;

            fp = fopen(fname, "r");
            if (fp == NULL) {
                    printf("fail to open fle!\n");
                    return 1;
            } else {
                    printf("succeed to open file!\n");
                    return 0;
            }
    }


    利用cmockery改造测试代码如下:
    /* test.c */
    #include <stdarg.h>
    #include <stddef.h>
    #include <setjmp.h>
    #include <stdio.h>
    #include "cmockery.h"
    #include "biz.h"

    FILE *fopen(const char *filename, const char *mode) {
            return (FILE*)mock();
    }

    void test_biz_operation_return_succ(void **state) {
            will_return(fopen, 0x1234);
            assert_true(biz_operation("foo.txt") == 0);
    }

    void test_biz_operation_return_fail(void **state) {
            will_return(fopen, NULL);
            assert_true(biz_operation("foo.txt") == 1);
    }

    int main() {
            const UnitTest tests[] = {
                    unit_test(test_biz_operation_return_succ),
                    unit_test(test_biz_operation_return_fail),
        };
            return run_tests(tests);
    }

    gcc  biz.c test.c -I ./ -I {YOUR_CMOCKERY_INSTALL_DIR}/include/google -L {YOUR_CMOCKERY_INSTALL_DIR}/lib -lcmockery

    执行a.out结果如下:
    test_biz_operation_return_succ: Starting test
    succeed to open file!
    test_biz_operation_return_succ: Test completed successfully.
    test_biz_operation_return_fail: Starting test
    fail to open fle!
    test_biz_operation_return_fail: Test completed successfully.
    All 2 tests passed

    在测试代码中override了C标准库中fopen的实现,代码很简单,就是调用一个mock,然后根据返回值类型做一个强制类型转换。那么执行 起来后mock究竟会返回什么值呢?这个值你可以任意设定,看到下面test_biz_operation_return_xx中的 will_return宏了吗,在will_return中你可以设定fopen中mock()调用的返回值:will_return(fopen, 0x1234)或will_return(fopen, NULL); 是不是思路清晰了许多了呢,没错。cmockery就是通过在will_return中设置mock的返回值,并在执行fopen这类被mock的函数接 口中通过接口实现中的mock调用将你设定的值返回出来,从而控制被测试接口biz_operation的执行路径和执行结果,以达到mock test的目的。

    cmockery的源代码行数不到3K,你阅读一下will_return和mock的源代码就一目了然了。大致思路应 该是:在堆上分配一块内存,用来存储你在will_return中设定的返回值,用函数名字符串做索引;在mock()中则通过调用mock的函数的名字 去匹配,得到已经设定好的存储在堆内存上的那个值,并在转型后返回。

    以上是cmockery支持的对通用函数返回值的 mock,cmockery还支持mock function的parameters checking、setup和teardown、assert failure和dynamic memory allocation的mock等,cmockery的manual中 有细致说明。不过原理都类似,在上面也都说过了。不过有些机制对被测试代码还是有一定侵入性的。cmockery虽好,但是毕竟是针对C语言的,就无法逃 过链接阶段符号resolved的问题,测试上面简单的代码还好,如果测试大工程中某复杂源代码文件中的接口时,链接时就会有些麻烦,所以工程源代码组织 的扁平化、接口功能单一化、包含和清晰的调用关系都会大大有助于实施mock单元测试,否则一旦你的biz_operation接口功能很复杂,调用了很 多其他接口,实现很冗长的话,那么你的mock测试同样也会很麻烦,这时你需要做的就应该是重构你的代码,将复杂的biz_operation拆分开了。

    另外cmockery的安装很简单,遵循常规的configure->make->make install,这里不再贽述了。

  • 暑去清凉来,一场大雨让燥热一去不复返了,这让身体舒服了许多。本周四晚有一次产品升级操作,按惯例每次升级前的都会对产品做一次针对性的回归测试,这次也不例外,不过临近下班时测试组爆出一个莫名奇妙的问题。

    测试人员在BUG说明中写到:产品在只运行某个流程A的情况是正常的,但是当流程A和流程B一起运行时,就会出XX异常情况。作为开发人员遇到类似的问题第一反映多为:这怎么可能呢?这个产品已经经过N轮测试并且早前已在某个省份上线运行了近两个月,如果有此潜在的BUG应该早就暴露出来了才对。及时找到测试人员沟通,测试人员很轻松的就复现出了该BUG,眼见为实!离升级时间点已经不多了,赶紧解决吧。

    使用GDB在我认定的关键代码路径上设置了断点,对测试环境下的某进程进行调试,不过无论如何发消息,代码始终没有走到该断点,这让我疑惑不已。负责维护这段代码的开发人员恰参加培训回来,用她擅长的通过调试方法-“加打印语句”又进行了一次调试,发现一些端倪,消息并未按照我们预期的流程走,问题被缩小到消息包中的一个关键字段上,通过打印发现这一字段的值与预期的值不同。我的第一反映:是否有内存污染问题,如果真有这样的问题那就严重了,一直到此时我的怀疑点也一直在产品本身上。

    这时测试人员在屏幕上的抓包结果引起了我们的注意:消息包中这个字段的值与设置的不符。通过进一步在产品中的打印结果也印证了这一点。难道是模拟器的问题?记忆中模拟器已经用了一年多了,这个问题之前怎么没有暴露出来呢。我们立即换了一个其他的模拟器进行了测试,结果:流程正常。看来就是模拟器的问题了。

    据测试人员说以前未暴露出该问题很可能是因为之前的测试要么只测试A流程,或要么只测试B流程,很少A和B流程一起并行测试,所以这个模拟器陷阱就没有被发现。模拟器在A和B两个流程的共同作用下出现了内存污染的bug,,将A流程中的协议包中的一个重要字段设置错了,导致产品在处理该流程消息时未得出预期结果。

    这次的“模拟器陷阱”问题起码暴露出两个问题:
    1、缺少对新实现的模拟器正确性的完备测试;
    2、测试人员在用例设计上还有提高的余地,应避免只有单一场景的用例了。

  • 上周测试组反馈在一台HP X86-64主机Solaris 10 for X86环境下部署的应用无法连接Oracle数据库,错误码ORA-12154。而另外一个产品的部署在这台主机上的应用却能正常连接到数据库。本周安排专人对该问题进行查找,在先后排除了用户环境设置、Oracle数据库服务端等问题后,我们最终把目光集中在了Oracle客户端的OCI库上。

    定位过程如下:
    1、SQLPLUS可以访问数据库;
    2、同环境下另一个应用可以访问数据库;
    以上证明用户环境和tnsnames.ora配置没有问题;
    3、通过抓包未发现客户端有到Oracle服务端的链接和数据传输,所以该问题应该与Oracle Server端一毛钱关系都没有;
    4、发现我们产品的应用使用的是32bit库编译的,而另外一个产品的应用使用的是64bit库,但两个产品底层调用都是一样的;
    5、基本锁定是该主机上装的Oracle OCI 32bit库有bug;
    6、我们的资深系统工程师在Oracle官方找到了该问题的根源;
    7、安装新patch后,应用顺利连接到Oracle Server,问题解决。

    Oracle官方对该问题的说明摘录如下:
    Solaris x86-64: Running 32-bit Applications Connecting to Database Using TNS Naming Adapter Fails With Segmentation Fault (SIGSEGV) or ORA-12154
      Doc ID:  388631.1 Type:  PROBLEM
      Modified Date :  23-OCT-2007 Status:  PUBLISHED

    --------------------------------------------------------------------------------
    Applies to:
    Oracle Server - Enterprise Edition - Version: 10.2.0.2

    Symptoms
    Running 32-bit applications connecting to Database using TNS Naming Adapter Fails With Segmentation Violation (SIGSEGV)

    Segmentation Fault(coredump)

    Running 64-bit work as expected.

    Other symptoms would be

    ORA-12154: TNS:could not resolve the connect identifier specified

    Cause
    This has been identified to be caused by
    Bug 5389730 10.2.0.1 32BIT OCI EXECUTABLES FAILS WITH ORA-12154 ON SOLARIS 10 X86-64(AMD64)

    TNS Naming Adapter was not included within the 32-bit Naming Libraries.

    Solution
    This is fixed Oracle11g Client 11.0.

    There exists patches for 10.2.0.2 and 10.2.0.3:

    download and installPatch 5389730 with opatch or

    To implement the solution manually, please execute the following steps:

    Download Patch 5389730
    cp $ORACLE_HOME/network/lib/ins_net_client.mk
    $ORACLE_HOME/network/lib/ins_net_client.mk.prePatch_5389730
    extract ins_net_client.mk into $ORACLE_HOME/network/lib/ins_net_client.mk
    cd $ORACLE_HOME/network/lib
    make -f ins_net_client.mk nnfgt.o
    Which update (check this)
    $ORACLE_HOME/lib/libn10.a and $ORACLE_HOME/lib32/libn10.a
    make -f ins_net_client.mk client_sharedlib

    which update (check this)
    #$ORACLE_HOME/lib32/libclntsh.so
    #$ORACLE_HOME/lib32/libclntsh.so.10.1
    #$ORACLE_HOME/lib/libclntsh.so
    #$ORACLE_HOME/lib/libclntsh.so.10.1

    Check that executable is loading $ORACLE_HOME/lib32/libclntsh.so.10.1 by ldd 'executable'

    All dynamically linked applications that use libclntsh should work now.
    Static linked applications, need to be relinked with the new libraries.

  • 周六,对于上班族来说是多么好的日子,能在家里享受自由的无拘无束的生活而且不用担心第二天的工作,应该说是一周中最没有压力的一天。六点半起床,慢慢喝下一杯225ml左右的凉白开(保健医生说20-25摄氏度的凉白开比较适宜作为起床后的第一杯水),套上运动短裤和上衣,打开MP3播放器,塞上耳机,出门在园区内慢跑。昨晚下了一场雨,所以园区早上的空气很好。耳畔酷玩乐队的“Viva La Vida”让我跑起来很轻松,30分钟的有氧慢跑能让我的大脑和心脏获得足够的氧气,心情也变得更好。最后绕着园区走上一圈结束锻炼。

    回房间后,舒舒服服的冲了个热水澡。简单的吃过早饭后就回到了本本前,本来计划解决一下本周五发现的一个GB2312转Unicode码的问题。但此时远在南方某省的技术支持人员打来电话,说我们的产品又出现问题了。这个问题早有端倪,曾先后引起客户总部的投诉、当地一些客户的投诉以及计费部门的投诉。前些时间在查这个问题时一直很迷惑,同样的机器和配置在其他省就没有问题,为什么唯独在该省问题严重。而且从业务量上来说,该省虽然业务量上比其他省高出一些,但按照目前我们产品的处理能力来看,还是完全可以满足要求的。在没有找到根本问题前,本周一直在做一些程序部署上的优化以及参数调整,希望能通过这些手段来缓解问题的严重程度。

    本周五刚刚完成了一些I/O上优化,周六却又出现了问题,而且这次是客户集团总部的投诉。前方的技术人员已经是火急火燎,但是查问题也不是一蹴而就的事情,还是需要细心、耐心和稳定的心理的,不能头脑发热。

    所有问题的查找都只能从已出现的问题现象着手。今天问题的现象是:我们的产品作为Server端时无法及时收消息并回应答,导致客户端异步发送窗口中的消息超时并重发该消息,而这条重发的消息因与前一条消息有着同样的消息ID而被我们的产品拒绝。还有一个现象就是:我们的产品作为Client端向一内部的鉴权子系统发鉴权请求,因未能及时收到应答而导致我们自己的异步发送窗口中的消息过期而直接进行了下一个环节的处理,这样一来这些消息在用户体验和计费上都会出现问题而导致投诉。

    试着调整一下两端通信的参数以及一些队列的缓存参数,生效后也仅仅缓解了一段时间就再次出现了类似的问题,严重时双方居然因为socket阻塞而导致链接断开。这时技术支持同事提到主机I/O特别高。I/O高倒是很好的解释了socket未能及时被读取的问题,但是本周明明做了些I/O优化,为什么I/O还是这么高,而且此时该省的业务量相当的小,基本排除因业务量过大而导致I/O高的可能了。但是又是什么导致阵列I/O高呢?甚是疑惑!

    究竟是什么问题导致大量磁盘操作呢?无意间在产品运行环境里发现一个Core文件,如果只发现一个core文件倒不足意外,但是发现这个core文件有上G的容量,而且一直在不断被刷新。难道就是这个core的不断刷新导致了I/O特高?遂尝试写了个脚本每个2秒尝试rm一次该core文件。果然经过这一处理,I/O降了下来,上面的问题也不再出现了。停掉脚本,I/O又攀升了上来,上面的问题就又出现了。“罪魁祸首”终于找到了!

    虽然使用脚本可以临时解决问题,但是这样解决问题显然是不负责任的。到底是什么导致Core的出现呢? 停掉脚本,让程序产生core,对core文件进行分析。通过pstack和gdb打开core文件,core文件输出的信息很少,很多信息都成了“???”,似乎被破坏了。不过可以获得出core文件的进程号以及dump core的接口函数名字。通过进程号和程序日志共同定位,发现出core的进程都是在处理同一个客户端提交的消息。让技术支持同事封掉该客户端的IP,果然再没有Core产生,看来是我们的程序在处理这家客户端提交的消息时出了问题。

    到目前为止已经大有收获了。继续!利用snoop工具获得了该客户端提交的消息包的信息。经过对比分析发现,该客户端提交的包信息与协议中定义的格式不符合。但是我们的程序居然没有发现这样的非法格式包,进一步结合代码、包信息和core信息进行分析,终于定位到了问题所在。原来是我们的程序的一个函数实现逻辑有误,而这种错误在处理正常格式包时是不会发生的,但是处理这种非法格式包时,会导致严重的栈上缓冲区溢出,直至进程运行混乱,dump core并退出。

    这时想起周五同事发来的一封邮件,说的是我们的另一个产品在另外一个省也遇到了类似情况,core的输出与今天处理的情况几乎相同。想必是一个问题。因为出问题的函数是很久以前的代码了,而且是复用库中的一处代码。估计所有复用了该库的产品都要做一次升级了。

    解决完问题已是日落时分,虽然身体感觉一丝疲乏,但是心情还是不错的,一天的努力终于有了成果,程序员的成就感就是由此而来的。