Пример того, почему удобно жить с unit-тестами

Нужна мне была функция, которая бы при низкоуровневой отладочной печати умела распечатать строку, заменив непечатаемые коды (меньше 32 и больше 126) кодами. Все крайне тривиально.

Имеем:

1234\x04\x1fooo\xff

Печататься должно:

1234<04><1f>ooo<ff>

Рождается функция:

// This function converts all non-ASCII characters (with codes less than 32
// and greater than 126) into the <xx> form where "xx" is a hexadecimal code
// of the character.
void MakeAllPrintable(const char* from, char* to, int to_capacity) {
  const static char hex[] = "0123456789abcdef";
  for (; *from && to_capacity > 1; ++from) {
    const unsigned char c = *from;
    if (c < 32 || c > 126) {
      if (to_capacity < 4 + 1)
        break;
      *to++ = '<';
      *to++ = hex[(c >> 4) & 0xf];
      *to++ = hex[c & 0xf];
      *to++ = '>';
    } else {
      *to++ = c;
    }
  }
  *to = '\0';
}

Увы, это С, а наша системы сборки частей на С, хоть и имеет cmockery, но добавление тестов всегда связано с ручным прописываением имен фунцкий, а это будет означать пересборку еще и makefile’ов, а это будет означать проверку этой пересборки на всех типах ОС, и в этоге на подготовку уйдет час или больше. А если потихоньку просто добавить функцию без теста, то 5 минут, так как изменится только один исходник. Мои мысли: “Ну я, типа, такой катаный колобок, врядли налажаю в столь простой функции. Так что можно без тестов.“. И, конечно, налажал. Код выше имеет конкретный косяк. Он бы, естественно, всплыл, может даже на code review, но в итоге я отогнал все ренегатские мысли, сделал всю рутину и сел за тесты, хотя по уму, я их должен быть писать до.

Очевидно, что вполне логичный (предпоследний) тест, там, где последний параметр 5, сразу показал, что я забыл проверку максимальной длины буфера (эдакая назадача!):

void test_MakeAllPrintable(void **state) {
  char buf[16 * 1024];

  MakeAllPrintable("", buf, sizeof(buf));
  assert_int_equal(strlen(buf), 0);

  MakeAllPrintable("abc", buf, sizeof(buf));
  assert_string_equal(buf, "abc");

  // This check verifies that the function always cleans the buffer at start.
  MakeAllPrintable("", buf, sizeof(buf));
  assert_int_equal(strlen(buf), 0);

  // This check verifies that the function doesn't overwrite the given buffer
  // and always reserves the last byte for '\0'.
  MakeAllPrintable("12345678", buf, 5);
  assert_string_equal(buf, "1234");$
  MakeAllPrintable("1\x01\r\n\t\x1f 2\x7e\x7f\x80\xff", buf, sizeof(buf));
  assert_string_equal(buf, "1<01><0d><0a><09><1f> 2~<7f><80><ff>");
}

А вот как код должен был выглядеть:

// This function converts all non-ASCII characters (with codes less than 32
// and greater than 126) into the <xx> form where "xx" is a hexadecimal code
// of the character.
void MakeAllPrintable(const char* from, char* to, int to_capacity) {
  const static char hex[] = "0123456789abcdef";
  for (; *from && to_capacity > 1; ++from) {
    const unsigned char c = *from;
    if (c < 32 || c > 126) {
      if (to_capacity < 4 + 1)
        break;
      *to++ = '<';
      *to++ = hex[(c >> 4) & 0xf];
      *to++ = hex[c & 0xf];
      *to++ = '>';
      to_capacity -= 4;   // (!!!)
    } else {
      *to++ = c;
      --to_capacity;      // (!!!)
    }
  }
  *to = '\0';
}

Пример, безусловно, примитивный, но очень показательный. Принцип простой: “нет тестов, нет кода”. Когда жизненный цикл проекта более нескольких дней, тесты обязаны быть частью кода, и по мне, не должно быть разделения на production code и test code по большому счету, так как первое без второго не существует.


Disclaimer

Комментарии