Хочу поделиться опытом в настройке системы непрерывной интеграции для проекта Windows Phone 7 в Team City. Надеюсь, сэкономлю тем, кто пойдёт той же тропой, потраченные мной самим время и нервы.
Дано:
- Довольно-таки массивное приложение Windows Phone 7 c unit-тестами, реализованными средствами Silverlight Toolkit.
- Настроенная сборка приложения в TeamCity без запуска unit-тестов. Агент для сборки — «физическая» (в смысле, не виртуальная) машина.
Необходимо:
- Настроить ещё одного build-агента TeamCity на виртуальной машине под VMWare.
- Запускать unit-тесты при сборках и сбора результатов их выполнения в статистику TeamCity.
Настройка билд-агента
В качестве билд-агента используется виртуальная машина с Windows Server 2008 R2. Настройка поначалу казалось простой — ставим Visual Studio, Windows Phone SDK и самого агента (агент скачивается прямо с сайта развёрнутого TeamCity). Тестовый проект без Silverlight «взлетел» без проблем — сразу запутились unit-тесты, появился контроль покрытия кода с использованием встроенного в TeamCity dotCover:
Приятно было получить такой результат за 5 минут с нуля!
Запуск эмулятора
Загоревшись, я взялся за запуск сборки Windows Phone. Здесь и началось самое интересное.
Нужно сказать, что в TeamCity у нас уже была настроена сборка, в процессе которой нужно было выполнять определённый код подготовки данных. Этот код использовался в «живом» приложении, и его поленились отрезать от Silverlight Runtime. Поэтому при сборке код выполнялся в эмуляторе, вызываемом агентом TeamCity. Такая схема работала на имеющемся билд-агенте.
Однако настроить аналогичную сборку на новом билд-агенте удалось только через пару дней, наполненных в основном руганью. Вот основные косяки, которые я поймал:
- Windows Phone SDK не устанавливался — жаловался, что ему нужна Windows 7, не признавая серверную ОС. Выручили гугл и MSDN. Поскольку у меня был полностью скачанный SDK, после редактирования baseline.dat по инструкциям я запускал setup.exe без ключей, при этом всё установилось как надо.
- Эмулятор не запускался, поскольку не был установлен Windows Media Player. Установка его под Windows Server 2008 тоже оказалась не вполне интуитивной.
- Говорят, что эмулятор может не запускаться без определённых флажков и ручной (!) правки конфигов виртуальной машины, но меня сие миновало. Возможно потому, что мне нужен был эмулятор Windows Phone 7, а танцы с бубном описаны для эмулятора Windows Phone 8. Кроме того, админ заверял, что в настройках виртуалки «всё включено».
- Эмулятор не запускался, пока объём ОЗУ виртуальной машины не увеличили с 1 ГБ до 2 ГБ. При этом не запускался молча — ни намёка на причину!
Кстати, через ярлык, создаваемый при установке SDK, эмулятор у меня так и не запускается. Не стал разбираться почему, поскольку при вызове его из утилиты управления он стартует нормально.
Самая большая проблема всплыла в конце, когда эмулятор наконец-то запустился. Эмулятор при запуске предупреждал, что для нормальной работы ему нужна нормальная видеокарта, которую «пробросить» в виртуальную машину не получилось. Несмотря на то, что после такого грозного предупреждения эмулятор нормально работал, кнопку ОК нужно было кому-то нажать. Предполагалось, что эмулятор будет запускаться службой агента TeamCity и не будет появляться на экране, поэтому кнопку было нажимать некому.
В отчаянной попытке я пытался кодом нажать эту злосчастную кнопку, но это не сработало при запуске эмулятора из службы агента.
Единственное, что я смог в итоге придумать — запускать билд-агента не как службу, а как приложение. В этом случае эмулятор запускался нормально, и на кнопку можно было нажать вручную (это достаточно делать только после перезагрузки сервера — будучи однажды запущенным, эмулятор используется для всех последующих сборок). Тем не менее я оставил код автоматического нажатия на кнопку, чтобы перезагрузка вирутальной машины была «необслуживаемой».
Запуск агента как приложения оказался несложным — в каталоге bin агента (c:BuildAgentbin по умолчанию) есть хороший набор bat-файлов, которыми можно снести ненужную уже службу агента, а также запустить агента как приложение (здесь есть на эту тему).
Итак, прописываем нужный bat-файл в автозапуск, настраиваем автоматический вход после запуска системы (для «необслуживаемой» перезагрузки) и вуаля! Сборка работает!
Последними штрихами явились запуск эмулятора и блокировка рабочей станции в скрипте запуска агента. Запуск эмулятора был нужен, чтобы исключить задержку в первой сборке, выполняемой после перезагрузки виртуальной машины, а блокировка — потому что админ попросил («негоже, когда консоль машины открыта!»).
Следующим этапом стало добавление в скрипт сборки запуска unit-тестов. Тесты, как я уже говорил, запускались под Unit Test Framework, идущем в комплекте с Silverlight Toolkit. Запуск на эмуляторе был уже пройденным этапом, поэтому тесты завелись без проблем:
Передача результатов тестов в TeamCity
Далее логично захотелось, чтобы тесты не просто запускались, а «валили» сборку при непрохождении. Для этого нужно было передать в TeamCity результаты выполнения тестов.
Очередное отступление — мы управляли эмулятором из сборочных скриптов через CoreCon API, за что спасибо Justin Angel и arty87. То есть, в скрипте сборки запускалось консольное приложение, которое собственно и управляло эмулятором.
Первое решение, которое пришло в голову и сразу было опробовано — «повалить» сборку при поваленном тесте, выйдя из приложения управления эмулятором с ненулевым exit code. При этом можно было ещё написать что-то в консоль — потом это можно будет увидеть в build log-е TeamCity.
Но хотелось красоты, чтобы TeamCity вел статистику тестов, да ещё и по ходу сборки было видно, как они выполняются (очень медитативно, между прочим). Поэтому было раскопано Service Message API, которое по факту оказалось «отловом» специального вида тегов из консольного вывода. Тут всплыло две особенности:
- Теги должны быть однострочными. TeamCity воспринимает разрыв строки как конец тега, соответственно тег оказывается неверного формата и игнорируется. В документации в примере показана передача символов перевода строк последовательностью "|n|r", но у меня это не заработало.
- Теги не должны быть слишком длинными. Попытка вырезать переводы строк из стектрейса и передать его как details в теге testFailed привела к тому, что TeamCity самовольно разбил получившуюся длинную строку где-то на 300 символах со всеми вытекающими. Интересно, что остаток строки длиной явно больше 300 символов показывался в Build Log без всяких разбиений. Детально я не экспериментировал и решил стектрейс ошибок выводить просто в Build Log
Осталось решить две задачи: отловить факт запуска и завершения тестов и передать это всё из кода, выполняемого в эмуляторе, в утилиту управления эмулятором (в консоль ведь должна писать утилита, а не эмулятор, на котором запускаются тесты).
Первая задача решилась просто. В фреймворке для Unit-тестов есть возможность подписаться на события запуска и завершения тестов, тестовых классов и целых сборок, в обработчиках которых мы можем узнать много информации (результат теста, Exception при сбое теста, время начала и завершения и т.п.). Документации по фреймворку я особо не искал, проще оказалось изучить «методом тыка».
Итак, для запуска unit-тестов с выводом результата мы немного модифицируем стандартный стартовый код:
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var testPage = UnitTestSystem.CreateTestPage(GetSettings()) as IMobileTestPage;
BackKeyPress += (x, xe) => xe.Cancel = testPage.NavigateBack();
(Application.Current.RootVisual as PhoneApplicationFrame).Content = testPage;
}
Модификация заключается в вызове функции GetSettings. Вот эта самая функция:
public static UnitTestSettings GetSettings()
{
var settings = UnitTestSystem.CreateDefaultSettings();
settings.TestHarness.TestClassStarting += TestHarnessTestClassStarting;
settings.TestHarness.TestClassCompleted += TestHarnessTestClassCompleted;
settings.TestHarness.TestMethodStarting += TestHarnessTestMethodStarting;
settings.TestHarness.TestMethodCompleted += TestHarnessTestMethodCompleted;
return settings;
}
Теперь мы пристально следим за нашими тестами. Код обработчиков событий тривиален, приводить его не буду.
Теперь нам нужно передать результаты слежки в программу управления эмулятором, где и вывести их в консоль в виде тегов Service Message API.
В качестве канала передачи использовался Isolated Storage, как собственно и было описано в статье. Правда, в отличие от предложенного FileDeployer для обмена файлами использовался класс RemoteIsolatedStorage.
Однако этот способ передачи оказался не очень хорош. Поскольку хотелось передачи информации о тестах в режиме реального времени, результаты тестов сразу записывались в файл Isolated Storage на эмуляторе, а утилита управления периодически вычитывала этот файл и выводила в консоль вновь полученные результаты. Из-за блокировок на файл запись периодически «падала», что никак не отражалось на выполняемых тестах, но приводило к потере информации о них. Решили проблему «костылём» — ловлей ошибок записи и повторами при ошибках. Конечно, по фен-шуй надо использовать более подходящий способ обмена данными с эмулятором, например через IP. Однако заморачиваться на этом уже не хотелось, поскольку долгожданный результат уже был получен:
Осталось нереализованным желание контролировать ещё и покрытие кода unit-тестами. Как оказалось, нормально контролировать покрытие кода в SIlverlight Runtime не получится. Большие люди советуют перекомпилировать тестируемый код в обычном .NET CLR для получения покрытия. Однако с имеющимся объемом тестов, порой весьма жестко привязанных к Silverlight Runtime, это посчитали делать нецелесообразным. Тем не менее мечта осталась, и я попробую её реализовать на начавшемся небольшом проекте. Надеюсь, всё получится и я смогу поделиться своей радостью.
P.S. Это мой первый пост, поэтому готов к конструктивной критике и предложениям.
Автор: lvr