LINUX.ORG.RU

Хитрый макрос


0

1

Давно не писал на чистом Си (С++ либо python обычно), а тут вот возникла необходимость написать маленького демона, но т.к. привык к разработке через TDD, то захотелось мне так и в Си сделать, краткий обзор тестовых фреймворков для Си показал, что кое что есть, но после gtest/gmock как-то всё не понравилось и начал я городить свой маленький велосипед посмотрев презентацию о разработке через тесты в Си. Вобщем всё довольно просто, написал один макрос вида :

#define check(expr) assert((expr) && __FILE__ && __LINE__)
И потом просто одна функция - один тест, в котором один или несколько этих самых check, типа
void test_fibonachi() {
  check(fibonachi(0) == 1);
  check(fibonachi(1) == 1);
  check(fibonachi(2) == 2);
  printf("%s %s .... PASS\n", __FILE__, __func__);
}
, а в main вызываеться каждая функция, для наглядности в конце каждой функции:
printf("%s %s .... PASS\n", __FILE__, __func__);
А значит дубляж кода и я должен это отрефакторить и пришла мне в голову мысль навеянная gtest, а не написать-ли мне макрос который такое:
TEST(testname) {
some test code
}
превратит в такое:
void testname() {
some test code
}
тут что-то сделать чтобы запомнить адресс функции и её название
в какой-то массив структур 
а потом бы в main что-то типа
int main(int argc, char **argv) {
  run_all_tests();
  return EXIT_SUCCESS;
}
ну а сам run_all_tests() нечто вроде
void run_all_tests() {
  for(int i=0; i < tests_count; i++) {
     tests[i].func_ptr();
     printf("%s %s .... PASS\n", tests[i].file_name, tests[i].func_name);
  }
}
И всё бы вроде хорошо, всё понятно но как так выкрутиться чтобы заполнить эту самую структуру, а именно как заставить выполняться некий код написанный рядом с функцией? Смотрел исходники gtest, то там всё проще - они классы новые создают, а тут я даже не знаю как поступить..

Задавай массив тестов вручную || набросай утилиту, которая пройдётся по файлу с соберёт сама этот массив тестов.

schizoid ★★★ ()

Поступить можно двумя способами: использовать дополнительный препроцессор, кроме cpp, либо забыть про переносимость между компиляторами и использовать в gcc «__attribute__ ((constructor))»

kim-roader ★★ ()
Ответ на: комментарий от kim-roader

Спасибо! Впринципе дополнительный препроцессор вроде как и есть (m4) ибо собирается все через autotools, но «constructor» очень заманчиво, главное чтобы версия gcc в ДЦ его поддерживала, а то я раз уже с атрибутами попался, благо тесты сразу показали..

TaranSergey ()

А чем CUnit (http://cunit.sourceforge.net/) не нравится?
Вопрос исключительно для собственно развития, так как пока имел опыт общения только с CUnit и CppUnit и, на мой взгляд, CUnit явно выигрывает, возможно это связано с общем низким знанием C++, так как большая часть работы связана с С.

imb ★★ ()
Ответ на: комментарий от TaranSergey

С автотулзами можно делать filename.c.in, а там использовать всякие define(tests, tests`test1, test2, test3') для накапливания списка тестов и вставления его в конец.

kim-roader ★★ ()
Ответ на: комментарий от imb

Конкретно в данной задаче он был избыточен, программа не большая (около 1k строк) и довольно простая, не хотелось тянуть для нее зависимости учитывая автоматизированную сборку пакетов в моке старого центоса это могло стать большим гимором :) Да и балованный я стал после gtest/gmock в c++ и pydoubles в python, чем-то мне синтаксис не понравился что ли, простоты хотелось вобщем.. Я нашел очень простой вариант для встраиваемых систем, но там было всего пару макросов которые мне бы наверняка захотелось расширить, а как посмотрел ту презентацию понял что сделать простоту очень легко, не смотря на «велосипедность» ситуации.. А может я и не прав и пошел не той дорогой, ведь я тоже не гуру, время покажет :) хотя проект уже вроде как и готов, развивать там особо не чего..

TaranSergey ()
Ответ на: комментарий от kim-roader

Тьфу блин, прошу прощения, m4 гарантированно доступно только на этапе генерации ./configure файла (ибо автотулзы гарантируют работу на системе в которой есть только позикс тулзы). Так что для переносимой сборки придётся создавать ./m4/test.m4 файлы, и прописывать в configure.ac генерацию test.c файлов.

kim-roader ★★ ()
Ответ на: комментарий от TaranSergey

По-моему хоть и велосипед, но получилось не плохо, по крайней мере мне нравиться как тестовый код выглядит теперь.. Может кому пригодиться, ну или мож кто покритикует то что вышло:

//unittest.h
#ifndef UNITTEST_H
#define UNITTEST_H
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef void test_function();
typedef struct _tests_data {
  test_function *func;
  char *name;
  char *file;
} tests_data;

extern tests_data *all_tests;
extern int tests_count;

void append_test(test_function *func, const char *name, const char *file);
void run_all_tests();

static void free_test_data() __attribute__((destructor));

#define check(expr) assert((expr) && __FILE__ && __LINE__)
#define STR(s) #s
#define TEST(name)void append_test_ ## name() __attribute__((constructor));\
  void name();\
  void append_test_ ## name() { \
    append_test(name, STR(name), __FILE__); \
  } \
  void name() 

#endif
//unittest.c
#include <unittest.h>

tests_data *all_tests = NULL;
int tests_count = 0;

void append_test(test_function *func, const char *name, const char *file) {
  all_tests = (tests_data*)realloc(all_tests, sizeof(tests_data) * (tests_count + 1));
  check(all_tests != NULL);
  all_tests[tests_count].func = func;
  all_tests[tests_count].name = strdup(name);
  all_tests[tests_count].file = strdup(file);
  tests_count++;
}

void free_test_data() {
  if(all_tests) {
    int i;
    for(i = 0; i < tests_count; i++) {
      if(all_tests[i].name) {
        free(all_tests[i].name);
      }
      if(all_tests[i].file) {
        free(all_tests[i].file);
      }
    }
    free(all_tests);
  }
}

void run_all_tests() {
  int i;
  for(i = 0; i < tests_count; i++) {
    printf("%s/%s......", all_tests[i].file, all_tests[i].name);
    all_tests[i].func();
    printf("PASS\n");
  }
  printf("------------------\n");
  printf("%d tests completed successfully!\n", tests_count);
}

Ну и вот так может выглядеть тестовый код:
//tests.c
#include <unittest.h>
#include <delimiters.h>

TEST(test_is_delimiter) {
  check(is_delimiter(' '));
  check(is_delimiter('\t'));
  check(is_delimiter('\n'));
  check(!is_delimiter('x'));
}

TEST(test_some_other) {
  ....
}

int main(int argc, char **argv) {
  run_all_tests();
}

TaranSergey ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.