代码调试篇(3):利用 gdb 自动化测试复杂条件逻辑——以 behaviac 行为树为例
Author: stormQ
Created: Monday, 25. February 2019 10:31PM
Last Modified: Monday, 26. October 2020 11:39PM
本文描述了一种利用 gdb 高效率地自动化测试程序中复杂条件分支逻辑的方法。
业务场景描述:业务中有一些复杂条件分支嵌套的业务处理,即执行这些业务处理函数前需要先满足调用该业务处理函数的所有条件。现在需要能够全覆盖测试这些业务处理是否可以正常工作。也就是说,期望结果是满足调用某个业务处理函数的所有条件时,就调用该业务处理函数。为了提高效率和保证质量,要求做到自动化测试。
业务场景抽象:
Sequence 1:Condition1 - > Condition2 - > Condition3 - > Condition4 - > Handler1
Sequence 2:Condition1 - > Condition2 - > Condition3 - > Condition5 - > Handler2
Sequence 3:Condition1 - > Condition6 - > Condition7 - > Handler3
...
注:每个 ConditionX 被封装到一个独立的函数中,这些执行分支每次只会执行其中一个。
step 1: 探究解决方案的实现思路
除了能够实现自动化测试以外,解决方案应该尽量不修改原有的业务代码。这样,可以避免引入新问题的可能性和降低代码的可读性和可维护性。为了达到这样的效果,我们需要在运行期自动地构造满足执行各个业务处理函数所需的条件。比如,为了让业务处理函数Handler1
被“正常地”调用,需要将其所有的前置条件的判断结果都修改为真。利用 gdb,可以做到这一点。
step 2: 利用 gdb 在运行期修改函数的返回值
先写一个手动版的示例。
示例源码,main.cpp:
// main.cpp
#include <iostream>
int g_cond1 = 1;
int g_cond2 = 2;
bool g_cond3 = true;
bool g_cond4 = false;
int conditon1()
{
return g_cond1;
}
int conditon2()
{
return g_cond2;
}
bool conditon3()
{
return g_cond3;
}
bool conditon4()
{
return g_cond4;
}
// precondition: conditon1() == 2 && conditon2() == 3 &&
// conditon3() == false && conditon4() == true
void handler1()
{
std::cout << "handler1() is called." << std::endl;
}
int main()
{
if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
{
handler1();
}
return 0;
}
注:示例代码中展示了某个业务处理函数要被调用需要满足的前置条件。
如何编译:
$ g++ -o main main.cpp -g
如何调试:
$ gdb -q ./main
Reading symbols from ./main...done.
(gdb) start
Temporary breakpoint 1 at 0x40089f: file main.cpp, line 39.
Starting program: /home/xuxiaoqiang/Desktop/test/at/main
Temporary breakpoint 1, main () at main.cpp:39
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
(gdb) b conditon1
Breakpoint 2 at 0x40084a: file main.cpp, line 12.
(gdb) b conditon2
Breakpoint 3 at 0x400856: file main.cpp, line 17.
(gdb) b conditon3
Breakpoint 4 at 0x400862: file main.cpp, line 22.
(gdb) b conditon4
Breakpoint 5 at 0x40086f: file main.cpp, line 27.
(gdb) c
Continuing.
Breakpoint 2, conditon1 () at main.cpp:12
12 return g_cond1;
(gdb) return 2
Make conditon1() return now? (y or n) y
#0 0x00000000004008a4 in main () at main.cpp:39
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
(gdb) c
Continuing.
Breakpoint 3, conditon2 () at main.cpp:17
17 return g_cond2;
(gdb) return 3
Make conditon2() return now? (y or n) y
#0 0x00000000004008ae in main () at main.cpp:39
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
(gdb) c
Continuing.
Breakpoint 4, conditon3 () at main.cpp:22
22 return g_cond3;
(gdb) return false
Make conditon3() return now? (y or n) y
#0 0x00000000004008b8 in main () at main.cpp:39
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
(gdb) c
Continuing.
Breakpoint 5, conditon4 () at main.cpp:27
27 return g_cond4;
(gdb) return true
Make conditon4() return now? (y or n) y
#0 0x00000000004008c4 in main () at main.cpp:39
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
(gdb) c
Continuing.
handler1() is called.
[Inferior 1 (process 27928) exited normally]
(gdb)
上述调试过程,是通过手动方式在运行期修改函数返回值的。这一点,不满足自动化测试的要求。
我们可以把手动执行的命令放到一个文件cmd_test.gdb
中,让 gdb 自动地逐条执行这些命令。
cmd_test.gdb 文件的完整内容:
file ./main
start
b conditon1
b conditon2
b conditon3
b conditon4
c
return 2
c
return 3
c
return false
c
return true
c
如何执行 cmd_test.gdb 文件:
$ gdb -q -x cmd_test.gdb
运行结果为:
$ gdb -q -x cmd_test.gdb
Temporary breakpoint 1 at 0x40089f: file main.cpp, line 39.
Temporary breakpoint 1, main () at main.cpp:39
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
Breakpoint 2 at 0x40084a: file main.cpp, line 12.
Breakpoint 3 at 0x400856: file main.cpp, line 17.
Breakpoint 4 at 0x400862: file main.cpp, line 22.
Breakpoint 5 at 0x40086f: file main.cpp, line 27.
Breakpoint 2, conditon1 () at main.cpp:12
12 return g_cond1;
Breakpoint 3, conditon2 () at main.cpp:17
17 return g_cond2;
Breakpoint 4, conditon3 () at main.cpp:22
22 return g_cond3;
Breakpoint 5, conditon4 () at main.cpp:27
27 return g_cond4;
handler1() is called.
[Inferior 1 (process 15348) exited normally]
(gdb)
从上面可以看出,现在仅需要执行一条命令就可以完成一条测试用例的测试工作了,满足自动化测试的要求。
实际上,测试用例会有很多。我们可以把每条测试用例相关的 gdb 命令封装到一个独立的函数中,最后让 gdb 依次调用这些函数。
修改后的 cmd_test.gdb 文件的内容:
file ./main
start
define test_handler1
b conditon1
b conditon2
b conditon3
b conditon4
c
return 2
c
return 3
c
return false
c
return true
c
end
test_handler1
运行修改后的 cmd_test.gdb 文件:
$ gdb -q -x cmd_test.gdb
Temporary breakpoint 1 at 0x40089f: file main.cpp, line 39.
Temporary breakpoint 1, main () at main.cpp:39
warning: Source file is more recent than executable.
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
Breakpoint 2 at 0x40084a: file main.cpp, line 12.
Breakpoint 3 at 0x400856: file main.cpp, line 17.
Breakpoint 4 at 0x400862: file main.cpp, line 22.
Breakpoint 5 at 0x40086f: file main.cpp, line 27.
Breakpoint 2, conditon1 () at main.cpp:12
12 return g_cond1;
Breakpoint 3, conditon2 () at main.cpp:17
17 return g_cond2;
Breakpoint 4, conditon3 () at main.cpp:22
22 return g_cond3;
Breakpoint 5, conditon4 () at main.cpp:27
27 return g_cond4;
handler1() is called.
[Inferior 1 (process 29848) exited normally]
(gdb)
现在还需要解决一个关键问题:当断点执行到某个函数时,如何判断该函数是否是我们所预期的业务处理函数。如果是,测试结果为成功;否则,测试结果为失败,此时需要排查失败的原因。
step 3: 如何判断预期的业务处理函数是否被调用了
一种实现思路为:判断下一条指令是否位于预期的业务处理函数中。为了便于比较,可以在函数的第一条指令处设置断点,如果继续执行后的下一条指令为预期的业务处理函数的第一条指令,那么说明预期的业务处理函数被调用了,测试结果为成功;否则,没有被调用,测试结果为失败。
这里需要注意一点,用b *函数名
而不是b 函数名
在函数的第一条指令处设置断点。后者设置的不是函数第一条指令处的断点。
修改后的 cmd_test.gdb 文件的内容:
file ./main
start
define test_handler1
b conditon1
b conditon2
b conditon3
b conditon4
b *handler1
c
return 2
c
return 3
c
return false
c
return true
c
# 获取下一条要执行的指令
set $next_actual = $pc
# 设置预期执行的下一条指令
set $next_expected = &handler1
if $next_actual == $next_expected
echo test_handler1[SUCCESS]\n
else
echo test_handler1[FAILED]\n
end
c
end
test_handler1
运行修改后的 cmd_test.gdb 文件:
$ gdb -q -x cmd_test.gdb
Temporary breakpoint 1 at 0x40089f: file main.cpp, line 39.
Temporary breakpoint 1, main () at main.cpp:39
warning: Source file is more recent than executable.
39 if (conditon1() == 2 && conditon2() == 3 && !conditon3() && conditon4())
Breakpoint 2 at 0x40084a: file main.cpp, line 12.
Breakpoint 3 at 0x400856: file main.cpp, line 17.
Breakpoint 4 at 0x400862: file main.cpp, line 22.
Breakpoint 5 at 0x40086f: file main.cpp, line 27.
Breakpoint 6 at 0x400878: file main.cpp, line 33.
Breakpoint 2, conditon1 () at main.cpp:12
12 return g_cond1;
Breakpoint 3, conditon2 () at main.cpp:17
17 return g_cond2;
Breakpoint 4, conditon3 () at main.cpp:22
22 return g_cond3;
Breakpoint 5, conditon4 () at main.cpp:27
27 return g_cond4;
Breakpoint 6, handler1 () at main.cpp:33
33 {
test_handler1[SUCCESS]
handler1() is called.
[Inferior 1 (process 24450) exited normally]
(gdb)
从上面可以看出,输出了test_handler1[SUCCESS]
,表示该测试用例通过了。
注: 如果要获取某个类成员函数的第一条指令,gdb 命令示例为set $expected =(int)'test.cpp'::Test::Handler1
(test.cpp
为类成员函数实现所在的文件名称,Test
为类名,Handler1
为成员函数名称)。
step 4: 其他问题要考虑的
测试用例之间的断点清理
为了避免受到其他测试用例的影响,可以在每个测试用例函数的开始处先删除所有的断点。
测试用例结果统计
开启 gdb 记录日志功能
编写脚本,分析 gdb 的输出日志,统计哪些测试用例通过了,哪些测试用例失败了。