C++ - 使用带有结构的 GTest 值参数化测试会导致 valgrind 错误

Posted

技术标签:

【中文标题】C++ - 使用带有结构的 GTest 值参数化测试会导致 valgrind 错误【英文标题】:C++ - using GTest value-parameterized tests with structs causes valgrind errors 【发布时间】:2016-02-18 05:46:33 【问题描述】:

您能帮我了解一下 GTest 和结构打包的情况吗?

问题似乎与在 GTest 中的值参数化测试中用作值时如何打包结构有关。采用直接为每个值实例化结构的方法会导致与未初始化值相关的 valgrind 错误。

以下是相关代码:

#include <gtest/gtest.h>

struct TestItem

    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
;

class TestBase : public ::testing::Test, public ::testing::WithParamInterface<TestItem> ;

TEST_P(TestBase, TestAtoi)

    TestItem item = GetParam();
    std::cout << sizeof(TestItem) << std::endl;
    ASSERT_FALSE(0);   // actual test doesn't matter


INSTANTIATE_TEST_CASE_P(
        TestBaseInstantiation,
        TestBase,
        ::testing::Values(
                TestItem  "0", 0, 0, 0 
));

当它被编译和链接时(我用 cmake 构建了 GTest):

g++ gtest_valgrind.c -o gtest_valgrind -I gtest-1.7.0/include -L gtest-1.7.0/build -lgtest -lpthread -lgtest_main -std=c++11

然后执行:

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./gtest_valgrind

产生以下输出:

==17290== Memcheck, a memory error detector
==17290== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==17290== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==17290== Command: ./gtest_valgrind
==17290== 
Running main() from gtest_main.cc
==17290== Use of uninitialised value of size 8
==17290==    at 0x55B89F1: _itoa_word (_itoa.c:180)
==17290==    by 0x55BC6F6: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55B89F8: _itoa_word (_itoa.c:180)
==17290==    by 0x55BC6F6: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55BC742: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409AA6: testing::internal::UniversalPrinter<TestItem>::Print(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55B9659: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409AA6: testing::internal::UniversalPrinter<TestItem>::Print(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
==17290== Conditional jump or move depends on uninitialised value(s)
==17290==    at 0x55B96DC: vfprintf (vfprintf.c:1660)
==17290==    by 0x55E1578: vsnprintf (vsnprintf.c:119)
==17290==    by 0x55C3531: snprintf (snprintf.c:33)
==17290==    by 0x41F107: testing::(anonymous namespace)::PrintByteSegmentInObjectTo(unsigned char const*, unsigned long, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F1AA: testing::(anonymous namespace)::PrintBytesInObjectToImpl(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x41F252: testing::internal2::PrintBytesInObjectTo(unsigned char const*, unsigned long, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B8E: testing::internal2::TypeWithoutFormatter<TestItem, (testing::internal2::TypeKind)2>::PrintValue(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B63: std::basic_ostream<char, std::char_traits<char> >& testing::internal2::operator<< <char, std::char_traits<char>, TestItem>(std::basic_ostream<char, std::char_traits<char> >&, TestItem const&) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B3E: void testing_internal::DefaultPrintNonContainerTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409B19: void testing::internal::DefaultPrintTo<TestItem>(char, testing::internal::bool_constant<false>, TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409ADB: void testing::internal::PrintTo<TestItem>(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==    by 0x409AA6: testing::internal::UniversalPrinter<TestItem>::Print(TestItem const&, std::ostream*) (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290==  Uninitialised value was created by a stack allocation
==17290==    at 0x404AAE: gtest_TestBaseInstantiationTestBase_EvalGenerator_() (in /home/davida/git/gitlab/fcdm/lwm2m.git/gtest_valgrind)
==17290== 
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from TestBaseInstantiation/TestBase
[ RUN      ] TestBaseInstantiation/TestBase.TestAtoi/0
24
[       OK ] TestBaseInstantiation/TestBase.TestAtoi/0 (76 ms)
[----------] 1 test from TestBaseInstantiation/TestBase (126 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (246 ms total)
[  PASSED  ] 1 test.
==17290== 
==17290== HEAP SUMMARY:
==17290==     in use at exit: 0 bytes in 0 blocks
==17290==   total heap usage: 239 allocs, 239 frees, 46,622 bytes allocated
==17290== 
==17290== All heap blocks were freed -- no leaks are possible
==17290== 
==17290== For counts of detected and suppressed errors, rerun with: -v
==17290== ERROR SUMMARY: 20 errors from 5 contexts (suppressed: 0 from 0)

这是很多输出,但基本上我认为这表明由于INSTANTIATE_TEST_CASE_P 中的TestItem "0", 0, 0, 0 行而实例化的结构存在一些不寻常之处。

还要注意,TestItem 的大小输出为 24。在 64 位系统上,我不太确定如何协调这一点。 char * 为 8 个字节,int 成员为 4 * 3 = 12 个字节。如果它们与 8 字节边界对齐,则大小应为 24 + 4 = 28 字节(如果包括打包,则为 32)。如果它们与 4 字节边界对齐,那么这应该是 20。关于结构打包,我在这里有些不明白。

可以通过以不同方式打包结构来消除 valgrind 警告。例如,所有这三个修改都会导致干净的 valgrind 运行:

使用#pragma pack:

#pragma pack(1)
struct TestItem

    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
;

输出20

使用 GNU g++ 打包属性:

struct TestItem

    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
 __attribute__((packed));

也输出20

最后,为结构添加一个虚拟值:

struct TestItem

    const char * aString;
    int anInt0;
    int anInt1;
    int anInt2;
    int anInt3;   // add an extra member
;

输出24

我有相当多的项目代码使用这种精确技术进行值参数化测试,在所有情况下,如果不使用其中一种避免策略,valgrind 都会抱怨。

我想知道我为这个测试创建值的方法是否存在根本性的问题,或者这是 gtest 实例化测试用例的结果?或者,最不可能的是,这是一个 gtest 错误吗?

【问题讨论】:

【参考方案1】:

TestItem 结构的sizeof 的输出是编译器structure alignment and trailing padding 的结果。

从上面的链接:

[...] 它是结构数据之后的第一个地址,具有相同的 对齐作为结构

尾随结构填充的一般规则是:编译器 将表现得好像结构有尾随填充到它的 跨步地址。此规则控制 sizeof() 将返回的内容。

在 64 位 x86 或 ARM 机器上考虑这个示例:

struct foo3 
    char *p;     /* 8 bytes */
    char c;      /* 1 byte */
;
struct foo3 singleton;
struct foo3 quad[4];

您可能认为sizeof(struct foo3) 应该是 9,但实际上是 16。步幅 地址是 (&p)[2] 的地址。因此,在四边形数组中,每个成员有 7 尾随填充字节,因为每个后续的第一个成员 struct 希望在 8 字节边界上自对齐。记忆 布局就像是这样声明的结构:

struct foo3 
     char *p;     /* 8 bytes */
     char c;      /* 1 byte */
     char pad[7];
;

这解释了为什么 sizeof(TestItem) 得到 24,因为尾随填充会将结构对齐到 sizeof (const char*) 的倍数,即 8。

这个尾随的填充字节未初始化,这就是 valgrind 报告的内容。 Gtest 正在运行一些代码,以便在测试失败时打印TestItem 参数的实际值。如果您通过测试并且 valgrind 没有显示错误,则可以确认这一点。

当您强制编译器使用特定对齐或向结构添加新成员以使结构不需要任何尾随填充时,valgrind 不会在 TestItem 实例中找到任何未初始化的字节。

Gtest 通常在打印值时调用operator&lt;&lt;,如果对象中的原始字节数组不可用,则返回打印对象中的原始字节数组。这可能就是为什么访问未初始化的尾随填充字节的原因。因此,您还可以通过为TestItem 定义operator&lt;&lt; 来消除valgrind 错误。

【讨论】:

谢谢,这是有道理的。但是测试通过了 - ASSERT_FALSE(0) 将成功,因此不会打印数据转储。但我认为你的观点仍然正确 - 测试框架必须根据 sizeof 扫描数据并在填充处读取未初始化的值。 @meowsqueak 所以 Gtest 正在尝试打印测试参数值,即使测试通过了。这很奇怪,因为您看不到标准输出中打印的实际值,对吧?您是否尝试定义operator&lt;&lt; 是的,我认为它至少在值上做了一个 snprintf,即使它不打印它。也许它准备了输出,以便实际运行测试的函数可以在必要时轻松打印预先准备好的描述。我有来源,我可以找出我是否需要。您定义 operator&lt;&lt; 的想法按预期工作,并消除了 valgrind 错误。无论如何,它比缓冲区转储有用得多,所以我可能会在整个项目中使用它。

以上是关于C++ - 使用带有结构的 GTest 值参数化测试会导致 valgrind 错误的主要内容,如果未能解决你的问题,请参考以下文章

Google开源C++单元测试框架Google Test

C++系列9:使用gtest测试

gtest单元测试框架介绍及简单使用

如何使用 Visual Studio 2005 设置 Google C++ 测试框架 (gtest)

如何使用 CMake 在 C++ 代码中运行 gtest? (未见测试)

带有 gtest 框架的 Spdlog 记录器无法正常工作