Как выполнить модульное тестирование пользовательских `operator new` и` operator delete` без изменения / настройки источника модуля? - PullRequest
1 голос
/ 29 апреля 2020

Так как эту проблему нельзя объяснить несколькими строчками, пожалуйста, не забывайте меня и размер этого вопроса.

Ситуация

Мы разрабатываем встроенную систему, которая нуждается в управлять своей кучей самостоятельно, заменяя операторов new, new[], delete и delete[]. Эти определяемые пользователем замещающие функции реализованы в их собственном модуле.

Мы решили использовать класс с методами stati c, мы могли бы также использовать пространство имен, чтобы глобальное пространство имен не загромождалось.

// 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 StackOverflow 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 StackOverflow example
}

void operator delete[](void*)
{
    // shutdown(); // commented out for StackOverflow example
}

На самом деле ОЗУ для кучи выделяется по-разному, но здесь это не имеет значения.

Для модульного тестирования этот модуль мы используем GoogleTest , но конкретная c среда тестирования на самом деле не имеет значения. Мы могли бы использовать любой другой фреймворк.

Следующий источник представляет собой симуляцию фреймворка для тестирования, который я составил для исследования проблемы. Он использует глобальные операторы 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 как минимум.

Эти Вот команды для компиляции и компоновки:

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

Теперь тест-драйвер может просто вызывать эти функции, потому что они больше не являются операторами.

Но наша безопасность персонал сказал "Ни за что!" И я должен согласиться. Тестирование инструментов в программном обеспечении, связанном с безопасностью, опасно, потому что оно может проникнуть в конечный продукт. Вы просто не делаете этого.

Второе решение, например agile из-за зависимостей от компилятора и его версии

Мы используем G CC в его воплощении MinGW64, поэтому я придумал опцию компоновщика -wrap. В одном предложении: эта опция заставляет компоновщика добавлять __wrap_ к символам на вызывающем сайте и __real_ на вызываемом сайте.

Поэтому я посмотрел искаженные имена операторов, так как компоновщик ничего не знает о C ++; это просто не нужно знать. ;-) Ну, G ++ в используемой нами версии имеет такой «перевод»:

_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);

// ...

Команда для связи теперь:

g++ -Wall -Wextra -pedantic -std=c++11 -Wl,-wrap,_Znwy,-wrap,_Znay,-wrap,_ZdlPv,-wrap,_ZdaPv testdriver.o framework.o allocator.o -o testdriver

Это тоже сработало. Но это довольно сложно и безобразно. И это работает только с G CC, кроме того, я не уверен, что разные версии будут держать эти искаженные имена. Скорее всего, они совместимы, но кто знает.

Мой единственный вопрос

Спасибо, что прочитали все это, вот мой вопрос:

Что еще можно попробовать?

Я ищу решение, которое не изменяет исходный код модуля и работает (в основном) с любым компилятором.

1 Ответ

0 голосов
/ 29 апреля 2020

Ну, ребята, я не ленился и вертел пальцами.

Я нашел решение, которое было бы простым (не примитивным) и элегантным. Он может не работать ни для кого другого, но он работает для нас.

Препроцессор G CC может использовать другой файл перед обработкой фактического источника. Я использую это, чтобы переопределить ключевое слово operator и расширить его до оператора, специфицирующего класс c.

// 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

Теперь ранее заменяющие операторы не больше не заменяют глобальные операторы. Они «обернуты» в класс, и тест-драйвер может вызывать их в экземплярах этого класса-оболочки. Конечно, для тестдрайвера важно отменять определение operator сразу после включения instrumentation.h, иначе вы получите тонны ошибок в остальной части кода тестдрайвера.

// 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"
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...