移植Embedded Unit到PowperPC平台

一、Embedded Unit 简介

  Embedded Unit(简称embUnit)是一个针对嵌入式C系统的单元测试框架。它不依赖于标准的C函数库,所有的对象都被静态编译链接。因此,可以比较方便地将其移植到嵌入式平台。

  下载地址:http://sourceforge.net/projects/embunit/files/

【备注】:Embedded Unit测试原理是通过将预期值与实际值进行比较来测试函数的逻辑,只能实现函数级别的单元测试而已,呵呵。

二、目标平台简介

  硬件平台:PowerPC

  操作系统:基于uclinux内核,但是所有系统调用都自己实现的一个精简操作系统;

三、移植思路

  1. 由于embUnit不依赖于标准的C函数库,因此,将我们的编译选项添加到embUnit中的Makefile中,将其代码编译成一个静态库,然后链接到我们原有的程序中;
  2. 额外创建一个源文件,用于编写测试代码,该文件也通过编译、链接,将其与原有的程序链接在一起;
  3. 在原有程序的main中(即程序入口处),调用embUnit这个框架提供的API函数,执行对函数的单元测试;

四、实战步骤

1. 下载的embUnit的源代码解压后,在解压形成的目录里面有一个embunit目录,这个目录就是embUnit的源代码所在的目录,首先我们分析这个目录下的makefile文件,文件内容如下:

View makefile
CC = gcc
CFLAGS = -O
AR = ar
ARFLAGS = ru
RANLIB = ranlib
RM = rm
OUTPUT = ../lib/
TARGET = libembUnit.a
OBJS = AssertImpl.o RepeatedTest.o stdImpl.o TestCaller.o TestCase.o TestResult.o TestRunner.o TestSuite.o

all: $(TARGET)

$(TARGET): $(OBJS)
    $(AR) $(ARFLAGS) $(OUTPUT)$@ $(OBJS)
    $(RANLIB) $(OUTPUT)$@

.c.o:
    $(CC) $(CFLAGS) $(INCLUDES) -c $<

AssertImpl.o: AssertImpl.h stdImpl.h
RepeatedTest.o: RepeatedTest.h Test.h
stdImpl.o: stdImpl.h
TestCaller.o: TestCaller.h TestResult.h TestListener.h TestCase.h Test.h
TestCase.o: TestCase.h TestResult.h TestListener.h Test.h
TestResult.o: TestResult.h TestListener.h Test.h
TestRunner.o: TestRunner.h TestResult.h TestListener.h Test.h stdImpl.h config.h
TestSuite.o: TestSuite.h TestResult.h TestListener.h Test.h

clean:
    -$(RM) $(OBJS) $(TARGET)

.PHONY: clean all

分析可知,这个makefile会将当前目录下的源文件编译和链接成一个名为libembUnit.a的静态库文件。然后,我们只要修改相应的编译选项,去掉不用的选项,下面是修改的Makefile:

View Makefile
include ../build/common.mk

.c.o:
    $(CC) $(CFLAGS) -c -o $*.o $<


embu_OBJS = AssertImpl.o RepeatedTest.o stdImpl.o TestCaller.o TestCase.o TestResult.o TestRunner.o TestSuite.o
embu_DIR = ../object/common/
TARGET = libembUnit.a

all: $(TARGET)

$(TARGET): $(embu_OBJS)
    $(AR) $(ARFLAGS) $(TARGET) $(embu_OBJS)
    cp -f $(TARGET) $(embu_DIR)

.c.o:
    $(CC) $(CFLAGS) $(INCLUDES) -c $<

AssertImpl.o: AssertImpl.h stdImpl.h
RepeatedTest.o: RepeatedTest.h Test.h
stdImpl.o: stdImpl.h
TestCaller.o: TestCaller.h TestResult.h TestListener.h TestCase.h Test.h
TestCase.o: TestCase.h TestResult.h TestListener.h Test.h
TestResult.o: TestResult.h TestListener.h Test.h
TestRunner.o: TestRunner.h TestResult.h TestListener.h Test.h stdImpl.h config.h
TestSuite.o: TestSuite.h TestResult.h TestListener.h Test.h

clean:
    -rm -f *.o *.a 

.PHONY: clean all

仔细分析,修改的内容就是将编译选项修改了,然后将生成的libembUnit.a文件复制到一个公共目录下;

【备注】其中../build/common.mk文件是我们源程序中公共选项脚本文件,里面定义了编译和链接的一些选项而已,呵呵

2. 修改完Makefile后,如果编译,会提示stdio.h这个头文件找不到。原因是embUnit的源代码中的config.h这个文件include了<stdio.h>这个C库文件,而由于我们的操作系统完全自己实现,并且没有提供stdio.h这个头文件,因此,将其注释掉即可;然后再执行make,就可以编译通过了,虽然会有一些警告,不过可以忽略;

3. 书写测试代码

View Code
/* include local files */

/* include embUnit include */
#include "../embUnit/embUnit.h"
#include "../embUnit/AssertImpl.h"
#include "../embUnit/config.h"
#include "../embUnit/HelperMacro.h"
#include "../embUnit/RepeatedTest.h"
#include "../embUnit/stdImpl.h"
#include "../embUnit/Test.h"
#include "../embUnit/TestCaller.h"
#include "../embUnit/TestCase.h"
#include "../embUnit/TestListener.h"
#include "../embUnit/TestResult.h"
#include "../embUnit/TestRunner.h"
#include "../embUnit/TestSuite.h"

#include "../rts_include/test.h"

int iFile = -1;

static void setUp(void)
{
    /* initialize */
    iFile = open("testFile.txt", O_WRONLY|O_CREAT|O_APPEND|O_NAND);
}

static void tearDown(void)
{
    /* terminate */
    close(iFile);
}

static void testFile(void)
{
    char buff[4] = "abcd";
    TEST_ASSERT_EQUAL_INT(6, write(iFile, buff, 4));
}

/*embunit:impl=+ */
/*embunit:impl=- */
TestRef testFile_tests(void)
{
    EMB_UNIT_TESTFIXTURES(fixtures) {
        /*embunit:fixtures=+ */
        /*embunit:fixtures=- */
        new_TestFixture("testFile", testFile),
    };
    EMB_UNIT_TESTCALLER(test,"test",setUp,tearDown,fixtures);
    return (TestRef)&test;
};

首先注意测试代码一方面要引用embUnit的头文件,另一方面也要引用原有程序相应的头文件,这样才能既使用embUnit提供的API函数,又能使用原有程序提供的接口函数;

上述代码的解释如下:

  • 上述代码的目标是对write函数进行测试,方法就是先open一个文件,然后写入固定的字节,判断写入的字节数是否正确,最后关闭文件。
  • setUp函数的作用是:提供待测试函数的前端输入。譬如,上述代码要测试write函数,必须要先用open打开一个文件,那么就可以在setUp这个函数中调用open函数去创建要write的文件;
  • tearDown函数的作用是:提供待测试函数的后端处理。譬如,上述代码,写入文件后,要关闭文件,那么就可以在tearDown这个函数调用close函数关闭文件;
  • testFile函数(函数名其实可以自己定义)的作用是:提供测试逻辑。譬如,上述代码,调用embUnit提供的断言宏,进行判断write函数是否执行成功。
  • testFile_tests函数的作用是:将上述几个函数整合到一个测试suite中,以供后续程序调用;

对测试代码编译(仍然采用目标平台的编译选项进行编译),使其生成的.a(如test.a)文件,并将.a文件复制到公共的库文件目录下;

4. 在原有函数的入口出执行测试程序

代码如下:

View Code
#include "../embUnit/embUnit.h"
#include "../embUnit/AssertImpl.h"
#include "../embUnit/config.h"
#include "../embUnit/HelperMacro.h"
#include "../embUnit/RepeatedTest.h"
#include "../embUnit/stdImpl.h"
#include "../embUnit/Test.h"
#include "../embUnit/TestCaller.h"
#include "../embUnit/TestCase.h"
#include "../embUnit/TestListener.h"
#include "../embUnit/TestResult.h"
#include "../embUnit/TestRunner.h"
#include "../embUnit/TestSuite.h"

void main(void)
{
    printf("***********************************************************\n");
    printf("**************      Unit Test Start         ***************\n");
    printf("***********************************************************\n");

    /* 将测试结果按照编译格式输出 */
    TestRunner_start();
    TestRunner_runTest(testFile_tests());
    TestRunner_end();

    printf("***********************************************************\n");
    printf("**************       Unit Test End          ***************\n");
    printf("***********************************************************\n");
}

在上述代码中,可以看到语句TestRunner_runTest(testFile_tests());调用的就是在测试代码中声明的testFile_tests函数;
需要的注意的是,在该程序中也要引用embUnit提供的头文件,否则无法编译通过。

5. 将上述代码也编译成.a文件,并且与刚才编译生成的libembUnit.atest.a文件,以及原有程序的其他.a文件链接一个可执行文件,并将其烧录到目标机器中,然后即可看到运行结果。下图是本人的运行结果:

由于写入的是4个字节,但是测试代码的预期值是6个字节,因此,测试没有通过。

到此,整个移植过程结束。不过,上述测试结果显示,不是那么友好,很幸运的是embUnit提供的源码中还有一个格式化测试结果的工具的源代码,我们就照葫芦画瓢将这个工具也移植过来;

五、移植embUnit的格式化测试结果工具

在刚才下载的压缩包解压后的根目录下有个textui的目录,这个目录提供的就是格式化测试结果的工具的源代码,移植的方法与上面类似,现简介如下:

1. 修改makefile

textui原有的makefile如下所示:

View makefile
CC = gcc
CFLAGS = -O
INCLUDES = ..
LIBS = ../lib
AR = ar
ARFLAGS = ru
RANLIB = ranlib
RM = rm
OUTPUT = ../lib/
TARGET = libtextui.a
OBJS = TextUIRunner.o XMLOutputter.o TextOutputter.o CompilerOutputter.o

all: $(TARGET)

$(TARGET): $(OBJS)
    $(AR) $(ARFLAGS) $(OUTPUT)$@ $(OBJS)
    $(RANLIB) $(OUTPUT)$@

.c.o:
    $(CC) $(CFLAGS) -I$(INCLUDES) -c $<

TextUIRunner.o: TextUIRunner.h XMLOutputter.h TextOutputter.h CompilerOutputter.h Outputter.h
XMLOutputter.o: XMLOutputter.h Outputter.h
TextOutputter.o: TextOutputter.h Outputter.h
CompilerOutputter.o: CompilerOutputter.h Outputter.h

clean:
    -$(RM) $(TARGET) $(OBJS)

.PHONY: clean all

很明显,原有的makefile是将所有的源文件编译生成一个libtextui.a的文件,现将这个makefile修改如下:

View Makefile
include ../build/common.mk

.c.o:
    $(CC) $(CFLAGS) -c -o $*.o $<

textui_OBJS = TextUIRunner.o XMLOutputter.o TextOutputter.o CompilerOutputter.o
textui_DIR = ../object/common/
TARGET = libtextui.a

all: $(TARGET)

$(TARGET): $(textui_OBJS)
    $(AR) $(ARFLAGS) $(TARGET) $(textui_OBJS)
    cp -f $(TARGET) $(textui_DIR)

TextUIRunner.o: TextUIRunner.h XMLOutputter.h TextOutputter.h CompilerOutputter.h Outputter.h
XMLOutputter.o: XMLOutputter.h Outputter.h
TextOutputter.o: TextOutputter.h Outputter.h
CompilerOutputter.o: CompilerOutputter.h Outputter.h

clean:
    -rm -f *.o *.a 


.PHONY: clean all

与前述一样,只是修改的编译选项,并且将生成的libtextui.a文件复制到一个公共的目录下;

2. 编译textui

  修改完Makefile文件后,此时编译会提示:stdout未定义

  原因是:textui中使用了fprintf将信息输出的stdout(即标准输出,一般指屏幕),但是我们的目标机不支持显示器等标准输出,也没有对stdout进行定义。

  解决办法是:在Outputter.h文件中,重新定义printf,即,忽略stdout,使用printf代替fprintf,具体如下所示:

#define fprintf(stdout, formats, args...)    printf(formats, ##args)

  解决上述问题后,编译时,仍然会提示错误,主要是头文件找不到的错误;

  原因是:在textui的源文件中,需要引用embUnit的头文件,但是其采用的是#include <>方式,当然找不到对应的头文件了;

  解决办法:我们将这些头文件的引用方式改为#include " "方式,并且指明对应头文件的相对路径;

3. 修改在程序入口处的调用测试程序的代码,具体如下所示:

View Code
void main(void)
{

    printf("***********************************************************\n");
    printf("**************      Unit Test Start         ***************\n");
    printf("***********************************************************\n");

    /* 将测试结果按照编译格式输出 */
    TestRunner_start();
    TestRunner_runTest(testFile_tests());
    TestRunner_end();

    /* 将测试结果按文本格式输出 */
    TextUIRunner_setOutputter(TextOutputter_outputter());
    TextUIRunner_start();
    TextUIRunner_runTest(testFile_tests());
    TextUIRunner_end();

    /* 将测试结果按照XML格式输出 */
    TextUIRunner_setOutputter(XMLOutputter_outputter());
    TextUIRunner_start();
    TextUIRunner_runTest(testFile_tests());
    TextUIRunner_end();

    printf("***********************************************************\n");
    printf("**************       Unit Test End          ***************\n");
    printf("***********************************************************\n");

}

  从上述代码可知,注册了两种测试结果输出方式:TextUIRunner_setOutputter(TextOutputter_outputter()); TextUIRunner_setOutputter(XMLOutputter_outputter());

4. 全编译所有代码,并链接成可执行程序,烧到目标机器上,即可看到测试结果,下图就是测试结果:

 到此,整个移植过程结束。

六、经验总结

  1. 不同平台间的代码移植原理其实很简单,就是将待移植的代码使用目标平台的编译环境进行编译,然后将其链接到原有的程序中;
  2. 待移植的代码一般会使用#include<>方式引用头文件,因此,移植的时候需要修改待移植程序的源文件的关于头文件引用方式;
  3. 待移植的代码还可能会使用一些目标平台不支持的API函数或这系统调用,因此,移植的时候应该重新封装这些接口;
  4. 尽量将待移植的代码独立成一个单独的模块,以静态库的方式链接到原有程序中,以便在不需要这些东东的时候,只要不链接其静态库即可;

【备注】:文字描述不清楚的地方还望见谅,咨询交流请发邮件至zhaixing@qq.com,^_^

参考资料:

[1].http://embunit.sourceforge.net/

原文地址:https://www.cnblogs.com/cnpirate/p/2704548.html