Kernel Engineer
1 subscriber
1 video
3 links
Авторский блог программиста

Хабр: https://habr.com/ru/users/dan_sw/
Download Telegram
Channel created
#тестирование

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

Мне очень часто приходилось исправлять уже существующие функции, которые просто не тестировались (вручную, разумеется) на тех граничных случаях, на которых выдают неправильный результат и из раза в раз приходилось осуществлять правки, а затем снова и снова проверять всё ли там работает. Такой метод тестирования действительно способен вымотать, да и с учётом развития современных методов и подходов к тестированию это объективно не нужно.

Рассмотрим наиболее быстрый и оптимальный способ подключения фреймворка для тестирования GTest к уже существующему C++ проекту на CMake.

Предположим, файловая структура вашего проекта выглядит следующим образом:

\math\sum.h
\math\sum.cpp
CMakeLists.txt
main.cpp


В файле sum.cpp будет определена следующая простая функция вычисления суммы:

#include "sum.g"

int Sum(int a, int b)
{
return a + b;
}


А в заголовке sum.h будет расположена её сигнатура:

#pragma once

int Sum(int a, int b);


Чтобы добавить фреймворк GTest, сначала создадим новую директорию tests и добавим в неё отдельный CMakeTests.txt файл, который будет конфигурировать модуль тестирования:

# GoogleTest

include(FetchContent)

# Загрузка исходного кода GTest
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.18.0
)

# Установка флагов для оптимизации борки библиотеки
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
set(gtest_build_tests OFF CACHE BOOL "" FORCE)
set(gtest_build_samples OFF CACHE BOOL "" FORCE)

if (MSVC)
# Сборка GTest с динамической CRT
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
endif()

FetchContent_MakeAvailable(googletest)

# Активируем тестирование
enable_testing()

# Название приложение
set(EXECUTABLE_FILENAME_TEST "app")

add_executable("${EXECUTABLE_FILENAME_TEST}"
"unit/easy-unit.cpp"
)

target_link_libraries("${EXECUTABLE_FILENAME_TEST}"
PRIVATE
app_core
GTest::gtest_main
)

include(GoogleTest)

gtest_discover_tests("${EXECUTABLE_FILENAME_TEST}")


Помимо прочего, следует сразу же в директорию tests добавить файл unit\easy-unit.cpp, в котором можно сразу же описать первый простой Unit-тест:

#include <gtest/gtest.h>

// Сразу подключаем заголовок для вызываемой функции
#include "math/sum.h"

TEST(MathTest, SumPositiveNumbers)
{
EXPECT_EQ(Sum(2, 3), 5);
}


Файловая структура проекта должна быть следующей:

...
\tests\unit\easy-unit.cpp
\tests\CMakeLists.txt
...


С базовой настройкой тестирования почти закончили. Осталось осуществить самый важный шаг - интегрировать модуль тестов в основной CMakeLists.txt файл. Делается это довольно просто:

# Создание статической библиотеки
add_library(app_core STATIC
"math/sum/sum.cpp"
)

# Установка публичного include пути статической библиотеки
target_include_directories(app_core
PUBLIC
${PROJECT_SOURCE_DIR}
)

# Подключение тестового компонента
add_subdirectory(tests)


Статическая библиотека необходима, чтобы в тестовом проекте (который является ответвлением от текущего) нам не приходилось импортировать все зависимости нашего приложения. Иначе каждый раз придётся вручную добавлять большое множество файлов в проект тестирования через инструкцию add_executable.

И да, тестовый проект это отдельный exe-файл, и собирается он отдельно от основного проекта (это стоит учитывать при инфраструктурных задачах).

В результате мы получаем простую интеграцию системы тестирования в проект и можем добавлять различные unit и интеграционные тесты для всевозможной проверки работоспособности нашей системы. Меньше времени на не нужную рутину, больше на программирование и решение задач.
This media is not supported in your browser
VIEW IN TELEGRAM
#из_практического_опыта

Одной из самых сложных задач в начале моего карьерного пути была реализация "простой" системы временных интервалов, которые могли между собой пересекаться (и друг друга поглощать), а также они должны были не мешать друг другу, если этих интервалов на сплошной временной линии будет находиться очень много. Даже пришлось сделать вычисление многочлена Лагранжа 6-го порядка для браузера Firefox, чтобы как можно быстрее выпустить рабочую версию продукта с этими нововведениями (координаты всё никак не хотели правильно рассчитываться на основании уже существующих формул для других браузеров). Возможно, такое решение и было костыльным, однако оно работает и работает хорошо. На какие ухищрения не пойдёшь, чтобы обеспечить кроссбраузерность :)
#системное_программирование

https://www.youtube.com/live/eUHeHWZCjz4?si=5rwjh6qVzqZ13Teh

Не так давно наткнулся на занимательное видео, в котором два спикера по очереди разбирают некоторые особенности программирования под ОС Linux через призму их практического опыта (почта mail).

Наиболее интересным (лично для меня) в данном материале был разбор проблемы дефрагментации памяти и методах её исправления.

Хоть сам видеоролик и был выпущен 9 лет назад, однако до сих пор актуален.
#тестирование

Как вообще тестируется ОС Linux? Несмотря на то, что Linux используется на миллиардах устройств по всему миру, даже ей необходимо своевременное тестирование и обнаружение потенциальных уязвимостей.

Помимо внутренних тестов самого ядра, существует отдельный проект призванный решить насколько корректно работают различные компоненты этой ОС: Linux Test Project (https://github.com/linux-test-project/ltp)

В данном проекте можно найти тестирование любого системного вызова. Например, вот так выглядит один из тестов на открытие файла через системный вызов open:

static void verify_open(unsigned int n)
{
struct tcase *tc = &tcases[n];
struct stat buf;

TST_EXP_FD_SILENT(open(tc->filename, tc->flag, tc->mode),
"open() with %s", tc->desc);
if (!TST_PASS)
return;

fd = TST_RET;

SAFE_FSTAT(fd, &buf);
if (!(buf.st_mode & tc->tst_bit))
tst_res(TFAIL, "%s is cleared unexpectedly", tc->desc);
else
tst_res(TPASS, "%s is set as expected", tc->desc);

SAFE_CLOSE(fd);
if (S_ISREG(buf.st_mode))
SAFE_UNLINK(tc->filename);
}


У интеграционных тестов, как и полагается, есть функции подготовки и очистки среды:

static void setup(void)
{
SAFE_MKDIR(TEST_DIR, 0755);
}

static void cleanup(void)
{
if (fd > 0)
SAFE_CLOSE(fd);
}


Немного разберём пример этого теста.
TST_EXP_FD_SILENT - выполняет системный вызов open и записывает результат его работы в TST_RET (глобальную переменную). Есть также глобальная переменная TST_PASS, которая отвечает за прохождение всего тела TST_EXP_FD_SILENT. Если TST_PASS = 0, то на каком-то этапе функция преждевременно завершила своё выполнение.

#define TST_EXP_POSITIVE__(SCALL, SSCALL, ...)       \
do { \
TEST(SCALL); \
TST_PASS = 0; \
\
if (TST_RET == -1) { \
TST_MSG_(TFAIL | TTERRNO, " failed", \
SSCALL, ##__VA_ARGS__); \
break; \
} \
if (TST_RET < 0) { \
TST_MSGP_(TFAIL | TTERRNO, " invalid retval %ld",\
TST_RET, SSCALL, ##__VA_ARGS__); \
break; \
} \
\
TST_PASS = 1; \
} while (0)

#define TST_EXP_FD_SILENT(SCALL, ...) \
TST_EXP_POSITIVE__(SCALL, #SCALL, ##__VA_ARGS__)


TST_RET, в данном случае, является дескриптором файла. И далее по телу теста используя данный дескриптор файла выполняются дополнительные проверки. В общем-то, ничего сложного.

Изучение Linux API и в целом внутреннее устройство данной ОС через (в том числе) её программные тесты однозначно может принести пользу для понимания как это всё устроено на практике.
#системное_программирование

Довольно часто при разработке приложений на C/C++ под Linux приходится сталкиваться с обработкой глобальной переменной errno. Как правило именно в неё записывают код ошибки большинство системных вызовов Linux. Да и в некоторых местах Windows также использует эту глобальную переменную, как и STL (особенно при работе с файлами).

Формально, в однопоточной среде, определение errno выглядит следующим образом:

extern int errno;


Т.е. это просто глобальная переменная, подключаемая через заголовочный файл errno.h.

Однако в многопоточной среде, её определение следует интерпретировать несколько иначе:
[https://git.musl-libc.org/cgit/musl/tree/src/errno/__errno_location.c]

#include <errno.h>
#include <tls.h>

int *
__errno_location (void)
{
return &errno;
}
libc_hidden_def (__errno_location)

// ...
#define errno (*__errno_location())


Я не просто так подчеркнул слово "интерпретировать". Всё дело в том, что в каждой библиотеке своя реализация данного определения, но суть у всех одна - показать, что errno - это по сути вызов функции с разыменованием её результата (результат - адрес на конкретный участок памяти потока называемый Thread Local Storage, в которой и содержится значение ошибки). В примере выше это не так очевидно (т.к. в основном вся внутренняя логика скрыта в runtime), но следующие примеры будут более явные.

Такой вызов потокобезопасен, поскольку предполагает, что каждый поток будет обращаться к своему участку памяти, поэтому вы можете быть спокойны при обработке errno в разных потоках. Однако асинхронно-сигнальная среда в Linux не безопасна даже для такого определения errno, и на это стоит обратить внимание.

Для наиболее лучшей интерпретации определения errno через функцию предлагаю ознакомится с этой реализацией:
[https://git.musl-libc.org/cgit/musl/tree/src/errno/__errno_location.c]

#include <errno.h>
#include "pthread_impl.h"

int *__errno_location(void)
{
return &__pthread_self()->errno_val;
}

weak_alias(__errno_location, ___errno_location);


В данной реализации сразу видно, что значение errno получается после ссылки на текущий поток. Кстати, такая реализация используется и в однопоточной среде, интерпретировать errno просто как глобальную переменную проще (но не правильно).

Это похоже на реализацию errno в MSVCRT:
[https://github.com/wine-mirror/wine/blob/master/dlls/msvcrt/errno.c]

int* CDECL _errno(void)
{
return &msvcrt_get_thread_data()->thread_errno;
}