在Linux下编程,往往不自觉地希望找到各种好用的开源工具来协助自己写出优美健壮的代码,UT工具当然也是必不可少的。cutter就是我喜欢的工具 之一,它没有太多花哨的功能,但却工作良好,简单易学。非常值得推荐的是,cutter的设计很适宜于TDD的开发过程。本文参考了cutter的参考手 册,去掉了一些冗余的部分,试图用一个更为简单的例子来介绍cutter的基本用法。
一、准备自动化编译环境创建工具
在学习cutter之前,有必要了解一些linux下面进行C语言开发的基础知识。这其中和cutter最为相关的就是automake工具。虽然通常我们称之为automake,但其实是一套工具,包括:
autoscan --- 在源码根目录下执行它,可以收集信息,创建一个“初步的”
configure.in文件。
aclocal --- 宏处理工具,它从configure.ac中收集宏,创建aclocal.m4
文件。这里的m4是宏处理器的意思,在很多编译器的前端都会
用到m4技术来识别和展开宏。
libtool --- 静态和动态库的生成都是架构相关的,因此libtool提供了标准
的方法来为我们完成这项操作。
autoheader --- 创建一个config.in.h文件,它会被configure脚本使用。
automake --- 以Makefile.am作为输入,生成Makefile.in文件。
autoconf --- 它会从根据以上工具生成的aclocal.m4, configure.ac,
Makefile.in创建configure脚本文件。
以上介绍的顺序也就是工具被使用的顺序,因此我们可以写一个简单的脚本来简化创建编译环境的工作。
- # autobuilder.sh
- #!/bin/sh
-
- run ()
- {
- $@
- if test $? -ne 0;then
- echo "Failed: $@"
- exit 1
- fi
- }
-
- run aclocal
- run libtoolize --copy --force
- run autoheader
- run automake --add-missing --foreign --copy
- run autoconf
别忘了赋予它可执行的权限。
二、用TDD的方法编码
TDD开发和传统开发不同之处在于前者首先将需求分解成测试用例。也就是说,TDD开发的每一条代码都是为了保证测试用例可以通过。这是极限编程(XP) 的重要实践活动之一。接下来,我用一个简单的例子介绍TDD的大致过程。在这个例子中也将展示cutter的应用方法。
PROBLEM:需要一个程序,它能从控制台得到一个浮点数,然后把这个数向上舍入到最近的0.05并输出。
1. 设计代码结构
请注意我在这里用结构而不是架构一词,因为此二者有着根本的区别,另文详述。我在进行软件开发之前都喜欢首先设计代码结构,因为一个良好的结构可以让我在后续的编码过程中始终保持头脑清醒。在进行大型软件开发活动中,这一点尤为重要。
在linux或者cygwin下面设计代码结构非常简单。针对这个编程问题,我们可以这样来创建:
- mkdir -p project/round/include
- mkdir -p project/round/src/lib
- mkdir -p project/round/src/exe
- mkdir -p project/round/test
-
- touch project/round/include/rndup.h
- touch project/round/src/lib/rndup.c
- touch project/round/src/exe/main.c
- touch project/round/test/test.c
现在,工程的目录结构如下:
project
`-- round
|-- autobuild.sh
|-- include
| `-- rndup.h
`-- src
|-- exec
| `-- main.c
|-- lib
| `-- rndup.c
`-- test
`-- test.c
然后,我们把之前创建的autobuilder.sh拷贝到round目录下,一个开发环境便建立起来了。
2. 建立自动化编译环境
当我们用GDI进行开发的时候,开发环境是工具自动帮我们建立的。但是更多的linux黑客们喜欢自己建立编译环境。在TDD开发模型下,这是编码活动开始之前必须先做的一步。
首先,我们执行autobuilder.sh,看看有什么惊喜的发现。
- ./autobuilder.sh
- aclocal: `configure.ac' or `configure.in' is required
- Failed: aclocal
结果似乎“有惊无喜”,aclocal的执行失败了。其原因是缺少configure.ac或者configure.in文件。这两个文件其实是同一 个,.in后缀是早期的aclocal支持的,后来为了和Makefile.am统一就改成了.ac后缀。具有这样后缀的文件通常需要程序员手工创建。不 过,autoscan工具可以帮我们做绝大部分事情。
执行autoscan之后,当前目录下会生成一个名为configure.scan的文件。
- # -*- Autoconf -*-
- # Process this file with autoconf to produce a configure script.
-
- AC_PREREQ([2.67])
- AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
- AC_CONFIG_SRCDIR([src/test/test.c])
- AC_CONFIG_HEADERS([config.h])
-
- # Checks for programs.
- AC_PROG_CC
-
- # Checks for libraries.
-
- # Checks for header files.
-
- # Checks for typedefs, structures, and compiler characteristics.
-
- # Checks for library functions.
-
- AC_OUTPUT
可以看出,这个文件事实上是一个configure.ac的模板,于是把它改名后再运行脚本。
- ./autobuilder.sh
- libtoolize: putting auxiliary files in `.'.
- libtoolize: copying file `./ltmain.sh'
- libtoolize: You should add the contents of the following files to `aclocal.m4':
- libtoolize: `/usr/share/aclocal/libtool.m4'
- libtoolize: `/usr/share/aclocal/ltoptions.m4'
- libtoolize: `/usr/share/aclocal/ltversion.m4'
- libtoolize: `/usr/share/aclocal/ltsugar.m4'
- libtoolize: `/usr/share/aclocal/lt~obsolete.m4'
- libtoolize: Remember to add `LT_INIT' to configure.ac.
- libtoolize: Consider adding `AC_CONFIG_MACRO_DIR([m4])' to configure.ac and
- libtoolize: rerunning libtoolize, to keep the correct libtool macros in-tree.
- libtoolize: Consider adding `-I m4' to ACLOCAL_AMFLAGS in Makefile.am.
- configure.ac: no proper invocation of AM_INIT_AUTOMAKE was found.
- configure.ac: You should verify that configure.ac invokes AM_INIT_AUTOMAKE,
- configure.ac: that aclocal.m4 is present in the top-level directory,
- configure.ac: and that aclocal.m4 was recently regenerated (using aclocal).
- automake: no `Makefile.am' found for any configure output
- Failed: automake --add-missing --foreign --copy
这回,错误发生在automake,因为我们还没有创建Makefile.am文件。尝试简单地touch一个空文件。
- touch Makefile.am
- ./autobuilder.sh
- libtoolize: putting auxiliary files in `.'.
- libtoolize: copying file `./ltmain.sh'
- libtoolize: You should add the contents of the following files to `aclocal.m4':
- libtoolize: `/usr/share/aclocal/libtool.m4'
- libtoolize: `/usr/share/aclocal/ltoptions.m4'
- libtoolize: `/usr/share/aclocal/ltversion.m4'
- libtoolize: `/usr/share/aclocal/ltsugar.m4'
- libtoolize: `/usr/share/aclocal/lt~obsolete.m4'
- libtoolize: Remember to add `LT_INIT' to configure.ac.
- libtoolize: Consider adding `AC_CONFIG_MACRO_DIR([m4])' to configure.ac and
- libtoolize: rerunning libtoolize, to keep the correct libtool macros in-tree.
- libtoolize: Consider adding `-I m4' to ACLOCAL_AMFLAGS in Makefile.am.
- configure.ac: no proper invocation of AM_INIT_AUTOMAKE was found.
- configure.ac: You should verify that configure.ac invokes AM_INIT_AUTOMAKE,
- configure.ac: that aclocal.m4 is present in the top-level directory,
- configure.ac: and that aclocal.m4 was recently regenerated (using aclocal).
- automake: no `Makefile.am' found for any configure output
- automake: Did you forget AC_CONFIG_FILES([Makefile]) in configure.ac?
- Failed: automake --add-missing --foreign --copy
automake依然出错,但这回多了一条提示:“Did you forget AC_CONFIG_FILES([Makefile]) in configure.ac?”回忆一下,我们把configure.scan改名之后还没有修改过它呢。于是根据提示和configure.ac的规则大 刀阔斧地做了一番改造,结果如下:
- # -*- Autoconf -*-
- # Process this file with autoconf to produce a configure script.
-
- AC_PREREQ([2.67])
- AC_INIT([rndup], [0.0.1], [ndujun@gmail.com])
- AC_CONFIG_SRCDIR([.])
- AC_CONFIG_HEADERS([config.h])
- AC_CONFIG_MACRO_DIR([m4])
- AM_INIT_AUTOMAKE([$PACKAGE_NAME], [$PACKAGE_VERSION])
- # Checks for programs.
- AC_PROG_CC
-
- # Checks for libraries.
-
- # Checks for header files.
-
- # Checks for typedefs, structures, and compiler characteristics.
-
- # Checks for library functions.
-
- AC_CONFIG_FILES([Makefile])
-
- AC_OUTPUT
试一试编译环境吧:
- ./autobuilder.sh
- libtoolize: putting auxiliary files in `.'.
- libtoolize: copying file `./ltmain.sh'
- libtoolize: putting macros in AC_CONFIG_MACRO_DIR, `m4'.
- libtoolize: copying file `m4/libtool.m4'
- libtoolize: copying file `m4/ltoptions.m4'
- libtoolize: copying file `m4/ltsugar.m4'
- libtoolize: copying file `m4/ltversion.m4'
- libtoolize: copying file `m4/lt~obsolete.m4'
- libtoolize: Remember to add `LT_INIT' to configure.ac.
- libtoolize: Consider adding `-I m4' to ACLOCAL_AMFLAGS in Makefile.am.
- configure.ac:9: installing `./install-sh'
- configure.ac:9: installing `./missing'
棒极了!这次顺利通过~~~
你要是足够仔细的话,就会发现开发路径下多了一些奇怪的文件,不过在这篇文章里你完全不用去理它,因为那不是我们要学习的重点。但有一个文件对我们很关 键,就是configure,它其实是一个shell脚本,执行完之后将根据configure.ac的定义创建Makefile文件。在刚才搭建的环境 下执行它:
- ./configure
- checking for a BSD-compatible install... /usr/bin/install -c
- checking whether build environment is sane... yes
- checking for a thread-safe mkdir -p... /bin/mkdir -p
- checking for gawk... no
- checking for mawk... mawk
- checking whether make sets $(MAKE)... yes
- checking for gcc... gcc
- checking whether the C compiler works... yes
- checking for C compiler default output file name... a.out
- checking for suffix of executables...
- checking whether we are cross compiling... no
- checking for suffix of object files... o
- checking whether we are using the GNU C compiler... yes
- checking whether gcc accepts -g... yes
- checking for gcc option to accept ISO C89... none needed
- checking for style of include used by make... GNU
- checking dependency style of gcc... none
- configure: creating ./config.status
- config.status: creating Makefile
- config.status: creating config.h
- config.status: executing depfiles commands
接着我们执行make:
- make
- make all-am
- make[1]: 正在进入目录 `/home/dujun/src/project/round'
- make[1]:正在离开目录 `/home/dujun/src/project/round'
显然,make没有什么事情可做。
3. 设计测试用例
有了一个可靠的编译环境,就可以开始编码了。同样,按照TDD的思路,首先把需求分解成测试用例。在这个问题中,我们必须理解什么叫做“向上舍入到最近的 0.05”。这句话的英文描述是:“Round up to the nearest 0.05”。浮点数的舍入有一些规则,我们最常用的是四舍五入--小于4就舍弃,大于等于5就进位。那么还有一种方法是先把浮点数方法一定的倍数,向上取 整之后再缩小同样的倍数。因为0.05等于1/20,所以意味着我们的程序应该把浮点数先扩大20倍,向上取整后再缩小1/20,并且保留小数点后2位。
理解了需求的含义,测试用例就非常好写了。我们可以手工构造一条:
输入:0.001
输出:0.05
4. 编写测试代码
打开project/round/src/test/test.c,添加一些代码:
- #include "rndup.h"
- #include <math.h>
- #include <cutter.h>
-
-
- void test_round_up(void)
- {
- cut_assert(round_up(0.001) == 0.05);
- }
这里有必要介绍一下cutter的设计,它分为两个部分:cutter tools和cutter API。写一个测试用例的方法是:
a. 写一个以test_作为前缀的函数;
b. 调用cut_作为前缀的一系列assert接口对测试用例下断言;
c. 把测试代码编译链接成共享库;
d. 在shell下面调用cutter <共享库所在路径>
OK,规则是比较简单的。接下来我们完成round目录下面空的Makefile.am,使它能够生成可构建共享库的Makefile。
5. 编译测试用例
为了能够顺利编译,我们需要修改configure.ac和Makefile.am。
首先,在configure.ac中为test.c所在路径添加Makefile配置。
- AC_CONFIG_FILES([Makefile]
- [src/test/Makefile])
然后,为了make能够进入该路径,还要在Makefile.am中加上下面这句:
接着,在上述源码目录添加Makefile.am
touch src/test/Makefile.am
现在,需要重新运行autobuilder.sh以便创建新的configure脚本。
- ./autobuilder.sh
- libtoolize: putting auxiliary files in `.'.
- libtoolize: copying file `./ltmain.sh'
- libtoolize: putting macros in AC_CONFIG_MACRO_DIR, `m4'.
- libtoolize: copying file `m4/libtool.m4'
- libtoolize: copying file `m4/ltoptions.m4'
- libtoolize: copying file `m4/ltsugar.m4'
- libtoolize: copying file `m4/ltversion.m4'
- libtoolize: copying file `m4/lt~obsolete.m4'
- libtoolize: Remember to add `LT_INIT' to configure.ac.
- libtoolize: Consider adding `-I m4' to ACLOCAL_AMFLAGS in Makefile.am.
一切顺利,可以执行configure了,它将在源码路径下创建Makefile文件。有了这个文件便能执行make程序。
- make
- make all-recursive
- make[1]: 正在进入目录 `/home/xxxxx/src/project/round'
- Making all in src/test/
- make[2]: 正在进入目录 `/home/xxxxx/src/project/round/src/test'
- make[2]: *** 没有规则可以创建目标“all”。 停止。
- make[2]:正在离开目录 `/home/xxxxx/src/project/round/src/test'
- make[1]: *** [all-recursive] 错误 1
- make[1]:正在离开目录 `/home/xxxxx/src/project/round'
- make: *** [all] 错误 2
可见,make程序已经递归进入了所有子目录,但因为刚才创建的Makefile.am都是空的,所以它找不到编译规则。我们先来为test.c添加规则。
- noinst_LTLIBRARIES = libtest.la
- libtest_la_SOURCES = test.c
第一行的意思是创建一个名为libtest.la的共享库,第二行声明用于创建共享库的源文件叫做test.c。再次运行autobuilder.sh,我们会看到如下错误:
- src/test/Makefile.am:1: Libtool library used but `LIBTOOL' is undefined
- src/test/Makefile.am:1: The usual way to define `LIBTOOL' is to add `AC_PROG_LIBTOOL'
- src/test/Makefile.am:1: to `configure.ac' and run `aclocal' and `autoconf' again.
- src/test/Makefile.am:1: If `AC_PROG_LIBTOOL' is in `configure.ac', make sure
- src/test/Makefile.am:1: its definition is in aclocal's search path.
- src/test/Makefile.am: installing `./depcomp'
- Failed: automake --add-missing --foreign --copy
这是因为从Makefile.am生成Makefile需要的LIBTOOL宏不存在,我们根据提示在configure.ac里加上一句
这样一来,LIBTOOL宏就可以被automake识别了。再次运行autobuilder.sh之后,执行make:
- make
- make all-recursive
- make[1]: 正在进入目录 `/home/dujun/src/project/round'
- Making all in src/test/
- make[2]: 正在进入目录 `/home/dujun/src/project/round/src/test'
- /bin/bash ../../libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I. -I../.. -g -O2 -MT test.lo -MD -MP -MF .deps/test.Tpo -c -o test.lo test.c
- libtool: compile: gcc -DHAVE_CONFIG_H -I. -I../.. -g -O2 -MT test.lo -MD -MP -MF .deps/test.Tpo -c test.c -fPIC -DPIC -o .libs/test.o
- test.c:1: fatal error: rndup.h: 没有那个文件或目录
- compilation terminated.
- make[2]: *** [test.lo] 错误 1
- make[2]:正在离开目录 `/home/dujun/src/project/round/src/test'
- make[1]: *** [all-recursive] 错误 1
- make[1]:正在离开目录 `/home/dujun/src/project/round'
- make: *** [all] 错误 2
这是一个普通的编译错误,rndup.h文件根本没有创建呢。
a. 在include目录下touch一个rndup.h
b. 在Makefile.am中添加rndup.h的搜索路径
c. 因为test.c引用了cutter.h,所以也要添加它的搜索路径
完整的Makefile.am如下:
- noinst_LTLIBRARIES = libtest.la
- libtest_la_SOURCES = test.c
- INCLUDES = -I$(top_srcdir)/include \
- -I/usr/include/cutter
要注意的是,每次修改了Mmakefile.am都应该执行一次autobuiler.sh,这样才能刷新configure以生成新的Makefile。
现在,make没有不成功之理了。
- make
- make all-recursive
- make[1]: 正在进入目录 `/home/dujun/src/project/round'
- Making all in src/test/
- make[2]: 正在进入目录 `/home/dujun/src/project/round/src/test'
- cd ../.. && /bin/bash /home/dujun/src/project/round/missing --run automake-1.11 --foreign src/test/Makefile
- cd ../.. && /bin/bash ./config.status src/test/Makefile depfiles
- config.status: creating src/test/Makefile
- config.status: executing depfiles commands
- make[2]:正在离开目录 `/home/dujun/src/project/round/src/test'
- make[2]: 正在进入目录 `/home/dujun/src/project/round/src/test'
- /bin/bash ../../libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I. -I../.. -I../../include -I/usr/include/cutter -g -O2 -MT test.lo -MD -MP -MF .deps/test.Tpo -c -o test.lo test.c
- libtool: compile: gcc -DHAVE_CONFIG_H -I. -I../.. -I../../include -I/usr/include/cutter -g -O2 -MT test.lo -MD -MP -MF .deps/test.Tpo -c test.c -fPIC -DPIC -o .libs/test.o
- libtool: compile: gcc -DHAVE_CONFIG_H -I. -I../.. -I../../include -I/usr/include/cutter -g -O2 -MT test.lo -MD -MP -MF .deps/test.Tpo -c test.c -o test.o >/dev/null 2>&1
- mv -f .deps/test.Tpo .deps/test.Plo
- /bin/bash ../../libtool --tag=CC --mode=link gcc -g -O2 -o libtest.la test.lo
- libtool: link: ar cru .libs/libtest.a .libs/test.o
- libtool: link: ranlib .libs/libtest.a
- libtool: link: ( cd ".libs" && rm -f "libtest.la" && ln -s "../libtest.la" "libtest.la" )
- make[2]:正在离开目录 `/home/dujun/src/project/round/src/test'
- make[2]: 正在进入目录 `/home/dujun/src/project/round'
- make[2]:正在离开目录 `/home/dujun/src/project/round'
- make[1]:正在离开目录 `/home/dujun/src/project/round'
此时,test目录下已经生成了libtest.la。我们对这个目录运行cutter命令
- cutter src/test
- cutter src/test
Finished in 0.000148 seconds (total: 0.000000 seconds)
- 0 test(s), 0 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
0% passed
结果提示有0个测试用例,这是怎么回事呢?
其实,cutter是用dlopen装入的,因此,需要在链接器选项中给定足够的信息。于是我们还要对Makefile.am做一些修改。
- noinst_LTLIBRARIES = libtest.la
- libtest_la_SOURCES = test.c
- INCLUDES = -I$(top_srcdir)/include \
- -I/usr/include/cutter
-
- LIBS = $(CUTTER_LIBS)
-
- LDFLAGS = -avoid-version -no-undefined -module -rpath $(libdir)
第6行告诉链接器需要加入-lcutter
第8行的逐个解释如下:
-avoid-version --- 链接器不会对这个共享库创建版本名称,这样便于cutter识别
-no-undefined --- 当引用一个UND类型符号时,会给出提示
-module --- 说明这个共享库将动态使用(dlopend),因此不必添加lib
前缀
-rpath --- 定义运行时的库查找路径
刷新Makefile并重新编译链接之后执行cutter
- cutter src/test/
- cutter: symbol lookup error: src/test/.libs/libtest.so: undefined symbol: round_up