>
>
>
Проблемы тестирования 64-битных приложе…

Андрей Карпов
Статей: 671

Евгений Рыжков
Статей: 125

Проблемы тестирования 64-битных приложений

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

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

Очередным этапом повышения вычислительных мощностей стал переход на 64-битные микропроцессорные системы. Этот шаг нельзя назвать революционным, но он позволяет существенно расширить возможности компьютерных систем. В первую очередь 64-битные системы позволили преодолеть барьер в 4GB, который уже начал ограничивать многих разработчиков программного обеспечения. В первую очередь это касается разработчиков пакетов численного моделирования, трехмерных редакторов, СУБД, игр. Большой объем оперативной памяти существенно расширяет возможности приложений, позволяя хранить большие объемы данных и обращаться к ним напрямую, не подгружая их из внешних хранилищ данных. Не стоит забывать и про более высокую производительность 64-битных версий программ, обусловленную большим количеством регистров, расширенными возможностями плавающей арифметики, возможностью работы с 64-битными числами.

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

Самым старым, проверенным и надежным подходом к поиску дефектов является совместный просмотр кода (англ. code review) [1]. Эта методика основана на совместном чтении кода с выполнением ряда правил и рекомендаций, хорошо описанным, например, в книге Стива Макконнелла "Совершенный код" [2]. К сожалению, эта практика неприменима для крупномасштабной проверки современных программных систем в силу их большого объема. Хотя этот способ дает наилучшие результаты, он не всегда используется в условиях современных жизненных циклов разработки программного обеспечения, где немаловажным моментом является срок разработки и время выхода продукта на рынок. Поэтому просмотр кода чаще всего сводится к нечастым встречам, целью которых ставится обучение новых и мене опытных сотрудников написанию качественного кода, нежели чем проверка работоспособности ряда модулей. Это очень хороший способ повышения квалификации программистов, но его нельзя рассматривать как полноценное средство контроля качества разрабатываемой программы.

На помощь разработчикам, которые осознают необходимость регулярного просмотра кода, но не имеют достаточного количества времени, приходят средства статического анализа кода. Их основной задачей является сокращение объема кода, требующего внимания человека, и тем самым сокращение времени его просмотра. К статическим анализаторам кода относится достаточно большой класс программ, реализованных для различных языков программирования и имеющих разнообразный набор функций, от простейшего контроля выравнивания кода, до сложного анализа потенциально опасных мест. Систематизированное использование статических анализаторов позволяет существенно повысить качество кода и найти многие ошибки. У подхода, основанного на статическом анализе, много поклонников и ему посвящено много интересных работ (например [3], [4]). Преимуществом данного подхода является то, что он не зависит от сложности и размера разрабатываемого программного решения.

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

Метод белого ящика [5]. Под тестированием методом белого ящика будем понимать выполнение максимально доступного количества различных веток кода с использованием отладчика или иных средств. Чем большее покрытие кода было достигнуто, тем более полно выполнено тестирование. Под тестированием по методу белого ящика также иногда понимают простую отладку приложения с целью поиска известной ошибки. Полноценное тестирование методом белого ящика всего кода программ уже давно стало невозможным в силу огромного объема кода современных программ. Сейчас тестирование по методу белого ящика удобно применять на этапе, когда ошибка найдена и необходимо понять причину ее возникновения. У тестирования методом белого ящика существуют оппоненты, отрицающие полезность отладки программ в реальном времени. Основной мотив заключается в том, что возможность наблюдать ход работы программы и при этом вносить изменения в ее состояние порождает недопустимый подход в программировании, основанный на большом количестве исправлений кода методом проб и ошибок. Мы не будем касаться данных споров, но заметим, что тестирование по методу белого ящика в любом случае очень дорогой способ повышения качества больших и сложных программных систем.

Намного лучше себя зарекомендовал метод черного ящика [6]. Сюда же можно отнести юнит-тестирование (англ. unit test) [7]. Основная идея метода заключается в написании набора тестов для отдельных модулей и функций, проверяющего все основные режимы их работы. Ряд источников относят юнит-тестирование к методу белого ящика, поскольку оно основывается на знании устройства программы. Автор придерживается позиции, что тестируемые функции и модули следует рассматривать как черные ящики, так как юнит-тесты не должны учитывать внутреннее устройство функции. Обоснованием этому может служить такая методология, когда тесты разрабатываются до начала написания самих функций, что способствует повышению контроля их функциональности с точки зрения спецификации.

Методу юнит-тестирования посвящено много литературы, например [8]. Юнит-тестирование хорошо зарекомендовало себя как при разработке простых, так и сложных проектов. Одним из преимуществ юнит-тестирования является то, что легко можно проверить корректность вносимых в программу исправлений прямо в ходе разработки. Стараются делать так, чтобы все тесты проходили в течение нескольких минут, что позволяет разработчику, который внес изменения в код, сразу заметить ошибку и исправить ее. Если прогон всех тестов невозможен, то обычно длительные тесты выносят отдельно и запускают, например, ночью. Это также способствует оперативному обнаружению ошибок, по крайней мере, на следующее утро.

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

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

Теперь перейдем к вопросу тестирования 64-битных программ, так как применение выбранных нами методик сталкивается с несколькими неприятными затруднениями. Начнем со статических анализаторов кода.

Как это ни странно, несмотря на все свои огромные возможности, длительный период разработки и практику использования, статические анализаторы оказались плохо готовы к поиску ошибок в 64-битных программах. Рассмотрим ситуацию на примере анализа Си++ кода как область, где статические анализаторы нашли наибольшее применение. Многие статические анализаторы поддерживают ряд правил, связанных с поиском кода, имеющего некорректное поведение при переносе его на 64-битные системы. Но реализуют они это весьма разрозненными методами и весьма неполно. Особенно хорошо это проявилось после начала массовой разработки приложений под 64-битную версию ОС Windows в среде Microsoft Visual C++ 2005.

Объяснением этому может служить тот факт, что большинство проверок основано на достаточно старых материалах по исследованию проблем переноса программ на 64-битные системы с точки зрения языка Си. В результате ряд конструкций, появившихся в языке Си++, был обделен вниманием с точки зрения контроля переносимости и не нашел своего отражения в анализаторах. Не учтен и ряд других изменений, таких как, например, существенно возросший объем оперативной памяти и использование в разных компиляторах различных моделей данных (LP64, LLP64, ILP64 [9]).

Для наглядности рассмотрим пару примеров:

double *DoubleArray;
unsigned Index = 0;
while (...)
  DoubleArray[Index++] = 1.0f;

Получить предупреждение на подобный код Вам не удастся даже такими мощными анализаторами, как Parasoft C++test (http://www.parasoft.com) и Gimpel Software PC-Lint (http://www.gimpel.com). Это не удивительно. Приведенный код не вызывает никаких подозрений у рядового разработчика, привыкшего к практике использования в качестве индексов переменных типа int или unsigned. К сожалению, приведенный код на 64-битной системе будет неработоспособен, если объем обрабатываемого массива DoubleArray превысит размер в 4Gb элементов. В этом случае произойдет переполнение переменной Index, и результат работы программы будет некорректен. Корректным вариантом будет использование типа size_t при программировании под Windows x64 (модель данных LLP64) или size_t/unsigned long при программировании под Linux (модель данных LP64).

Причина, по которой статические анализаторы не могут диагностировать подобный код, пожалуй, скрывается в том, что когда исследовались вопросы переноса под 64-битные системы, то вряд ли кто-то представлял себе массивы более чем из 4 миллиардов элементов. А 4 миллиарда элементов типа double это 4 * 8 = 32 гигабайт памяти для одного массива. Огромный объем, тем более если учесть, что это 1993-1995 год. Именно на это время приходится большинство публикаций и обсуждений, посвященных использованию 64-битных систем.

В результате на возможную некорректную индексацию при использовании типа int никто не обратил внимания, а в дальнейшем вопросы переноса исследовались достаточно редко. И практически ни один статический анализатор не выдаст предупреждение на приведенный код. Исключением может служить, пожалуй, только анализатор Viva64 (http://www.viva64.com). Он был разработан для того, чтобы закрыть пробелы в диагностике 64-битного Си/Си++ кода другими анализаторами, и основан на вновь проведенных исследованиях. Но у него есть существенный недостаток, заключающийся в том, что это не анализатор общего назначения. Он специализируется только на анализе ошибок, возникающих при переносе кода на 64-битные системы Windows, и поэтому должен быть использован только в сочетании с другими анализаторами, чтобы обеспечить надлежащее качество кода.

Рассмотрим другой пример:

char *p;
long g=(long)p;

С помощью этого простого примера можно проверить, какие модели данных умеет понимать используемый вами статический анализатор. Проблема состоит в том, что большинство из них рассчитаны только на модель данных LP64. Это вновь вызвано историей развития 64-битных систем. Именно модель данных LP64 на начальных этапах развития 64-битных систем получила наибольшую популярность и сейчас широко используется в Unix-мире. В этой модели данных тип long имеет размер 8 байт, а, значит, такой код полностью корректен. Но в 64-битных системах Windows реализована модель данных LLP64, где размер long остался 4-байтовым и, следовательно, приведенный код будет некорректен. В Windows следовало бы, например, использовать тип LONG_PTR или ptrdiff_t.

К счастью, приведенный код будет диагностироваться как опасный как самим компилятором Microsoft Visual C++ 2005, так и анализатором Viva64. Но всегда следует помнить о подобных подводных камнях при использовании статических анализаторов.

Получилась интересная ситуация. Вопрос переноса программ на 64-битные системы был подробно обсужден, были реализованы различные методики и правила проверки в статических анализаторах, после чего интерес к этой тематике угас. Прошло много лет, многое изменилось, но правила, по которым осуществляется анализ, остаются без изменений и модификаций. Чем это вызвано - объяснить сложно. Возможно, разработчики просто не замечают изменений, считая, что вопрос тестирования и проверки 64-битных приложений давно решен. Хочется, чтобы Вы не попали в ту же ловушку. Будьте аккуратны. То, что было актуально 10 лет назад, сейчас может таковым не является, но зато появилось много нового. Используя средства статического анализа, убедитесь, что они совместимы с используемой Вами 64-битной моделью данных. Если анализатор не удовлетворяет необходимым условиям, не поленитесь поискать другой или восполнить пробел, используя узконаправленный анализатор Viva64. Усилия, потраченные на это, с лихвой окупятся повышением надежности вашей программы, уменьшением сроков отладки и тестирования.

Теперь поговорим о юнит-тестах. C ними на 64-битных системах нас также ожидает ряд неприятных моментов. Стремясь сократить время выполнения тестов, при их разработке стараются использовать небольшой объем вычислений и объем обрабатываемых данных. Например, разрабатывая тест на функцию поиска элемента в массиве, не имеет большого значения, будет она обрабатывать 100 элементов или 10.000.000. Ста элементов будет достаточно, а вот по сравнению с обработкой 10.000.000 элементов скорость выполнения теста может быть существенно выше. Но если Вы хотите разработать полноценные тесты, чтобы проверить эту функцию на 64-битной системе, вам потребуется обработать более 4 миллиардов элементов! Вам кажется, что если функция работает на 100 элементах, она будет работать и на миллиардах? Нет. Если не верите, то для демонстрации попробуйте приведенный пример на 64-битной системе:

bool FooFind(char *Array, char Value,
             size_t Size)
{
  for (unsigned i = 0; i != Size; ++i)
    if (i % 5 == 0 && Array[i] == Value)
      return true;
  return false;
}
#ifdef _WIN64
  const size_t BufSize = 5368709120ui64;
#else
  const size_t BufSize = 5242880;
#endif
int _tmain(int, _TCHAR *) {
  char *Array =
    (char *)calloc(BufSize, sizeof(char));
  if (Array == NULL)
    std::cout << "Error allocate memory" << std::endl;
  if (FooFind(Array, 33, BufSize))
    std::cout << "Find" << std::endl;
  free(Array);
}

Как видно из примера, если Ваша программа на 64-битной системе начнет обрабатывать больший объем данных, то не стоит рассчитывать на старые наборы юнит-тестов. Следует их обязательно расширить с учетом обработки больших объемов данных.

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

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

Естественно, Вам потребуется воспользоваться системой автоматизированного тестирования, которая позволит организовать запуск тестов на нескольких машинах. Примером может служить система автоматизации тестирования Windows-приложений AutomatedQA TestComplete (http://www.automatedqa.com). С ее помощью можно выполнять распределенное тестирование приложений на нескольких рабочих станциях, осуществлять синхронизацию и сбор результатов [10].

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

Подводя итоги, хочется сказать, что не стоит полагаться на отдельную методику. Качественное приложение может быть разработано только с применением нескольких из рассмотренных подходов к тестированию.

Резюмируя проблемы разработки 64-битных систем, хочется еще раз напомнить Вам ключевые моменты:

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

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

Библиографический список