Search Unity

IL2CPP: ИНФРАСТРУКТУРЫ ТЕСТИРОВАНИЯ

, 20 июля, 2015

Это восьмая и последняя статья в серии материалов о IL2CPP. В предыдущих постах мы раскрыли различные аспекты работы IL2CPP во время сборки и выполнения приложений. В этот раз мы коснемся процесса разработки и тестирования этой технологии.

Разработка через тестирование

Команда IL2CPP придерживается практики разработки через тестирование (TDD). Любой код, присылаемый в запросах на включение, подвергается всесторонней проверке.

Так как список возможных входных данных для IL2CPP не бесконечен, хотя и велик — в соответствии с ECMA 335 — процесс разработки хорошо укладывается в концепцию TDD. Большинство тестов написано ещё до создания кода, который будет тестироваться; сам процесс написания кода сводится к тому, чтобы удовлетворить тесты. Считается, что предварительные версии кода должны проваливать тесты ожидаемым образом.

Побочный эффект такого метода разработки — наличие отлаженного набора тестов, позволяющего проверить практически все аспекты работы IL2CPP. Он дает два важных преимущества разработчикам:

  1. Уверенность: Если изменение, внесенное в код IL2CPP, проходит тест, можно с большой уверенностью сказать, что в нём нет регрессии.
  2. Быстрое решение проблем: Так как основной код IL2CPP работает так, как ожидается, ошибки чаще всего содержатся в новых включениях или же в ещё не рассмотренных случаях. Таким образом, пространство для поиска причины ошибки сужается.

Статистика тестирования

Множество тестов, проводимых над кодом IL2CPP, можно разделить на несколько уровней:

  • Модульные тесты:
    • C#: 472
    • C++: 44
  • Интеграционные тесты:
    • C#: 1735
    • IL: 173

Если все тесты пройдены, то испытуемая версия IL2CPP готова к выпуску. Версии из нашей основной ветви, которые работают с новейшими версиями Unity, всегда проходят все тесты; если же очередной выпуск в этой ветви содержит серьезную ошибку, исправления обычно появляются за несколько минут.

Разработчики часто создают собственные ответвления от основной ветви для внесения изменений, поэтому все выпуски в основной ветви должны быть проверены тестами для предотвращения накопления ошибок. Управление версиями в главной ветви и ответвлениях осуществляется с помощью системы Katana — собственной разработки Unity.

Тесты выполняются с помощью NUnit. NUnit управляется тремя способами:

  • Для Windows: ReSharper
  • Для OSX: Xamarin Studio
  • Специальный скрипт Perl для командной строки Windows и OSX

Виды тестов

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

Модульные тесты проверяют небольшой фрагмент кода, часто — отдельный метод. В таких тестах код исполняется с различными вводными данными, и результаты сравниваются с предположениями об ожидаемом поведении.

Интеграционные тесты используют il2cpp.exe для получения кода C++ из трансляции, компилируют полученный код и запускают собранный исполняемый файл. Так как поведение IL2CPP хорошо изучено (используется версия Mono, включенная в Unity), мы также исполняем трансляцию с помощью Mono (на Windows, а также с помощью .NET). Полученные результаты затем сравниваются. В отличие от модульных тестов, ожидаемые значения не используются.

Модульные тесты для C#

Наиболее быстрые тесты низкого уровня. Используются для проверки поведения различных частей AOT-утилиты для IL2CPP, il2cpp.exe, написанной полностью на C#. Обычно такие тесты занимают несколько секунд на качественной системе.

Модульные тесты для C++

Большая часть исполняемого кода для IL2CPP (libil2cpp) создается на C++. Модульные тесты используются для проверки тех фрагментов кода, которые недоступны для публичных интерфейсов. Однако большая их часть охватывается интеграционными тестами, поэтому модульных тестов для C++ относительно немного. Они занимают больше времени, чем для C#, так как для них нужен запуск il2cpp.exe.

Интеграционные тесты для C#

Крупнейший набор тестов для IL2CPP, разделенный на несколько групп, проверяющих различные аспекты поведения IL2CPP: использование icall, генерацию кода, p/Invoke и другие. Большинство интеграционных тестов для С# невелики и состоят из 5–10 строк; весь набор выполняется на большинстве компьютеров приблизительно за минуту. Доступны различные дополнительные возможности.

Интеграционные тесты для IL

Отличаются от тестов для C# использованием класса ILGenerator для создания трансляции. Этот вид тестов обладает большой гибкостью, хотя их написание требует больше времени, чем для C#. Часто возникают ситуации, когда фрагмент IL-кода не собирается или несовместим с используемой версией компилятора Mono C#: тогда зачастую можно создать тестовый случай с помощью IL-кода. Кроме того, интеграционные тесты могут использоваться для глубокого тестирования опкодов, у которых имеется четкая основная линия поведения с большим количеством нюансов (например, conv.i и другие опкоды его семейства). Весь набор интеграционных тестов для IL выполняется менее чем за минуту.

Katana выполняет полный набор тестов с различными вариациями приблизительно за 20–30 минут в зависимости от занятости оборудования. Это происходит каждый раз после включения нового кода.

Зачем так много интеграционных тестов?

Может показаться, что пирамида тестов для IL2CPP перевернута. Действительно, интеграционные тесты (вершина пирамиды) находятся у нас в большинстве.

Следование практике TDD с использованием тестов, продолжающихся дольше чем несколько секунд, может быть затруднительно. Поэтому мы используем отдельные сегменты набора интеграционных тестов и осуществляем пошаговую сборку кода C++, сгенерированного тестами. В таком случае время отдельного теста оказывается приемлемым, хотя и не слишком быстрым.

Мы сознательно приняли решение о расширенном использовании интеграционных тестов. Большая часть кода IL2CPP подверглась изменениям — даже по сравнению с версиями первого общедоступного выпуска января 2015 года. При этом мы все ещё используем множество тестов, созданных ранее. Проведя большое количество экспериментов с тестами различных уровней (включая проверку сгенерированного исходного кода на C++), мы заключили, что именно интеграционные тесты дают лучшее соотношение затраченного времени к стабильности теста. Нам крайне редко приходится что-то изменять в наборе интеграционных тестов после изменений в коде IL2CPP: настолько редко, что можно с уверенностью утверждать, что новый код, после включения которого перестал выполняться интеграционный тест, содержит ошибку. Это позволяет нам улучшать код IL2CPP и выполнять рефакторинг без опаски случайно испортить тесты.

Тесты большого масштаба

Код IL2CPP включается в масштабную экосистему тестирования Unity. Для каждой платформы, поддерживающей IL2CPP, проводятся тесты среды выполнения Unity. Они собраны в единый проект, включающий в себя более 1 тыс. сцен, в каждой из которых поведение кода проверяется с помощью ожидаемых значений. Эти тесты служат защитой от регрессий, которые мы могли пропустить в новую версию IL2CPP, а также помогают нам проверять код, используемый для интеграции IL2CPP в наборы инструментов Unity для каждой платформы. Полный набор выполняется около 60–90 минут, хотя мы часто используем только отдельные компоненты.

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

Влияние практики TDD

Один из принципов Unity — «решай сложные задачи». Мы думаем, что сложность можно измерить количеством неудачных попыток. Чем больше их требуется совершить в процессе поиска решения, тем сложнее задача.

Создание нового, высокопроизводительного, масштабируемого AOT-компилятора и виртуальной машины для использования в качестве серверного приложения Unity — сложная задача, и мы долго искали правильное решение. Таких задач будет ещё много, но теперь, когда у нас есть исчерпывающий набор тестов, собирающий множество полезной информации о каждой неудачной попытке, мы можем работать быстрее.

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

Заключение

Мы были рады поделиться подробностями реализации IL2CPP и дать, как мы надеемся, полезные советы по оптимизации и поиску ошибок. Пишите нам, если вы хотите видеть больше статей по темам, связанным с IL2CPP.

7 replies on “IL2CPP: ИНФРАСТРУКТУРЫ ТЕСТИРОВАНИЯ”

Is it safe to assume that you also test the more esoteric features of C#, e.g. ‘stackalloc’? (We had a couple of mysterious crashes in 5.1 that decidedly was ‘definitely maybe’ due to stackalloc usage in our game code. Will try again with 5.2 soon.)

For bugs that occur despite passing the test suite — do you add a test that reproduces the bug before fixing it? Also, how often do you find the unit tests failing after making code changes? Is there a particular area that seems to be more sensitive (changes result in test failures) more than the others? (just curious)

«The more difficult a problem is to solve, the more failures I need accomplish…»

<..>

… like Unity’s serialization. :p

Comments are closed.