Как убедить гейм-дизайнера запустить тесты?

    Полагаю, ни для кого не секрет, что в разработке игр участвует очень много специалистов, а не только программисты. Выпуск игры невозможен без художников, моделлеров, VFX-художников, и, конечно, гейм-дизайнеров. Кстати о последних. Мы их очень любим, но они часто ломают ресурсы. Не то чтобы они хотят это делать, но из-за особенностей работы им нужно делать много мелких правок, и шанс накосячить выше. И ведь множество ошибок — это тривиальные опечатки, недописанная или, наоборот, лишняя удалённая строка. Всё это можно исправить не отходя от кассы. Но как это сделать? Прописать в регламенте, что перед коммитом обязательно запустить %my_folder%/scripts/mega_checker? Мы проверяли — не работает. Человек — существо сложное и забывчивое. А проверять ресурсы хочется.

    Но мы нашли выход — теперь нельзя закоммитить в репозиторий без тестов. По крайней мере незаметно и безнаказанно.



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


    Первое, что нам нужно — это система тестирования. Мы её уже описывали здесь. Напомним, что нужен один и тот же код и для запуска на сервере Ci, и локально, чтобы не было сложности в поддержке. Желательно, чтобы на проекте могли задавать разнообразные параметры для общих тестов, а ещё лучше — расширять собственными. Конечно, конфетка сразу не получилась.

    Этап первый — запускать можно, но больно. Что делать с python-кодом ещё понятно, а вот со всевозможными утилитами вроде CppCheck, Bloaty, optipng, нашими внутренними костылями-велосипедами — нет. Для корректного запуска нужны исполняемые файлы для всех платформ, на которых работают наши коллеги (mac, windows и linux). На данном этапе все необходимые бинарные файлы находились в репозитории, а в настройках системы тестов указывался относительный путь к папке с бинарниками.

    <CppCheck bin_folder=”utils/cppcheck”>...</CppCheck>

    Это порождает несколько проблем:

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

    Этап второй — наводим порядок в утилитах. А что, если выписать все нужные утилиты и собрать их в одном хранилище? Идея в том, что на сервере находятся уже собранные утилиты для всех нужных платформ, которые ещё и версионируются. У нас уже использовался Nexus Sonatype, поэтому мы пошли в соседний отдел и договорились за файлики. В итоге получилась структура: 


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

    Опуская тонкости реализации
    def get_tools_info(project_tools_xml, available_tools_xml):
        # Parse available tools at first and feel up dictionary
        root = etree.parse(available_tools_xml).getroot()
        tools = {}
    
        # Parse xml and find current installed version ...
        return tools
    
    def update_tool(tool_info: ToolInfo):
        if tool_info.current_version == tool_info.needed_version:
            return
        if tool_info.needed_version not in tool_info.versions:
            raise RuntimeError(f'Tool "{tool_info.tool_id}" has no version "{tool_info.needed_version}"')
        if os.path.isdir(tool_info.output_folder):
            shutil.rmtree(tool_info.output_folder)
        g_server_interface.download(tool_id=tool_info.tool_id, version=tool_info.needed_version,
                                    output_folder=tool_info.output_folder)
    
    def run_tool(tool_info: ToolInfo, tool_args):
        system_name = platform.system().lower()
        tool_bin = tool_info.exe_infos[system_name].executable
        full_path = os.path.join(tool_info.output_folder, tool_bin)
        command = [full_path] + tool_args
        try:
            print(f'Run tool: "{tool_info.tool_id}" with commands: "{" ".join(tool_args)}"')
            output = subprocess.check_output(command)
            print(output)
        except Exception as e:
            print(f'Fail with: {e}')
            return 1
        return 0
    
    def run(project_tools_xml, available_tools_xml, tool_id, tool_args):
        tools = get_tools_info(project_tools_xml=project_tools_xml, available_tools_xml=available_tools_xml)
        update_tool(tools[tool_id])
        return run_tool(tool_info, tool_args)
    

    На сервере мы добавили файл с описанием утилит. Адрес этого файла неизменный, поэтому первым делом идём туда и смотрим, что у нас есть в наличии. Опуская тонкости, это имена пакетов и путь к исполняемому файлу внутри пакета для каждой платформы.

    xml «на сервере»
    <?xml version='1.0' encoding='utf-8'?>
    <Tools>
    	<CppCheck>
    		<windows executable="cppcheck.exe" />
    		<darwin executable="cppcheck" />
    		<linux executable="cppcheck" />
    	</CppCheck>
    </Tools>
    


    А на проекте добавляем файл с описанием того, что нужно.

    xml проекта
    
    <?xml version='1.0' encoding='utf-8'?>
    <Tools>
    	<CppCheck version="1.89" />
    </Tools>

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

    Запуск утилиты
    python -m utility_runner --available-source D:\Playrix\![habr]\gd_hooks\available_source.xml --project-tools D:\Playrix\![habr]\gd_hooks\project\project_tools.xml --run-tool CppCheck -- --version
    

    После данных манипуляций процесс работы стал выглядеть предельно прозрачно:

    • на проекте есть только один файл, где прописаны версии утилит, которые на данный момент актуальны
    • стало просто распространять обновления, и главное, что понятна текущая версия на проекте. А это правда сильно упрощает поиск проблемы.


    На сервере мы это относительно быстро подняли, но наша глобальная цель — запустить тесты у ГД.

    А запускать-то как?


    Вы когда-нибудь пробовали объяснить, как приготовить круассаны человеку, который никогда не готовил даже яичницы? Вот и нам сложно было объяснять гейм-дизайнерам, как запускать нужные скрипты. Если на сервере один раз настроил — и оно бодро выполняется, то с локальным запуском есть не так много вариантов. Точнее только один: хуки git.

    По-простому, хук — это bash-скрипт, который запускается в определённые моменты при работе с git: перед pull или push, во время создания коммита, есть хуки даже на git-сервере.

    Из всего разнообразия нас интересуют только три, которые запускаются во время создания коммита:

    • pre-commit — он выполняется первым и стоит на страже данных. Если код выхода отличный от нуля, то создание коммита прерывается.
    • prepare-commit-msg — он работает до вызова редактора сообщения коммита, но после создания стандартного сообщения. Нам он нужен, чтобы модифицировать сообщения коммитов слияния или rebase.
    • commit-msg — в этом хуке можно проверить сообщение к коммиту. Например, что разработчик не забыл добавить ссылку на задачу. Если он вернёт не ноль, то создание коммита прерывается.

    Чтобы хук начал действовать, его мало положить в репозиторий, как скрипт, его нужно скопировать в папку .git/hooks. Автоматически это сделать нельзя — эксплойт. Мы не выдумывали хитрых технологий, а сделали два командных файла (для Windows и Mac), которые копируют хуки из одной папки в другую и запускаются двойным кликом. Выполнить их нужно только один раз, и такое уже несложно объяснить человеку без технического образования.

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

    Магия у пользователя. Непонятная на первый взгляд ошибка, но стандартная проблема вроде нестандартных символов в путях, отсутствия git-bash на Windows. Для этих случаев мы пишем FAQ.

    Недавний случай
    Мы перебрали несколько предположений: нет прав на запись в папку, нет доступа на сервер, dns не резолвится. А оказалось, что curl не переваривает символы [ в пути.


    Тонкости работы систем. Предусмотреть все возможные варианты и параметры мы не смогли, поэтому периодически вылавливаем разнообразные баги. Мы или подпираем их в скрипте, или добавляем пункт в FAQ. Например, папка .git/hooks не всегда находится в корне репозитория. Чтобы узнать точное расположение, можно использовать команду:

    git rev-parse --git-path hooks
    

    В зависимости от того, в каком типе репозитория запускается команда, она вернёт следующее:

    Главная папка репозитория
    .git/hooks
    Worktree
    %repo_abs%/.git/hooks
    submodule
    %repo_abs%/.git/modules/hooks

    Другой интересный случай — это переключение между ветками во время разработки. Мы не очищали папку .git/hooks, и там могли оставаться старые хуки. Они пытались запуститься и падали. Это довольно сильно расстраивало пользователей, поэтому мы добавили в скрипт очистку .git/hooks перед тем, как начать копирование новых хуков.

    Всё сделать идеально с первого раза нельзя, поэтому просто необходима  возможность внести правки в репозитории, и чтобы они каким-либо образом автоматически подхватились у всех локально. Это сильно спасает, когда мы находим серьезный просчёт и не бегаем всем в личные сообщения с просьбой опять вызвать наш убер-скрипт. Вся эта работа должна быть максимально скрыта для разработчика — это не его война. Единственная сложность в том, что в момент выполнения хука мы не можем обновить сам файл хука — запись в него заблокирована системой. Одно из решений:

    1. При вызове  pre-commit обновить все файлы, кроме него самого. И создать pre-commit-tmp
    2. При вызове commit-msg заменить файл pre-commit на созданный в первом шаге pre-commit-tmp

    Вот, теперь хорошо: один раз скопировали хуки, и они будут запускаться во время коммита и автоматически обновляться. Мы выдохнули, но пользователи прислали нам скриншот.


    Причина ошибки
    Причина простая: сначала установили 32-битный питон в глобальном окружении; поняли свою ошибку, удалили и поставили 64-битный; pip install видит, что пакет уже установлен и ничего не делает. Но пакет-то для 32-битной версии — возникает конфликт.

    Но всё же, как запускать?


    Сначала мы сделали многостраничную инструкцию о том, какие круассаны вкуснее, какой python нужно установить. Но мы помним про гейм-дизайнеров и яичницу? Она всегда была подгоревшей: то python не той битности, то 2.7 вместо 3.7. И всё это множится ещё и на две платформы, где работают пользователи: windows и mac. (Пользователи Linux у нас либо гуру и сами всё настраивали, тихо притопывая под звуки бубна, либо их миновали проблемы.)

    Мы решили вопрос радикально — собрали python нужной версии и битности. А на вопрос «как нам его поставить и где хранить» ответили: Nexus! Единственная проблема: у нас ещё нет python, чтобы запустить python-скрипт, который мы сделали для запуска утилит из Nexus.

    И тут на помощь приходит bash! Он не такой уж и страшный, а даже хороший, когда к нему привыкнешь. И работает везде: на unix уже всё хорошо, а на Windows он ставится вместе с git-bash (это наше единственное требование к локальной системе). Алгоритм установки очень простой:

    1. Скачать архив собранного python для нужной платформы. Проще всего это сделать через curl — он есть почти везде (даже в Windows).

      mkdir -p "$PYTHON_PRIMARY_DIR"
      curl "$PYTHON_NEXUS_URL" --output "$PYTHON_PRIMARY_DIR/ci_python.zip" --insecure || exit 1

    2. Разархивировать его, создать виртуальное окружение, ссылающееся на скачанный бинарник. Не повторяйте наших ошибок: не забудьте прибить версию virtualenv.

      echo "Unzip python..."
      unzip "$PYTHON_PRIMARY_DIR/ci_python.zip" -d "$PYTHON_PRIMARY_DIR" > "unzip.log"
      rm -f "$PYTHON_PRIMARY_DIR/ci_python.zip"
      
      echo "Create virtual environment..."
      "$PYTHON_EXECUTABLE" -m pip install virtualenv==16.7.9 --disable-pip-version-check --no-warn-script-location
    3. Если нужны какие-то библиотеки из lib/*, нужно их скопировать самому. virtualenv об этом не задумывается.
    4. Установить все нужные пакеты. Тут мы договорились с проектами, что у них будет файл ci/required.txt, в котором будут все зависимости в формате pip.
      Установка зависимостей
      OUT_FILE="$VENV_DIR/pip_log.txt"
      "$PYTHON_VENV_EXECUTABLE" -m pip install -r "$REQUIRED_FILE" >> "$OUT_FILE" 2>&1
      result=$?
      if [[ "$result" != "0" ]]; then
      	var2=$(grep ERROR "$OUT_FILE")
      	echo "$(tput setaf 3)" "$var2" "$(tput sgr 0)"
      	echo -e "\e[1;31m" "Error while installing requirements. More details in: $OUT_FILE" "\e[0m"
      	result=$ERR_PIP
      fi
      exit $result
      

      Пример required.txt
      pywin32==225;sys_platform == "win32"
      cryptography==3.0.0
      google-api-python-client==1.7.11
      


    Когда обращаются с проблемой, то обычно прилагают скриншот консоли, где вывелись ошибки. Для облегчения себе работы мы не только храним вывод последнего запуска pip install, но и добавили красок в жизнь, выводя цветом ошибки из лога прямо в консоль. Да здравствует grep!

    Как это выглядит


    На первый взгляд может показаться, что нам не нужно виртуальное окружение. Ведь мы и так скачали отдельный бинарник, в отдельную директорию. Даже если есть несколько папок, где развёрнута наша система, то всё равно бинарники разные. Но! У virtualenv есть скрипт activate, который делает так, что можно вызывать python, как будто он в глобальном окружении. Это изолирует выполнение скриптов и упрощает запуск.

    Представьте: вам нужно запустить командный файл, из которого запускается python-скрипт, из которого запустится другой python-скрипт. Пример не выдуманный — так выполняются post-build события при сборке приложения. Без virtualenv пришлось бы везде вычислять на лету нужные пути, а с activate везде просто используем python. Точнее vpython — мы добавили свою обёртку, чтобы удобней запускать и из консоли, и из скриптов. В оболочке мы проверяем, находимся ли уже в активированном окружении или нет, запускаемся ли на TeamCity (где своё виртуальное окружение), а заодно подготавливаем окружение.

    vpython.cmd
    set CUR_DIR=%~dp0
    set "REPO_DIR=%CUR_DIR%\."
    
    rem VIRTUAL_ENV is the variable from activate.bat and is set automatically
    rem TEAMCITY - if we are running from agent we need no virtualenv activation
    if "%VIRTUAL_ENV%"=="" IF "%TEAMCITY%"=="" (
    	set RETURN=if_state
    	goto prepare
    	:if_state
    	if %ERRORLEVEL% neq 0 (
    		echo [31m Error while prepare environment. Run ci\PrepareAll.cmd via command line [0m
    		exit /b 1
    	)
    	call "%REPO_DIR%\.venv\Scripts\activate.bat"
    	rem special variable to check if venv activated from this script
    	set VENV_FROM_CURRENT=true
    )
    
    rem Run simple python and forward args to it
    python %*
    
    SET result=%errorlevel%
    
    if "%VENV_FROM_CURRENT%"=="true" (
    	call "%REPO_DIR%\.venv\Scripts\deactivate.bat"
    	set CI_VENV_RUN=
    	set VENV_FROM_CURRENT=
    )
    
    :eof
    exit /b %result%
    
    :prepare
    setlocal
    set RUN_FROM_SCRIPT=true
    call "%REPO_DIR%\ci\PrepareEnvironment.cmd" > NUL
    endlocal
    goto %RETURN%
    


    Танакан, или не надо забывать ставить тесты


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

    Когда наша система запустилась, она модифицирует комментарий к коммиту и ставит метку «одобрено». В качестве метки мы решили не мудрствовать и добавлять [+] или [-] в конце комментария к коммиту.

    На сервере крутится скрипт, который парсит сообщения, и если он не находит заветный набор символов — создаёт задачу на автора.Это самое простое и элегантное решение. Непечатаемые символы — не очевидно. Чтобы запускать серверные хуки, нужен другой тарифный план на Гитхабе, а покупать премиум для одной фичи никто не будет. Пробежаться по истории коммитов, поискать символ и поставить задачу — очевидно и не так дорого.

    Да, можно поставить символ и своими ручками, но вы уверены, что не сломаете сборку на сервере? А если сломаете… да-да, за вами уже едет лысый из Homescapes.

    Каков итог


    Отследить количество ошибок, которые нашли хуки, довольно сложно — они не попадают на сервер. Есть только субъективное мнение, что зелёных сборок стало гораздо больше. Однако есть и отрицательная сторона — коммит стал занимать довольно много времени. В некоторых случаях доходит до 10 минут, но это отдельная история про оптимизацию.
    Playrix
    Разработчик мобильных free-to-play игр

    Комментарии 6

      0
      Всё это можно исправить не отходя от кассы. Но как это сделать? Прописать в регламенте, что перед коммитом обязательно запустить %my_folder%/scripts/mega_checker? Мы проверяли — не работает. Человек — существо сложное и забывчивое. А проверять ресурсы хочется.
      Но мы нашли выход — теперь нельзя закоммитить в репозиторий без тестов. По крайней мере незаметно и безнаказанно.

      Я далек от геймдева, но в моей области(бэкенд разработка) в CI билд проекта, тесты и линтеры при мердже это стандарт, у меня на домашних проектах где только я коммичу это настроено.
      И пулл реквест без ревью хотя бы одного человека всегда запрещен.

        0

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


        А тесты локально, перед коммитом, спасают сильно: много ломающих изменений чинятся сразу, пока человек максимально в контексте текущей задачи.

        +1

        Привет. Правильно понимаю, что проверки выполняются локально прекомит хуком? Когда перед пушем делается пул чтобы поребейзить локальный комит, отметка о пройденных проверках остаётся или надо опять прогнать проверки? Ну и главный вопрос: + 10 минут на комит для контентмейкеров — легко ли они это приняли и точно ли оно того стоит?

          0

          Насколько я помню, при ребейз, если нет конфликта, не вызовет pre-commit. А сами проверки вызываются именно в нём. Могу сказать точно про слияния: если в них нет конфликтов и всё слияние прошло автоматически, то проверки точно не запустится. А чтобы наша система на сервере определила наличие хуков у пользователя мы ставим [-] в конце сообщения к коммиту.


          Обычно проверки идут до двух минут. Десятиминутые проверки воспринимаются плохо, конечно. Всеми силами стараемся оптимизировать и сокращать время. В целом систему ГД воспринимают хорошо, по отзывам она локально ловит очень много опечаток и пропущенных/лишних ресурсов. Поэтому вместо задачи в трекере от QA, или поваленной сборки, они правят ошибку сразу.

          +1
          Помню время когда сам писал тесты для квестов, будучи гд
            0

            Есть желание попробовать научить писать тесты для себя, но мы пока только в начале пути. Если выйдет что-то интересное, то возможно, появится ещё одна статья.

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

          Самое читаемое