Побочный результат: проверяем Firebird с помощью PVS-Studio

    Firebird and PVS-Studio
    Сейчас мы заняты большой задачей. Мы хотим провести сравнение четырёх анализаторов кода: Cppcheck, PVS-Studio и Visual Studio 2013 (встроенный анализатор кода). Для этого мы решили проверить не менее 10 открытых проектов и проанализировать отчёты, которые выдадут анализаторы. Это очень трудоёмкая задача и пока она не завершена. Но так как ряд проектов уже проверен, то про некоторые из них можно написать статьи. Чем я сейчас и займусь. Для начала опишу, что интересного удалось найти с помощью PVS-Studio в Firebird.

    Firebird


    Firebird (FirebirdSQL) — компактная, кроссплатформенная, свободная система управления базами данных (СУБД), работающая на Linux, Microsoft Windows и разнообразных Unix платформах.

    Сайт: http://www.firebirdsql.org/

    Описание в Wikipedia: Firebird

    Давайте посмотрим, что интересного можно найти в коде этого проекта, воспользовавшись PVS-Studio.

    Неинициализированные переменные


    static const UCHAR* compile(const UCHAR* sdl, sdl_arg* arg)
    {
      SLONG n, count, variable, value, sdl_operator;
      ....
      switch (op)
      {
        ....
        case isc_sdl_add:
          sdl_operator = op_add;
        case isc_sdl_subtract:
          if (!sdl_operator)
            sdl_operator = op_subtract;
      ......
    }

    V614 Uninitialized variable 'sdl_operator' used. sdl.cpp 404

    Мне кажется, оператор 'break' между «case isc_sdl_add:» и «case isc_sdl_subtract:» отсутствует специально. Здесь не учтён случай, когда мы сразу попадаем в «case isc_sdl_subtract:». Если это произойдёт, то переменная 'sdl_operator' ещё не будет инициализирована.

    Другая схожая ситуация. Переменная 'fieldNode' может остаться неинициализированной, если «field == false».
    void blb::move(....)
    {
      ....
      const FieldNode* fieldNode;
      if (field)
      {
        if ((fieldNode = ExprNode::as<FieldNode>(field)))
        ....
      }
      ....
      const USHORT id = fieldNode->fieldId;
      ....
    }

    V614 Potentially uninitialized pointer 'fieldNode' used. blb.cpp 1043

    Вот почему плохо в функции давать разным переменным одно и то же имя:
    void realign(....)
    {
      for (....)
      {
        UCHAR* p = buffer + field->fld_offset;
        ....
        for (const burp_fld* field = relation->rel_fields;
             field; field = field->fld_next)
        {
          ....
          UCHAR* p = buffer + FB_ALIGN(p - buffer, sizeof(SSHORT));
      ........
    }

    V573 Uninitialized variable 'p' was used. The variable was used to initialize itself. restore.cpp 17535

    При инициализации второй переменной 'p' хотели использовать значение первой переменной 'p'. А получилось, что используется вторая, ещё неинициализированная переменная.

    Для авторов проекта. Посмотрите ещё сюда: restore.cpp 17536

    Опасное сравнение строк (уязвимость)


    Обратите внимание, что результат работы функции memcmp() помещается в переменную типа 'SSHORT'. 'SSHORT' это не что иное, как синоним типа 'short'.
    SSHORT TextType::compare(
      ULONG len1, const UCHAR* str1, ULONG len2, const UCHAR* str2)
    {
      ....
      SSHORT cmp = memcmp(str1, str2, MIN(len1, len2));
    
      if (cmp == 0)
        cmp = (len1 < len2 ? -1 : (len1 > len2 ? 1 : 0));
    
      return cmp;
    }

    V642 Saving the 'memcmp' function result inside the 'short' type variable is inappropriate. The significant bits could be lost breaking the program's logic. texttype.cpp 338

    Вы недоумеваете, что здесь не так?

    Давайте вспомним, что функция memcmp() возвращает значение типа 'int'. Но результат помещается в переменную типа 'short'. Происходит потеря значений старших бит. Это опасно!

    Функция возвращает следующие значения: меньше нуля, ноль или больше нуля. Под «больше нуля» может подразумеваться что угодно. Это может быть 1, 2 или 19472341. Нельзя сохранять результат работы функции memcmp() в переменную, размером меньше чем размер типа 'int'.

    Проблема может показаться надуманной. Но это самая настоящая проблема уязвимости. Именно уязвимостью была признана аналогичная ошибка в коде MySQL: Security vulnerability in MySQL/MariaDB sql/password.c. Там результат помещался в переменную типа 'char'. Тип 'short' с точки зрения безопасности не сильно лучше.

    Аналогичные опасные сравнения можно найти здесь:
    • cvt2.cpp 256
    • cvt2.cpp 522

    Опечатки


    Опечатки есть везде и всегда. Как правило большинство из них быстро находятся в процессе тестирования. Но всё равно, можно найти опечатки практически в любом проекте.
    int Parser::parseAux()
    {
      ....
      if (yyps->errflag != yyps->errflag) goto yyerrlab;
      ....
    }

    V501 There are identical sub-expressions to the left and to the right of the '!=' operator: yyps->errflag != yyps->errflag parse.cpp 23523

    Думаю, комментарии здесь не нужны. А вот здесь, наверное, использовался Copy-Paste:
    bool CMP_node_match( const qli_nod* node1, const qli_nod* node2)
    {
      ....
      if (node1->nod_desc.dsc_dtype != node2->nod_desc.dsc_dtype ||
          node2->nod_desc.dsc_scale != node2->nod_desc.dsc_scale ||
          node2->nod_desc.dsc_length != node2->nod_desc.dsc_length)
      ....
    }

    V501 There are identical sub-expressions 'node2->nod_desc.dsc_scale' to the left and to the right of the '!=' operator. compile.cpp 156

    V501 There are identical sub-expressions 'node2->nod_desc.dsc_length' to the left and to the right of the '!=' operator. compile.cpp 157

    Получается, что в функции CMP_node_match() неправильно сравниваются члены класса 'nod_desc.dsc_scale' и 'nod_desc.dsc_length'.

    Ещё одну опечатку авторы проекта могут посмотреть здесь: compile.cpp 183

    Странные циклы


    static processing_state add_row(TEXT* tabname)
    {
      ....
      unsigned i = n_cols;
      while (--i >= 0)
      {
        if (colnumber[i] == ~0u)
      {
           bldr->remove(fbStatus, i);
           if (ISQL_errmsg(fbStatus))
             return (SKIP);
        }
      }
      msg.assignRefNoIncr(bldr->getMetadata(fbStatus));
      ....
    }

    V547 Expression '-- i >= 0' is always true. Unsigned type value is always >= 0. isql.cpp 3421

    Переменная 'i' имеет тип 'unsigned'. Это значит, что переменная 'i' всегда больше или равно 0. В результате условие (--i >= 0) не имеет смысла. Оно всегда истинно.

    Этот цикл наоборот закончится раньше, чем нужно:
    SLONG LockManager::queryData(....)
    {
      ....
      for (const srq* lock_srq = (SRQ) 
             SRQ_ABS_PTR(data_header.srq_backward);
         lock_srq != &data_header;
         lock_srq = (SRQ) SRQ_ABS_PTR(lock_srq->srq_backward))
      {
        const lbl* const lock = ....;
        CHECK(lock->lbl_series == series);
        data = lock->lbl_data;
        break;
      }
      ....
    }

    Что это за подозрительный 'break'?

    Ещё одна аналогичная ситуация здесь: pag.cpp 217

    Классика


    Как всегда, много классических недоработок, связанных с указателями. В начале указатель разыменовывается, а потом проверяется на равенство нулю. Далеко не всегда это может привести к ошибке, однако это плохой и опасный код. Покажу только один пример. Остальные места перечислю в отдельном списке.
    int CCH_down_grade_dbb(void* ast_object)
    {
      ....
      SyncLockGuard bcbSync(
        &bcb->bcb_syncObject, SYNC_EXCLUSIVE, "CCH_down_grade_dbb");
      bcb->bcb_flags &= ~BCB_exclusive;
    
      if (bcb && bcb->bcb_count)
      ....
    }

    V595 The 'bcb' pointer was utilized before it was verified against nullptr. Check lines: 271, 274. cch.cpp 271

    В начале, указатель 'bcb' разыменовывается в выражении «bcb->bcb_flags &= ....». Из следующей проверки видно, что 'bcb' может оказаться равен нулю.

    Список таких мест (31 предупреждение): firebird-V595.txt

    Shift operators


    Так как Firebird собирается разными компиляторами под разные платформы, то стоит поправить сдвиги, которые приводят к неопределённому поведению. Они вполне могут проявить себя со временем негативным образом.
    const ULONG END_BUCKET = (~0) << 1;

    V610 Undefined behavior. Check the shift operator '<<. The left operand '(~0)' is negative. ods.h 337

    Нельзя сдвигать отрицательные числа. Подробнее: "Не зная брода, не лезь в воду. Часть третья".

    Здесь лучше сделать так:
    const ULONG END_BUCKET = (~0u) << 1;

    И ещё два плохих сдвига:
    • exprnodes.cpp 6185
    • array.cpp 845

    Бессмысленные проверки


    static processing_state add_row(TEXT* tabname)
    {
      ....
      unsigned varLength, scale;
      ....
      scale = msg->getScale(fbStatus, i);
      ....
      if (scale < 0)
      ....
    }

    V547 Expression 'scale < 0' is always false. Unsigned type value is never < 0. isql.cpp 3716

    Переменная 'scale' имеет тип 'unsigned'. Сравнение (scale < 0) не имеет смысла.

    Аналогично: isql.cpp 4437

    Посмотрим на другую функцию:
    static bool get_switches(....)
      ....
      if (**argv != 'n' || **argv != 'N')
      {
        fprintf(stderr, "-sqlda :  "
                "Deprecated Feature: you must use XSQLDA\n ");
        print_switches();
        return false;
      }
      ....
    }

    Неправильно обрабатываются аргументы командной строки. Условие (**argv != 'n' || **argv != 'N') выполняется всегда.

    Разное


    void FB_CARG Why::UtlInterface::getPerfCounters(
      ...., ISC_INT64* counters)
    {
      unsigned n = 0;
      ....
      memset(counters, 0, n * sizeof(ISC_INT64));
      ....
    }

    V575 The 'memset' function processes '0' elements. Inspect the third argument. perf.cpp 487

    Кажется, что переменной 'n' в теле функции забыли присвоить значение, отличное от нуля.

    Функция convert() в качестве третьего аргумента принимает длину строки:
    ULONG convert(const ULONG srcLen,
                  const UCHAR* src,
                  const ULONG dstLen,
                  UCHAR* dst,
                  ULONG* badInputPos = NULL,
                  bool ignoreTrailingSpaces = false);

    Однако, используется функция неправильно:
    string IntlUtil::escapeAttribute(....)
    {
      ....
      ULONG l;
      UCHAR* uc = (UCHAR*)(&l);
      const ULONG uSize =
        cs->getConvToUnicode().convert(size, p, sizeof(uc), uc);
      ....
    }

    V579 The convert function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. intlutil.cpp 668

    Мы имеем дело с 64-битной ошибкой, которая проявит себя в Win64.

    Выражение 'sizeof(uc)' возвращает размер указателя, а не размер буфера. Это не страшно, если размер указателя совпадает с размером переменной типа 'unsigned long'. Размер указателя совпадает с размером типа 'long', если мы программируем для Linux. Не будет проблем и в Win32.

    Ошибка возникает, когда мы скомпилируем приложение для Win64. Функция convert() будет думать, что размер буфера равен 8 байт (размер указателя). А на самом деле, размер буфера равен 4 байтам.

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

    Заключение


    Читателю, наверное, интересно узнать, что удалось найти в этом проекте с помощью Cppcheck и VS2013. Да, эти анализаторы нашли кое-что, что не удалось PVS-Studio. Но совсем немного. На этом проекте PVS-Studio показал себя лидером. Более подробную информацию можно будет найти в статье о сравнении, которую мы уже скоро начнём писать.
    PVS-Studio 220,93
    Ищем ошибки в C, C++ и C# на Windows и Linux
    Поделиться публикацией
    Похожие публикации
    Комментарии 44
    • –22
      Заранее прошу прощения за оффтоп, но: PVS-Studio, довольно серьезный продукт, имеет большое комьюнити, много кастомеров с громкими именами, и соответственно (думаю не ошибусь) — большую команду разработчиков.
      «Хочешь сделать хорошо — сделай сам» тут не пройдет, а вот «сколько людей — столько мнений» (а формальнее «стилей программирования/уровней подготовки» и тд) как раз уместно.
      В связи с этим возникла небольшая, но мне кажется интересная мысль, а не пробовали ли вы проверить PVS-Studio с помощью PVS-Studio?
    • 0
      Спасибо. Как всегда, есть над чем задуматься.

      Мне кажется, оператор 'break' между <...> отсутствует специально.

      И это не документировано? Жуть.
      • 0
        Нет. Сейчас ещё раз посмотрел. А может и забыты тут break;… ХЗ.
        	case isc_sdl_add:
        		sdl_operator = op_add;
        	case isc_sdl_subtract:
        		if (!sdl_operator)
        			sdl_operator = op_subtract;
        	case isc_sdl_multiply:
        		if (!sdl_operator)
        			sdl_operator = op_multiply;
        	case isc_sdl_divide:
        		if (!sdl_operator)
        			sdl_operator = op_divide;
        		COMPILE(p, arg);
        		COMPILE(p, arg);
        		STUFF(sdl_operator, arg);
        		return p;
        
        • +1
          нет, не забыты. но изначально надо инициализировать нулём.
      • +1
        неплохо бы в самом начале статьи указать, код какой именно версии Firebird проверялся. Например, если 3.0, то она пока в alpha-состоянии.
        • +1
          Последняя, которая была месяц назад.

          Собственно, решается задача сравнения анализаторов на наборе проектов, а не проверка самой последней версии проекта. Всё равно, авторам надо самим проверять проект, так как я вполне могу пропустить ошибки. Или могу не описать очень плохой, но работающий код, так как он не подходит для сравнения. Но этот код явно стоит изучить и отрефакторить.

          Статья хорошо показано, что анализатор находит ошибки в этом проекте, а значит может быть полезен разработчикам. Может что-то из ошибок поправлено, но зато добавлено новое. Плюс не забываем, что анализ надо выполнять регулярно, а не раз в год.
          • +2
            что значит «последняя»? В коде Firebird параллельно идут обновления версий 2.1, 2.5, 3.0.
            sourceforge.net/p/firebird/code/HEAD/tree/firebird/

            см. tags. Если trunk, то это альфа ФБ 3.0.
            • +1
              Ну и что?

              Скачкой занимался коллега. С вероятность 99% это был trunk. Могу попросить его вспомнить, но в этом нет смысла.

              Самое плохое, что можно сделать, это начать править ошибки, основываясь на этой статье.

              1. Я могу ошибаться и ошибки нет.
              2. Я могу пропустить другие ошибки.
              3. Особенно забавно, когда сразу двое доброходов правят один и тот-же код, прочитав статью. Уже были такие случаи. :) В статье написано, что перепутан 2 и 3 аргумент функции memset(). Первый меняет эти аргументы. Всё правильно. А потом второй человек, ещё раз меняет аргументы. В результате, получается то, что и было. :)

              Вот переведём статью, покажем её разработчикам. Они увидят, что можно найти ошибки. Они спокойно проверят код и поправят ошибки. Profit.
              • 0
                Статью можно не переводить — разработчики вполне себе местные :)
                • 0
                  Еще бы «местные разработчики» покупали также охотно, как американские и европейские… Но это так, реплика от «бизнеса».
                  • +1
                    Ну, Firebird Foundation тоже неместная (австралийская, правда non-profit) организация, которая оплачивает работу наших программистов, занятых фуллтайм над опенсорсным проектом. Не только наших, конечно, просто сейчас основные разработчики из Росии и Украины.
                • –1
                  не подумайте, что я прикопался, но подобные
                  — ошибки в коде альфа-версии вполне допустимы. конечно, их нужно вычищать, и спасибо вам и инструменту за это
                  — ошибки в коде релиза недопустимы, и если исследовался код релиза, то эти ошибки нужно срочно исправлять

                  в этом и был смысл моих вопросов. Либо «ахтунг!», либо «ничего страшного» :-)
                  • +2
                    Не, «ахтунг!» однозначно. Если Вы используете инкрементальный анализ на машинах всех разработчиков (т.е. автоматическую проверку только что перекомпилированных файлов), то ошибка даже в систему контроля версий не попадет.

                    А так сколько времени пройдет, пока вы ее найдете. Да еще если и на этапе тестирования только выявлять… Помните, сколько стоит исправление ошибки на каждом этапе разработки ПО?
                    • 0
                      тут я особых проблем не вижу, cvs все стерпит :-), но Windows только у двух разработчиков, так что если PVS и использовать, то только на билд-сервере.
                      • +1
                        Билд-серверный вариант у нас в PVS-Studio отлично проработан, можете пробовать.

                        Правда нельзя забывать, что «cvs стерпит», но с табличкой не поспоришь:
                        Средняя стоимость исправления дефектов в зависимости от времени их внесения и обнаружения (данные для таблицы взяты из книги С. Макконнелла 'Совершенный Код')
                        Средняя стоимость исправления дефектов в зависимости от времени их внесения и обнаружения (данные для таблицы взяты из книги С. Макконнелла 'Совершенный Код')
              • +1
                кстати, передаю спасибо от разработчиков Firebird.

                p.s. в статье есть «Что это за подозрительный 'braeak'?», при этом слова braeak я больше нигде не вижу.
                • 0
                  Т.е. Вы один из разработчиков?
                  braeak — опечатка в тексте. Спасибо. Поправил.
                  • +1
                    нет, я не разработчик, я спонсор :-) просто у разработчиков нет аккаунтов тут.
                    • +4
                      Спонсор? Это даже лучше, чем разработчик! :-). Обсудим (в почте) вопрос внедрения PVS-Studio?
            • –3
              Есть ли новости о портировании cppcat под *nix? Планируете ли этим заниматься?
              • 0
                Кстати, интересно — а ядро Линукса вы проверяли?
                • +2
                  На данный момент мы проверили и написали про: "Open-source проекты, которые мы проверили с помощью PVS-Studio".

                  Ядро Линукс не проверяли, но когда ни будь проверим. Но не думаю, что из этого что-то интересное получится. Его чем только не проверяли.

                  Я уверен, можно было бы найти много забавного, если ежедневно выкачивать исходники Linux, проверять и смотреть сообщения, относящиеся к свежему коду. Думаю, за пару месяцев было бы немало интересного насобирать. Но это достаточно сложная в реализации задача, которая нам не интересна.
                • 0
                  Андрей, спасибо за статью. Ждем новых проверок — PostgreSQL и других :)
                • 0
                  А разве сдвиг влево знакового числа не определен? Мне казалось, что это только про сдвиг вправо.
                  • +1
                    Знакового — определён. Отрицательного — нет. (~0) — имеет тип int и равно -1.

                    The shift operators <> group left-to-right.

                    shift-expression << additive-expression

                    shift-expression >> additive-expression

                    The operands shall be of integral or unscoped enumeration type and integral promotions are performed.

                    1. The type of the result is that of the promoted left operand. The behavior is undefined if the right operand is negative, or greater than or equal to the length in bits of the promoted left operand.

                    2. The value of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1 * 2^E2, reduced modulo one more than the maximum value representable in the result type. Otherwise, if E1 has a signed type and non-negative value, and E1*2^E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

                    3. The value of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a non-negative value, the value of the result is the integral part of the quotient of E1/2^E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.
                  • +3
                    Есть мнение, что вектор движения на логотипе предпочтительнее направлять направо-вверх. Так конверсия будет ещё больше. :)
                    image
                    • –11
                      А assert не пробывали+логика Хоара. Я понимаю, отрасль перегрета, полно идиотов, которые считают себя программистам,.но на них делать, деньги.

                      Стыдно товарищи! + агрессивная реклама. Еще раз стыдно.
                      • –4
                        Стыдно? Неужели?
                        • +5
                          Мы гордимся тем, что делаем. Не понятно, почему нам должно быть стыдно. Это Ваши фантазии (зависть) и не более того.

                          Мы делаем самую замечательную и полезную рекламу, которая может быть. Мы существенно помогаем миру open-source программ. Многие разработчики этих проектов благодарны нам.

                          И мир это ценит. Наши клиенты не «идиоты», а как раз наоборот компании, которые понимают всю важность качества кода, и сколько стоят ошибки: наши клиенты. Мы прилучаем от них много благодарственных слов. Многие компании продлевают лицензию уже не один год.

                          Недавно мы пошли навстречу и небольшим компаниям, выпустив лёгкий вариант анализатора за $250 — CppCat. Теперь со временем нас полюбят и маленькие команды и индивидуальные разработчики.

                          Продолжать дискуссию с Вами считаю нецелесообразным.
                          • –4
                            «Мы делаем самую замечательную и полезную рекламу, которая может быть.»

                            Вы уже думаете на английском языке. И она рассчитана на идиотов.

                            Основная идея Хоара дать для каждой конструкции императивного языка пред и постусловие записанное в виде логической формулы. Поэтому и возникает в названии тройка — предусловие, конструкция языка, постусловие.
                            Ясно, что для пустого оператора пред и постусловия совпадают.
                            Для оператора присваивания в постусловие кроме предусловия должно учитывать факт, что значение переменной стало другим.
                            Для составного оператора (в Python это отступы, в C это {}) имеем цепочку пред и постусловий. В результате для составного оператора можно оставить первое предусловие и последнее постусловие.
                            Правило вывода говорит, что можно усилить пред и ослабить постусловие если нам это понадобиться. Нет смысла волочь через всю программу какое-то утверждение, которое не помогает решить поставленную задачу.
                            Оператор ветвления или просто if. Его условно можно разбить на две ветки then и else. Если к предусловию добавить истинность логического условия (то, что стоит под if), то после выполнения ветки then должно следовать постусловие. Аналогично, если к предусловию добавить отрицание логического условия (то, что стоит под if), то после выполнения ветки else должно следовать постусловие
                            Оператор цикла. Это самое нетривиальное и сложное, поскольку цикл может выполняется много раз и даже не окончится. Чтобы решить проблему возможно многократного повтора тела цикла вводят инвариант цикла. Инвариант цикла это то, что истинно перед его выполнением, истинно после каждого выполнения тела цикла и следовательно истинно и после его окончания. Предусловие для оператора цикла это просто его инвариант цикла. Если истинно условие продолжения цикла (то, что стоит под while), то после выполнения тела цикла должна следовать истинность инвариант цикла. В результате после окончания цикла имеем в качестве постусловия истинность инвариант цикла и отрицание условия продолжения цикла.
                            Оператора цикла с полной корректностью. Для этого к предыдущему пункту добавляют ограничивающую функцию, с помощью которой легко доказать, что цикл будет выполнятся ограниченное число раз. На нее накладывают условия, что она всегда >=0, строго убывает после каждого выполнения тела цикла и в точности =0, когда цикл заканчивается.

                            Хоар фактически предложил:
                            Давайте воспользуемся произволом при написании программ и будем их писать так, чтобы легче было доказать их корректность. В результате и программу легче написать, и доказательство корректности сразу получим.
                            Это мои слова.

                            По поводу доказательства правильности алгоритма. Есть в математике набор инструментов позволяющий доказывать теоремы. Я назову общеизвестные: метод математической индукции, от противного. Так вот логика Хоара это инструмент доказательства корректности программ (правильнее частичной корректности, но это уже нюансы), который в принципе может быть использован и для доказательства корректности алгоритма, если он записан в основных конструкциях императивного программирования, для которых есть Тройки Хоара.

                            Это мое и взято отсюда freehabr.ru/blog/programming/1934.html

                            Мне за державу обидно, потому и на Вас наехал. И по Вашим комментариям, похоже правильно.
                      • 0
                        А SQLite не планируете проверить? Очень интересно, помогает ли полное покрытие тестами избавиться от различных ошибок :)
                        • 0
                          Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.

                          Edsger W. Dijkstra, 1970

                          Впрочем согласен, SQLite тестируется как ничто другое, интересно поможет ли инструмент.
                          • 0
                            дело в том, что тесты позволяют найти часть ошибок во время тестирования. но это требует намного больше времени, чем просто поймать ошибку сразу, еще до тестирования…

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

                        Самое читаемое
                        Интересные публикации