c++ visual-studio unit-testing memory-leaks

C++-¿Es posible implementar pruebas de fuga de memoria en una prueba unitaria?



visual-studio unit-testing (4)

Estoy tratando de implementar pruebas unitarias para mi código y me cuesta mucho hacerlo.

Idealmente, me gustaría probar algunas clases no solo para una buena funcionalidad sino también para una asignación / desasignación de memoria adecuada. Me pregunto si esta verificación se puede hacer usando un marco de prueba de unidad. Estoy usando Visual Assert por cierto. Me encantaría ver algún código de muestra, si es posible!


1) Después de una investigación mía y basado en una muy buena solución (para Windows) de Chris Becke, he creado una solución muy similar para el sistema operativo Linux.

2) Mis objetivos de detección de fugas de memoria:

Son bastante claros: detecte fugas mientras también:

2.1) Idealmente de una manera precisa: indique exactamente cuántos bytes se asignaron AÚN no desasignados.

2.2) Mejor esfuerzo: si no es exactamente, indíquelo de manera "falsamente positiva" (infórmenos sobre una fuga, incluso si no es necesariamente una y al mismo tiempo NO se pierda ninguna detección de fugas). Es mejor ser más duros con nosotros mismos aquí.

2.3) Ya que estoy escribiendo mis pruebas de unidad en el marco de GTest, pruebe cada prueba de unidad de GTest como una "entidad atómica".

2.4) Tenga en cuenta también las asignaciones de "estilo C" (desasignaciones) utilizando malloc / free.

2.5) Lo ideal es tener en cuenta C ++ "asignaciones in situ".

2.6) Fácil de usar e integrar en un código existente (clases basadas en GTest para pruebas de unidad).

2.7) Tiene la capacidad de "configurar" las configuraciones de las comprobaciones principales (habilitar / deshabilitar la comprobación de memoria, etc.) para cada prueba y / o toda la clase de prueba.

3) Arquitectura de la solución:

Mi solución utiliza las capacidades heredadas de usar el marco GTest, por lo que define una clase "base" para cada clase de prueba unitaria que agregaremos en el futuro. Básicamente, las funcionalidades principales de la clase base se pueden dividir en las siguientes:

3.1) Ejecute la "primera" prueba de estilo GTest para comprender la cantidad de "memoria adicional" asignada en el montón en caso de una falla de prueba. Como lo mencionó Chris Becke en la última oración de su respuesta anterior.

3.2) Fácil de integrar: simplemente herede de esta clase base y escriba las funciones de "estilo TEST_F" de sus pruebas unitarias.

3.3.1) Para cada prueba, podemos decidir si se establece o no la comprobación de fugas de memoria. Esto se realiza a través del método SetIgnoreMemoryLeakCheckForThisTest (). Nota: No es necesario que lo "reinicie" de nuevo; ocurrirá automáticamente para la próxima prueba debido a la forma en que funcionan las pruebas de unidad de GTest (llaman al Ctor antes de cada llamada de función).

3.3.2) Además, si por alguna razón sabe de antemano que su prueba "perderá" algunas desasignaciones de memoria y sabe la cantidad, puede aprovechar las dos funciones para tener en cuenta este hecho una vez que realice la verificación de la memoria (que, por cierto, se realiza al "simplemente" restar la cantidad de memoria en uso al comienzo de la prueba de la cantidad de memoria en uso al final de la prueba).

A continuación se muestra la clase base de encabezado:

// memoryLeakDetector.h: #include "gtest/gtest.h" extern int g_numOfExtraBytesAllocatedByGtestUponTestFailure; // The fixture for testing class Foo. class MemoryLeakDetectorBase : public ::testing::Test { // methods: // ------- public: void SetIgnoreMemoryLeakCheckForThisTest() { m_ignoreMemoryLeakCheckForThisTest= true; } void SetIsFirstCheckRun() { m_isFirstTestRun = true; } protected: // You can do set-up work for each test here. MemoryLeakDetectorBase(); // You can do clean-up work that doesn''t throw exceptions here. virtual ~MemoryLeakDetectorBase(); // If the constructor and destructor are not enough for setting up // and cleaning up each test, you can define the following methods: // Code here will be called immediately after the constructor (right // before each test). virtual void SetUp(); // Code here will be called immediately after each test (right // before the destructor). virtual void TearDown(); private: void getSmartDiff(int naiveDiff); // Add the extra memory check logic according to our // settings for each test (this method is invoked right // after the Dtor). virtual void PerformMemoryCheckLogic(); // members: // ------- private: bool m_ignoreMemoryLeakCheckForThisTest; bool m_isFirstTestRun; bool m_getSmartDiff; size_t m_numOfBytesNotToConsiderAsMemoryLeakForThisTest; int m_firstCheck; int m_secondCheck; };

Y aquí está la fuente de esta clase base:

// memoryLeakDetectorBase.cpp #include <iostream> #include <malloc.h> #include "memoryLeakDetectorBase.h" int g_numOfExtraBytesAllocatedByGtestUponTestFailure = 0; static int display_mallinfo_and_return_uordblks() { struct mallinfo mi; mi = mallinfo(); std::cout << "========================================" << std::endl; std::cout << "========================================" << std::endl; std::cout << "Total non-mmapped bytes (arena):" << mi.arena << std::endl; std::cout << "# of free chunks (ordblks):" << mi.ordblks << std::endl; std::cout << "# of free fastbin blocks (smblks):" << mi.smblks << std::endl; std::cout << "# of mapped regions (hblks):" << mi.hblks << std::endl; std::cout << "Bytes in mapped regions (hblkhd):"<< mi.hblkhd << std::endl; std::cout << "Max. total allocated space (usmblks):"<< mi.usmblks << std::endl; std::cout << "Free bytes held in fastbins (fsmblks):"<< mi.fsmblks << std::endl; std::cout << "Total allocated space (uordblks):"<< mi.uordblks << std::endl; std::cout << "Total free space (fordblks):"<< mi.fordblks << std::endl; std::cout << "Topmost releasable block (keepcost):" << mi.keepcost << std::endl; std::cout << "========================================" << std::endl; std::cout << "========================================" << std::endl; std::cout << std::endl; std::cout << std::endl; return mi.uordblks; } MemoryLeakDetectorBase::MemoryLeakDetectorBase() : m_ignoreMemoryLeakCheckForThisTest(false) , m_isFirstTestRun(false) , m_getSmartDiff(false) , m_numOfBytesNotToConsiderAsMemoryLeakForThisTest(0) { std::cout << "MemoryLeakDetectorBase::MemoryLeakDetectorBase" << std::endl; m_firstCheck = display_mallinfo_and_return_uordblks(); } MemoryLeakDetectorBase::~MemoryLeakDetectorBase() { std::cout << "MemoryLeakDetectorBase::~MemoryLeakDetectorBase" << std::endl; m_secondCheck = display_mallinfo_and_return_uordblks(); PerformMemoryCheckLogic(); } void MemoryLeakDetectorBase::PerformMemoryCheckLogic() { if (m_isFirstTestRun) { std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic - after the first test" << std::endl; int diff = m_secondCheck - m_firstCheck; if ( diff > 0) { std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic - setting g_numOfExtraBytesAllocatedByGtestUponTestFailure to:" << diff << std::endl; g_numOfExtraBytesAllocatedByGtestUponTestFailure = diff; } return; } if (m_ignoreMemoryLeakCheckForThisTest) { return; } std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic" << std::endl; int naiveDiff = m_secondCheck - m_firstCheck; // in case you wish for "more accurate" difference calculation call this method if (m_getSmartDiff) { getSmartDiff(naiveDiff); } EXPECT_EQ(m_firstCheck,m_secondCheck); std::cout << "MemoryLeakDetectorBase::PerformMemoryCheckLogic - the difference is:" << naiveDiff << std::endl; } void MemoryLeakDetectorBase::getSmartDiff(int naiveDiff) { // according to some invastigations and assumemptions, it seems like once there is at least one // allocation which is not handled - GTest allocates 32 bytes on the heap, so in case the difference // prior for any further substrcutions is less than 32 - we will assume that the test does not need to // go over memory leak check... std::cout << "MemoryLeakDetectorBase::getMoreAccurateAmountOfBytesToSubstructFromSecondMemoryCheck - start" << std::endl; if (naiveDiff <= 32) { std::cout << "MemoryLeakDetectorBase::getSmartDiff - the naive diff <= 32 - ignoring..." << std::endl; return; } size_t numOfBytesToReduceFromTheSecondMemoryCheck = m_numOfBytesNotToConsiderAsMemoryLeakForThisTest + g_numOfExtraBytesAllocatedByGtestUponTestFailure; m_secondCheck -= numOfBytesToReduceFromTheSecondMemoryCheck; std::cout << "MemoryLeakDetectorBase::getSmartDiff - substructing " << numOfBytesToReduceFromTheSecondMemoryCheck << std::endl; } void MemoryLeakDetectorBase::SetUp() { std::cout << "MemoryLeakDetectorBase::SetUp" << std::endl; } void MemoryLeakDetectorBase::TearDown() { std::cout << "MemoryLeakDetectorBase::TearDown" << std::endl; } // The actual test of this module: TEST_F(MemoryLeakDetectorBase, getNumOfExtraBytesGTestAllocatesUponTestFailureTest) { std::cout << "MemoryLeakDetectorPocTest::getNumOfExtraBytesGTestAllocatesUponTestFailureTest - START" << std::endl; // Allocate some bytes on the heap and DO NOT delete them so we can find out the amount // of extra bytes GTest framework allocates upon a failure of a test. // This way, upon our legit test failure, we will be able to determine of many bytes were NOT // deleted EXACTLY by our test. std::cout << "MemoryLeakDetectorPocTest::getNumOfExtraBytesGTestAllocatesUponTestFailureTest - size of char:" << sizeof(char) << std::endl; char* pChar = new char(''g''); SetIsFirstCheckRun(); std::cout << "MemoryLeakDetectorPocTest::getNumOfExtraBytesGTestAllocatesUponTestFailureTest - END" << std::endl; }

Por último, una clase de prueba unitaria "basada en GTest" de muestra que utiliza esta clase de bases e ilustra los usos y varias POC (prueba de concepto) para todo tipo de asignaciones y verificación diferentes si somos capaces (o no) de detectar la falta de -asignaciones.

// memoryLeakDetectorPocTest.cpp #include "memoryLeakDetectorPocTest.h" #include <cstdlib> // for malloc class MyObject { public: MyObject(int a, int b) : m_a(a), m_b(b) { std::cout << "MyObject::MyObject" << std::endl; } ~MyObject() { std::cout << "MyObject::~MyObject" << std::endl; } private: int m_a; int m_b; }; MemoryLeakDetectorPocTest::MemoryLeakDetectorPocTest() { std::cout << "MemoryLeakDetectorPocTest::MemoryLeakDetectorPocTest" << std::endl; } MemoryLeakDetectorPocTest::~MemoryLeakDetectorPocTest() { std::cout << "MemoryLeakDetectorPocTest::~MemoryLeakDetectorPocTest" << std::endl; } void MemoryLeakDetectorPocTest::SetUp() { std::cout << "MemoryLeakDetectorPocTest::SetUp" << std::endl; } void MemoryLeakDetectorPocTest::TearDown() { std::cout << "MemoryLeakDetectorPocTest::TearDown" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForNativeType) { std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeType - START" << std::endl; // allocate some bytes on the heap and intentially DONT release them... const size_t numOfCharsOnHeap = 23; std::cout << "size of char is:" << sizeof(char) << " bytes" << std::endl; std::cout << "allocating " << sizeof(char) * numOfCharsOnHeap << " bytes on the heap using new []" << std::endl; char* arr = new char[numOfCharsOnHeap]; // DO NOT delete it on purpose... //delete [] arr; std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeType - END" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForUserDefinedType) { std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedType - START" << std::endl; std::cout << "size of MyObject is:" << sizeof(MyObject) << " bytes" << std::endl; std::cout << "allocating MyObject on the heap using new" << std::endl; MyObject* myObj1 = new MyObject(12, 17); delete myObj1; std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedType - END" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyMallocAllocationForNativeType) { std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - START" << std::endl; size_t numOfDoublesOnTheHeap = 3; std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - sizeof double is " << sizeof(double) << std::endl; std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - allocaitng " << sizeof(double) * numOfDoublesOnTheHeap << " bytes on the heap" << std::endl; double* arr = static_cast<double*>(malloc(sizeof(double) * numOfDoublesOnTheHeap)); // NOT free-ing them on purpose !! // free(arr); std::cout << "MemoryLeakDetectorPocTest::verifyMallocAllocationForNativeType - END" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForNativeSTLVectorType) { std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeSTLVectorType - START" << std::endl; std::vector<int> vecInt; vecInt.push_back(12); vecInt.push_back(15); vecInt.push_back(17); std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForNativeSTLVectorType - END" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyNewAllocationForUserDefinedSTLVectorType) { std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedSTLVectorType - START" << std::endl; std::vector<MyObject*> vecMyObj; vecMyObj.push_back(new MyObject(7,8)); vecMyObj.push_back(new MyObject(9,10)); size_t vecSize = vecMyObj.size(); for (int i = 0; i < vecSize; ++i) { delete vecMyObj[i]; } std::cout << "MemoryLeakDetectorPocTest::verifyNewAllocationForUserDefinedSTLVectorType - END" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyInPlaceAllocationAndDeAllocationForUserDefinedType) { std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationAndDeAllocationForUserDefinedType - START" << std::endl; void* p1 = malloc(sizeof(MyObject)); MyObject *p2 = new (p1) MyObject(12,13); p2->~MyObject(); std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationAndDeAllocationForUserDefinedType - END" << std::endl; } TEST_F(MemoryLeakDetectorPocTest, verifyInPlaceAllocationForUserDefinedType) { std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationForUserDefinedType - START" << std::endl; void* p1 = malloc(sizeof(MyObject)); MyObject *p2 = new (p1) MyObject(12,13); // Dont delete the object on purpose !! //p2->~MyObject(); std::cout << "MemoryLeakDetectorPocTest::verifyInPlaceAllocationForUserDefinedType - END" << std::endl; }

El archivo de cabecera de esta clase:

// memoryLeakDetectorPocTest.h #include "gtest/gtest.h" #include "memoryLeakDetectorBase.h" // The fixture for testing class Foo. class MemoryLeakDetectorPocTest : public MemoryLeakDetectorBase { protected: // You can do set-up work for each test here. MemoryLeakDetectorPocTest(); // You can do clean-up work that doesn''t throw exceptions here. virtual ~MemoryLeakDetectorPocTest(); // Code here will be called immediately after the constructor (right // before each test). virtual void SetUp(); // Code here will be called immediately after each test (right // before the destructor). virtual void TearDown(); };

Espero que sea útil y por favor, hágamelo saber si hay algo que no está claro.

Aclamaciones,

Chico.


Es posible que pueda detectar fugas de memoria en las pruebas al proporcionar su propia implementación de funciones nuevas, de eliminación, de malloc y gratuitas, agregando información de seguimiento de memoria en la asignación.


Puede usar la biblioteca de asignación tcmalloc de Google, que proporciona un heapchecker .

(Tenga en cuenta que la comprobación de la pila puede agregar una sobrecarga notable al rendimiento de su programa, por lo que probablemente solo desee habilitarlo en construcciones de depuración o pruebas unitarias).

Y pediste código de ejemplo, así que aquí está .


Puede utilizar la funcionalidad de depuración directamente en dev studio para realizar la comprobación de fugas, siempre que las pruebas de su unidad se ejecuten utilizando el c-tiempo de ejecución de la depuración.

Un ejemplo simple se vería así:

#include <crtdbg.h> struct CrtCheckMemory { _CrtMemState state1; _CrtMemState state2; _CrtMemState state3; CrtCheckMemory() { _CrtMemCheckpoint(&state1); } ~CrtCheckMemory() { _CrtMemCheckpoint(&state2); // using google test you can just do this. EXPECT_EQ(0,_CrtMemDifference( &state3, &state1, &state2)); // else just do this to dump the leaked blocks to stdout. if( _CrtMemDifference( &state3, &state1, &state2) ) _CrtMemDumpStatistics( &state3 ); } };

Y para usarlo en una prueba unitaria:

UNIT_TEST(blah) { CrtCheckMemory check; // TODO: add the unit test here }

Algunos marcos de prueba de unidad hacen sus propias asignaciones: Google, por ejemplo, asigna bloques cuando falla una prueba de unidad, por lo que cualquier bloque de prueba que falle por cualquier otro motivo también tiene una "fuga" positiva falsa.