Люди нередко хотят поместить функцию в отдельный «режим» только в модульных тестах. Возможно, в нетестовом режиме вы хотите записывать вывод непосредственно в stdout , но в модульных тестах вы хотите записывать во внутренний буфер, содержимое которого вы можете проверить в тесте. Возможно, в тесте вы хотите получить данные из файла, но это было бы небезопасно делать в нетестовом коде, поэтому вы хотите предотвратить случайное выполнение этого в нетестовом коде.

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

только тест

Это работает с использованием флага testonly, поддерживаемого большинством правил сборки в Bazel. Этот флаг можно использовать в большинстве правил сборки, и когда вы testonly = True в библиотеке, от этой библиотеки могут зависеть только модульные тесты (правила, объявленные с cc_test или подобным) или другие библиотеки, которые также имеют testonly = True. Объединив это с закрытыми статическими переменными-членами и friend функциями, мы можем добиться желаемого эффекта: способ включить специальное тестирование только для функции, которую действительно можно использовать только в тестах.

Я продемонстрирую это с помощью распределителя памяти графического процессора. Этот аллокатор выделяет память графического процессора, но у нас есть несколько модульных тестов, которые проверяют структуру данных, содержащую память графического процессора, хотя тест на самом деле не требует, чтобы эта память находилась на графическом процессоре. Графические процессоры дороги, поэтому мы предпочитаем запускать этот тест на машине без графического процессора, нам просто нужно найти способ построить эту структуру данных, содержащую память графического процессора, даже если у нас нет графического процессора.

Для этого нам понадобятся две библиотеки, одна из которых помечена testonly.

СТРОИТЬ

В вашем файле сборки объявлены 2 библиотеки: «основная» библиотека и отдельная библиотека testonly, определяющая функцию, которая переключает режимы.

cc_library(
    name = "allocate_gpu_memory",
    hdrs = ["allocate_gpu_memory.h"],
)
cc_library(
    name = "switch_gpu_memory",
    # Bazel will prevent non-test targets from using this library
    testonly = True, 
    hdrs = ["switch_gpu_memory.h"],
)

Цель allocate_gpu_memory содержит функцию, которая нас действительно интересует, а switch_gpu_memory содержит функцию, которая сообщает allocate_gpu_memory изменить режимы. Это функция, которую мы хотим вызывать только из модульных тестов, поэтому мы помечаем эту библиотеку как testonly = True.

allocate_gpu_memory.h

Заголовок основной библиотеки объявляет приватную переменную, которая включает отдельный режим, и friends функции в switch_gpu_memory библиотеке, чтобы она могла переключать режимы.

class GpuMemorySwitch {
private:
    static bool gUseNonGpuMemory = false;
    friend SwitchToNonGpuMemory;
    friend AllocateGpuMemory;
};
void* AllocateGpuMemory(size_t size) {
    if (GpuMemorySwitch::gUseNonGpuMemory) {
        return malloc(size);
    } else {
        return malloc_gpu(size);
    }
}

Здесь важно то, что gUseNonGpuMemory равно private. Его нельзя изменить кроме с помощью его friend функций, и все функции, которые его изменяют, определены в testonly библиотеке.

Основная функция, о которой мы действительно заботимся, считывает эту глобальную переменную, чтобы определить, какие еще активны, поэтому она также должна быть другом. Вместо этого вы можете сделать его статической функцией-членом GpuMemorySwitch, если предпочитаете такой стиль API. Вероятно, в этом случае вы также не стали бы включать Switch в имя класса, содержащего эту статическую переменную.

switch_gpu_memory.h

Библиотека testonly содержит функцию переключения режимов. Не так уж и много.

void SwitchToNonGpuMemory() {
    GpuMemorySwitch::gUseNonGpuMemory = true;
}

Вы можете проявить фантазию и добавить более продвинутые функции, такие как возможность обратного переключения на память графического процессора. Я расскажу о некоторых из этих вариантов позже.

unit_test.cpp

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

#include <switch_gpu_memory.h>
TEST(SomeFunction, tests) {
    SwitchToNonGpuMemory(); 
    // other test code
}

Передовые методы

По мере роста ваших потребностей вам может потребоваться более гибкий API. Далее я покажу вам, как добавить возможность временного переключения режимов. Одним из вариантов использования этого может быть попеременный запуск теста с памятью графического процессора и процессора.

Мы собираемся добиться этого с помощью объекта RAII, который меняет режимы в конструкторе и обратно в деструкторе. Не забудьте friend добавить этот класс в свою основную библиотеку!

class SwitchToCpuMemory {
    bool _oldState;
public:
    SwitchToCpuMemory()
        :_oldState(GpuMemorySwitch::gUseNonGpuMemory)
    {
        GpuMemorySwitch::gUseNonGpuMemory = true;
    }
    ~SwitchToCpuMemory() {
        GpuMemorySwitch::gUseNonGpuMemory = _oldState;
    }
};

Это позволяет вам писать такой код:

for (auto&& test : tests) {
    test();
    auto switch = SwitchToCpuMemory();
    test();    
}

а затем все ваши тесты тестируются как с памятью ЦП, так и с памятью графического процессора. Самая важная часть этого класса — сохранение предыдущего значения в элементе _oldState. Это помогает избежать сложных ошибок, когда деструктор всегда включает память графического процессора, даже если она была отключена до вызова конструктора. Если вы будете писать такой класс в будущем, вы также можете поместить атрибут [[nodiscard]] в этот конструктор, чтобы предотвратить немедленное уничтожение объекта и переключиться обратно в старый режим.

Честное предупреждение, этот метод не поможет вам освободить память с помощью функции освобождения графического процессора, если выделенная память живет дольше, чем объект SwitchToCpuMemory. Вам понадобится что-то еще более продвинутое, если вам нужно это сделать.

Вывод

Я не могу однозначно рекомендовать использовать этот шаблон. Переключение режимов в модульных тестах не обязательно является хорошей идеей, поскольку это может привести к тому, что модульные тесты будут вести себя иначе, чем в рабочей среде, что может привести к появлению ошибок, которые трудно обнаружить с помощью модульных тестов. Этот метод также основан на глобальных переменных, на которые обычно не обращают внимания, и которые также создают проблемы, такие как синхронизация переменной переключателя в многопоточной среде, которая не рассматривается в этой статье. Но иногда переключение режимов действительно является лучшим решением ваших проблем, и я думаю, что это довольно хороший способ реализовать переключение режимов, минимизируя при этом случайное неправильное использование и другие воздействия на остальную часть кодовой базы.