如何在没有 changing/instrumenting 模块源代码的情况下对用户定义的“operator new”和“operator delete”进行单元测试?
How to unit-test user-defined `operator new` and `operator delete` without changing/instrumenting the module's source?
由于这个问题无法用几行文字来说明,还请大家多多包涵这个问题的大小。
情况
我们正在开发一个嵌入式系统,它需要通过替换运算符 new
、new[]
、delete
和 delete[]
来自行管理其堆。这些用户定义的替换函数在它们自己的模块中实现。
我们决定对静态方法使用 class,我们也可以使用命名空间来保持全局命名空间的整洁。
// allocator.h
#include <cstddef> // for size_t
class Allocator
{
public:
static void setup();
static void* allocate(size_t size);
};
由于在 运行 时间内分配的实例不会被删除,我们通过关闭系统来禁止应用程序的其余部分调用 delete
。测试框架可以拦截关机,所以这个已经可以测试了
// allocator.cpp
#include "allocator.h"
static char* baseAddress = nullptr;
static size_t spaceLeft = 0;
void Allocator::setup()
{
static char heap[1000]; // super-simple for Whosebug example
baseAddress = heap;
spaceLeft = sizeof heap;
}
void* Allocator::allocate(size_t size)
{
void* p = 0;
if (size <= spaceLeft)
{
p = static_cast<void*>(baseAddress);
baseAddress += size;
spaceLeft -= size;
}
return p;
}
void* operator new(size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void* operator new[](size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void operator delete(void*)
{
// shutdown(); // commented out for Whosebug example
}
void operator delete[](void*)
{
// shutdown(); // commented out for Whosebug example
}
实际上,堆的 RAM 分配不同,但这并不重要。
为了对该模块进行单元测试,我们使用 GoogleTest,但具体的测试框架并不重要。我们可以使用任何其他框架。
以下来源是我为调查该问题而编写的测试框架的模拟。它使用全局运算符 new
和 delete
,当然,这些不应被上面的用户定义运算符替换。否则框架会尝试在尚不存在的堆上分配新对象。
// framework.cpp
#include <cstdlib> // for malloc() and free()
#include <iostream>
#if 0 // For the example to compile and link, currently commented out
void* operator new(size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void* operator new[](size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void operator delete(void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
void operator delete[](void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
#endif
void framework()
{
int* p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
}
为了您的方便,当然还有它的头文件。
// framework.h
void framework();
这是测试驱动程序,针对此示例进行了简化。它调用框架的东西(在幕后的真实情况下),用被测模块测试失败和成功的分配,然后再次调用框架的东西。
// testdriver.cpp
#include <iostream>
#include "framework.h"
#include "allocator.h"
int main()
{
framework();
#if 1 // possibility to comment out for experiments
int* p1 = new int; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
#endif
Allocator::setup();
#if 1 // possibility to comment out for experiments
p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
#endif
framework();
return 0;
}
软件开发使用的标准是C++98,因为我们被这样一个古老的编译器所束缚。
用于测试的标准是 C++11,因为 GoogleTest 需要它作为最低要求。
这些是编译命令 link:
g++ -Wall -Wextra -pedantic -std=c++11 -c allocator.cpp -o allocator.o
g++ -Wall -Wextra -pedantic -std=c++11 -c framework.cpp -o framework.o
g++ -Wall -Wextra -pedantic -std=c++11 -c testdriver.cpp -o testdriver.o
g++ -Wall -Wextra -pedantic -std=c++11 testdriver.o framework.o allocator.o -o testdriver
第一个解决方案,用测试人工制品污染模块的源代码
我的第一个想法是将这些条件编译的行插入到模块的源代码中。
// allocator.cpp
//...
#if !defined(TESTING)
#define operator_new_single operator new
#define operator_new_array operator new[]
#define operator_delete_single operator delete
#define operator_delete_array operator delete[]
#endif
//...
void* operator_new_single(size_t size) // void* operator new(size_t size)
{
// ...
}
void* operator_new_array(size_t size)
{
// ...
}
void operator_delete_single(void*)
{
// ...
}
void operator_delete_array(void*)
{
// ...
}
为了测试编译,我使用了:
g++ -Wall -Wextra -pedantic -std=c++11 -c -DTESTING allocator.cpp -o allocator.o
现在测试驱动程序可以简单地调用这些函数,因为它们不再是运算符。
但是我们的安全人员说 "No way!" 我不得不同意。安全相关软件中的测试仪器很危险,因为它可能会潜入最终产品。你根本不这样做。
第二种解决方案,由于对编译器及其版本的依赖而脆弱
我们在 MinGW64 的化身中使用 GCC,所以我想出了 linker 选项 -wrap
。一句话:此选项使 linker 将 __wrap_
添加到调用站点的符号,并在被调用站点添加 __real_
。
所以我查找了运算符的名称,因为 linker 对 C++ 一无所知;它只是不需要知道。 ;-) 好吧,我们使用的版本中的 G++ 有这个 "translation":
_Znwy := operator new(unsigned long long)
_Znay := operator new[](unsigned long long)
_ZdlPv := operator delete(void*)
_ZdaPv := operator delete[](void*)
现在我可以使用 C 内存分配器使用运算符的替换来扩展测试驱动程序。 (感谢 C++ 人员,将这些东西留在库中!)
// testdriver.cpp
#include <cstring> // for malloc() and free()
// ...
extern "C" void* __wrap__Znwy(size_t size)
{
return malloc(size);
}
extern "C" void* __wrap__Znay(size_t size)
{
return malloc(size);
}
extern "C" void __wrap__ZdlPv(void* block)
{
free(block);
}
extern "C" void __wrap__ZdaPv(void* block)
{
free(block);
}
// ...
这些是被测模块中真正的操作符的声明,供测试驱动程序调用。
// testdriver.cpp
// ...
extern "C" void* __real__Znwy(size_t size);
extern "C" void* __real__Znay(size_t size);
extern "C" void __real__ZdlPv(void* block);
extern "C" void __real__ZdaPv(void* block);
// ...
link 的命令现在是:
g++ -Wall -Wextra -pedantic -std=c++11 -Wl,-wrap,_Znwy,-wrap,_Znay,-wrap,_ZdlPv,-wrap,_ZdaPv testdriver.o framework.o allocator.o -o testdriver
这也奏效了。但它有点复杂和丑陋。而且它只适用于 GCC,另外我不确定不同的版本是否会保留这些经过处理的名称。很可能他们这样做,为了兼容,但谁知道呢。
我的一个问题
感谢阅读所有这些,我的问题来了:
我还能尝试什么?
我正在寻找一种解决方案,它不会改变模块的源代码,并且适用于(大部分)任何编译器。
好吧,伙计们,我没有偷懒和玩弄我的拇指。
我找到了一个既简单(非原始)又优雅的解决方案。它可能对其他人不起作用,但对我们有用。
GCC 的预处理器可以选择在处理实际源代码之前包含另一个文件。我用它来重新定义关键字 operator
并将其扩展为特定于 class 的运算符。
// instrumentation.h
#include <cstddef> // for size_t
class T
{
public:
void* operator new(size_t);
void* operator new[](size_t);
void operator delete(void*);
void operator delete[](void*);
};
#define operator T::operator
用这个编译被测模块:
g++ -Wall -Wextra -pedantic -std=c++11 -c -Wp,-include,instrumentation.h allocator.cpp -o allocator.o
现在以前替换的运算符不再替换全局运算符。它们 "wrapped" 变成了 class,测试驱动程序可以在这个包装器 class 的实例上调用它们。测试驱动程序在包含 instrumentation.h
后立即取消定义 operator
当然很重要,否则您将在测试驱动程序的其余代码中遇到大量错误。
// testdriver.cpp
#include <iostream>
#include "framework.h"
#include "allocator.h"
#include "instrumentation.h"
#undef operator
int main()
{
framework();
T* p1 = new T; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
T* p2 = new T[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
Allocator::setup();
p1 = new T;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new T[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
framework();
return 0;
}
如果编译器的预处理器没有这样的选项,可以使用一个小的包装器来代替。
// allocator_wrapper.cpp
#include "instrumentation.h"
#include "allocator.cpp"
由于这个问题无法用几行文字来说明,还请大家多多包涵这个问题的大小。
情况
我们正在开发一个嵌入式系统,它需要通过替换运算符 new
、new[]
、delete
和 delete[]
来自行管理其堆。这些用户定义的替换函数在它们自己的模块中实现。
我们决定对静态方法使用 class,我们也可以使用命名空间来保持全局命名空间的整洁。
// allocator.h
#include <cstddef> // for size_t
class Allocator
{
public:
static void setup();
static void* allocate(size_t size);
};
由于在 运行 时间内分配的实例不会被删除,我们通过关闭系统来禁止应用程序的其余部分调用 delete
。测试框架可以拦截关机,所以这个已经可以测试了
// allocator.cpp
#include "allocator.h"
static char* baseAddress = nullptr;
static size_t spaceLeft = 0;
void Allocator::setup()
{
static char heap[1000]; // super-simple for Whosebug example
baseAddress = heap;
spaceLeft = sizeof heap;
}
void* Allocator::allocate(size_t size)
{
void* p = 0;
if (size <= spaceLeft)
{
p = static_cast<void*>(baseAddress);
baseAddress += size;
spaceLeft -= size;
}
return p;
}
void* operator new(size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void* operator new[](size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void operator delete(void*)
{
// shutdown(); // commented out for Whosebug example
}
void operator delete[](void*)
{
// shutdown(); // commented out for Whosebug example
}
实际上,堆的 RAM 分配不同,但这并不重要。
为了对该模块进行单元测试,我们使用 GoogleTest,但具体的测试框架并不重要。我们可以使用任何其他框架。
以下来源是我为调查该问题而编写的测试框架的模拟。它使用全局运算符 new
和 delete
,当然,这些不应被上面的用户定义运算符替换。否则框架会尝试在尚不存在的堆上分配新对象。
// framework.cpp
#include <cstdlib> // for malloc() and free()
#include <iostream>
#if 0 // For the example to compile and link, currently commented out
void* operator new(size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void* operator new[](size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void operator delete(void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
void operator delete[](void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
#endif
void framework()
{
int* p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
}
为了您的方便,当然还有它的头文件。
// framework.h
void framework();
这是测试驱动程序,针对此示例进行了简化。它调用框架的东西(在幕后的真实情况下),用被测模块测试失败和成功的分配,然后再次调用框架的东西。
// testdriver.cpp
#include <iostream>
#include "framework.h"
#include "allocator.h"
int main()
{
framework();
#if 1 // possibility to comment out for experiments
int* p1 = new int; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
#endif
Allocator::setup();
#if 1 // possibility to comment out for experiments
p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
#endif
framework();
return 0;
}
软件开发使用的标准是C++98,因为我们被这样一个古老的编译器所束缚。
用于测试的标准是 C++11,因为 GoogleTest 需要它作为最低要求。
这些是编译命令 link:
g++ -Wall -Wextra -pedantic -std=c++11 -c allocator.cpp -o allocator.o
g++ -Wall -Wextra -pedantic -std=c++11 -c framework.cpp -o framework.o
g++ -Wall -Wextra -pedantic -std=c++11 -c testdriver.cpp -o testdriver.o
g++ -Wall -Wextra -pedantic -std=c++11 testdriver.o framework.o allocator.o -o testdriver
第一个解决方案,用测试人工制品污染模块的源代码
我的第一个想法是将这些条件编译的行插入到模块的源代码中。
// allocator.cpp
//...
#if !defined(TESTING)
#define operator_new_single operator new
#define operator_new_array operator new[]
#define operator_delete_single operator delete
#define operator_delete_array operator delete[]
#endif
//...
void* operator_new_single(size_t size) // void* operator new(size_t size)
{
// ...
}
void* operator_new_array(size_t size)
{
// ...
}
void operator_delete_single(void*)
{
// ...
}
void operator_delete_array(void*)
{
// ...
}
为了测试编译,我使用了:
g++ -Wall -Wextra -pedantic -std=c++11 -c -DTESTING allocator.cpp -o allocator.o
现在测试驱动程序可以简单地调用这些函数,因为它们不再是运算符。
但是我们的安全人员说 "No way!" 我不得不同意。安全相关软件中的测试仪器很危险,因为它可能会潜入最终产品。你根本不这样做。
第二种解决方案,由于对编译器及其版本的依赖而脆弱
我们在 MinGW64 的化身中使用 GCC,所以我想出了 linker 选项 -wrap
。一句话:此选项使 linker 将 __wrap_
添加到调用站点的符号,并在被调用站点添加 __real_
。
所以我查找了运算符的名称,因为 linker 对 C++ 一无所知;它只是不需要知道。 ;-) 好吧,我们使用的版本中的 G++ 有这个 "translation":
_Znwy := operator new(unsigned long long)
_Znay := operator new[](unsigned long long)
_ZdlPv := operator delete(void*)
_ZdaPv := operator delete[](void*)
现在我可以使用 C 内存分配器使用运算符的替换来扩展测试驱动程序。 (感谢 C++ 人员,将这些东西留在库中!)
// testdriver.cpp
#include <cstring> // for malloc() and free()
// ...
extern "C" void* __wrap__Znwy(size_t size)
{
return malloc(size);
}
extern "C" void* __wrap__Znay(size_t size)
{
return malloc(size);
}
extern "C" void __wrap__ZdlPv(void* block)
{
free(block);
}
extern "C" void __wrap__ZdaPv(void* block)
{
free(block);
}
// ...
这些是被测模块中真正的操作符的声明,供测试驱动程序调用。
// testdriver.cpp
// ...
extern "C" void* __real__Znwy(size_t size);
extern "C" void* __real__Znay(size_t size);
extern "C" void __real__ZdlPv(void* block);
extern "C" void __real__ZdaPv(void* block);
// ...
link 的命令现在是:
g++ -Wall -Wextra -pedantic -std=c++11 -Wl,-wrap,_Znwy,-wrap,_Znay,-wrap,_ZdlPv,-wrap,_ZdaPv testdriver.o framework.o allocator.o -o testdriver
这也奏效了。但它有点复杂和丑陋。而且它只适用于 GCC,另外我不确定不同的版本是否会保留这些经过处理的名称。很可能他们这样做,为了兼容,但谁知道呢。
我的一个问题
感谢阅读所有这些,我的问题来了:
我还能尝试什么?
我正在寻找一种解决方案,它不会改变模块的源代码,并且适用于(大部分)任何编译器。
好吧,伙计们,我没有偷懒和玩弄我的拇指。
我找到了一个既简单(非原始)又优雅的解决方案。它可能对其他人不起作用,但对我们有用。
GCC 的预处理器可以选择在处理实际源代码之前包含另一个文件。我用它来重新定义关键字 operator
并将其扩展为特定于 class 的运算符。
// instrumentation.h
#include <cstddef> // for size_t
class T
{
public:
void* operator new(size_t);
void* operator new[](size_t);
void operator delete(void*);
void operator delete[](void*);
};
#define operator T::operator
用这个编译被测模块:
g++ -Wall -Wextra -pedantic -std=c++11 -c -Wp,-include,instrumentation.h allocator.cpp -o allocator.o
现在以前替换的运算符不再替换全局运算符。它们 "wrapped" 变成了 class,测试驱动程序可以在这个包装器 class 的实例上调用它们。测试驱动程序在包含 instrumentation.h
后立即取消定义 operator
当然很重要,否则您将在测试驱动程序的其余代码中遇到大量错误。
// testdriver.cpp
#include <iostream>
#include "framework.h"
#include "allocator.h"
#include "instrumentation.h"
#undef operator
int main()
{
framework();
T* p1 = new T; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
T* p2 = new T[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
Allocator::setup();
p1 = new T;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new T[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
framework();
return 0;
}
如果编译器的预处理器没有这样的选项,可以使用一个小的包装器来代替。
// allocator_wrapper.cpp
#include "instrumentation.h"
#include "allocator.cpp"