Skip to content

Instantly share code, notes, and snippets.

@androiddevcoding
Last active December 16, 2025 13:28
Show Gist options
  • Select an option

  • Save androiddevcoding/d7b6158f4d6e0c09680bb6240f3ccc72 to your computer and use it in GitHub Desktop.

Select an option

Save androiddevcoding/d7b6158f4d6e0c09680bb6240f3ccc72 to your computer and use it in GitHub Desktop.
Скрипт для запуска Maestro UI-тестов на Android эмуляторе.
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# =============================================================================
# Скрипт для запуска Maestro UI-тестов на Android эмуляторе.
# Может использоваться в pre-push хуке или запускаться вручную.
#
# Использование:
# ${0##*/} [--sdk SDK_PATH] [--maestro MAESTRO_PATH] [--avd AVD_NAME] \
# [--flows TESTS_DIR] [--gradle-task TASK] [--apk APK_PATH]
#
# Переменные окружения (переопределяют значения по умолчанию):
# ANDROID_SDK_ROOT/ANDROID_HOME, MAESTRO_BIN, AVD_NAME, MAESTRO_DIR,
# GRADLE_TASK, APP_APK
# =============================================================================
# -----------------------------------------------------------------------------
# Глобальные константы и значения по умолчанию
# -----------------------------------------------------------------------------
readonly AVD_NAME_DEFAULT="Pixel_6_Pro"
readonly MAESTRO_DIR_DEFAULT="testMaestro/mock"
readonly BOOT_TIMEOUT_DEFAULT=240
readonly GRADLE_TASK_DEFAULT="assembleRustoreMockDebug"
readonly APP_APK_DEFAULT="app/build/outputs/apk/rustoreMock/debug/app-rustore-mock-debug.apk"
# Путь к scrcpy (установлен через winget)
readonly SCRCPY_DIR="/c/Users/codin/AppData/Local/Microsoft/WinGet/Packages/Genymobile.scrcpy_Microsoft.Winget.Source_8wekyb3d8bbwe/scrcpy-win64-v3.3.3"
# -----------------------------------------------------------------------------
# Глобальные переменные (инициализируются позже)
# -----------------------------------------------------------------------------
ROOT=""
LOG_DIR=""
LOG_FILE=""
TS=""
SCRCPY_AVAILABLE=0
SCRCPY_PID=""
SCRCPY_FILE=""
SCRCPY_RECORD_DIR=""
UNAME_OUT=""
SDK_ROOT=""
ADB=""
EMULATOR=""
MAESTRO_BIN=""
AVD_NAME=""
MAESTRO_DIR=""
BOOT_TIMEOUT=""
GRADLE_TASK=""
APP_APK=""
# Метка для именования папок отчётов (будет заменена на имя AVD после его определения)
RUN_LABEL=""
# =============================================================================
# ФУНКЦИИ ЛОГИРОВАНИЯ
# =============================================================================
# -----------------------------------------------------------------------------
# Вывод сообщения с меткой в консоль и лог-файл
# Параметры: сообщение для вывода
# -----------------------------------------------------------------------------
log() {
local label="${RUN_LABEL:-maestro}"
echo "[${label}] $*"
}
# -----------------------------------------------------------------------------
# Инициализация системы логирования
# Определяет корневую директорию, создаёт папку для логов, настраивает tee
# -----------------------------------------------------------------------------
init_logging() {
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
TS="$(date +"%Y%m%d-%H%M%S")"
log "Запуск: $(date)"
log "Репозиторий: $ROOT"
}
# =============================================================================
# ФУНКЦИИ ИНИЦИАЛИЗАЦИИ SCRCPY (ЗАПИСЬ ЭКРАНА)
# =============================================================================
# -----------------------------------------------------------------------------
# Проверка доступности scrcpy и добавление в PATH при наличии
# -----------------------------------------------------------------------------
init_scrcpy() {
if [[ -x "$SCRCPY_DIR/scrcpy.exe" ]]; then
export PATH="$SCRCPY_DIR:$PATH"
SCRCPY_AVAILABLE=1
log "scrcpy включён: $SCRCPY_DIR"
else
log "scrcpy не найден в $SCRCPY_DIR - запись видео отключена"
fi
}
# -----------------------------------------------------------------------------
# Запуск записи экрана через scrcpy
# Вызывается перед установкой APK
# -----------------------------------------------------------------------------
start_scrcpy_recording() {
if [[ "$SCRCPY_AVAILABLE" -eq 1 && -z "$SCRCPY_PID" ]]; then
mkdir -p "$SCRCPY_RECORD_DIR"
SCRCPY_FILE="$SCRCPY_RECORD_DIR/${RUN_LABEL}-${TS}.mp4"
log "Запуск записи scrcpy: $SCRCPY_FILE"
scrcpy --no-window --no-audio --record="$SCRCPY_FILE" &
SCRCPY_PID=$!
fi
}
# -----------------------------------------------------------------------------
# Корректная остановка записи scrcpy для получения валидного MP4-файла
# Отправляет SIGINT, ожидает завершения, проверяет файл
# -----------------------------------------------------------------------------
stop_scrcpy_recording() {
if [[ -z "$SCRCPY_PID" ]]; then
return 0
fi
log "Остановка записи scrcpy (PID=$SCRCPY_PID)"
# Отправляем SIGINT (эквивалент Ctrl+C) для корректного завершения
kill -INT "$SCRCPY_PID" 2>/dev/null || true
# Ожидаем до 10 секунд корректного завершения
local waited=0
while kill -0 "$SCRCPY_PID" 2>/dev/null; do
sleep 1
waited=$((waited + 1))
if [[ $waited -ge 10 ]]; then
log "scrcpy не завершился вовремя; отправляем SIGTERM"
kill -TERM "$SCRCPY_PID" 2>/dev/null || true
break
fi
done
# Финальное ожидание процесса
wait "$SCRCPY_PID" 2>/dev/null || true
SCRCPY_PID=""
# Пауза для записи и закрытия контейнера
sleep 1
# Проверяем результат записи
if [[ -n "$SCRCPY_FILE" && -f "$SCRCPY_FILE" ]]; then
local size
size=$(stat -c %s "$SCRCPY_FILE" 2>/dev/null || stat -f %z "$SCRCPY_FILE" 2>/dev/null || echo 0)
if [[ "$size" -gt 0 ]]; then
log "Запись scrcpy завершена: $SCRCPY_FILE (${size} байт)"
else
log "Предупреждение: файл записи пуст или повреждён: $SCRCPY_FILE"
fi
else
log "Предупреждение: файл записи не найден: $SCRCPY_FILE"
fi
}
# =============================================================================
# ФУНКЦИИ ОБРАБОТКИ КОНФИГУРАЦИИ
# =============================================================================
# -----------------------------------------------------------------------------
# Применение значений по умолчанию из переменных окружения
# -----------------------------------------------------------------------------
apply_defaults() {
AVD_NAME="${AVD_NAME:-$AVD_NAME_DEFAULT}"
MAESTRO_DIR="${MAESTRO_DIR:-$MAESTRO_DIR_DEFAULT}"
BOOT_TIMEOUT="${BOOT_TIMEOUT:-$BOOT_TIMEOUT_DEFAULT}"
GRADLE_TASK="${GRADLE_TASK:-$GRADLE_TASK_DEFAULT}"
APP_APK="${APP_APK:-$APP_APK_DEFAULT}"
}
# -----------------------------------------------------------------------------
# Настройка метки запуска и путей для логов/отчётов/записей
# Вызывается после определения AVD_NAME
# -----------------------------------------------------------------------------
setup_run_label() {
# Метка формируется из имени AVD
RUN_LABEL="$AVD_NAME"
# Все логи, отчёты и записи хранятся в maestro-report
LOG_DIR="$ROOT/maestro-report/${RUN_LABEL}-${TS}"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/run.log"
# Папка для записи видео (внутри папки отчёта)
SCRCPY_RECORD_DIR="$LOG_DIR"
# Перенаправляем stdout и stderr в консоль и лог-файл одновременно
exec > >(tee -a "$LOG_FILE") 2>&1
log "Метка запуска: $RUN_LABEL"
log "Папка отчёта: $LOG_DIR"
log "Лог-файл: $LOG_FILE"
}
# -----------------------------------------------------------------------------
# Разбор аргументов командной строки
# Параметры: все аргументы скрипта ($@)
# -----------------------------------------------------------------------------
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
--sdk)
if [[ $# -lt 2 ]]; then
log "Ошибка: --sdk требует путь"
exit 1
fi
ANDROID_SDK_ROOT="$2"
shift 2
;;
--maestro)
if [[ $# -lt 2 ]]; then
log "Ошибка: --maestro требует путь"
exit 1
fi
MAESTRO_BIN="$2"
shift 2
;;
--avd)
if [[ $# -lt 2 ]]; then
log "Ошибка: --avd требует имя"
exit 1
fi
AVD_NAME="$2"
shift 2
;;
--flows|--maestro-dir)
if [[ $# -lt 2 ]]; then
log "Ошибка: --flows требует директорию"
exit 1
fi
MAESTRO_DIR="$2"
shift 2
;;
--gradle-task|--task)
if [[ $# -lt 2 ]]; then
log "Ошибка: --gradle-task требует имя задачи"
exit 1
fi
GRADLE_TASK="$2"
shift 2
;;
--apk)
if [[ $# -lt 2 ]]; then
log "Ошибка: --apk требует путь к файлу"
exit 1
fi
APP_APK="$2"
shift 2
;;
--help|-h)
show_help
exit 0
;;
*)
log "Ошибка: Неизвестный параметр $1"
exit 1
;;
esac
done
}
# -----------------------------------------------------------------------------
# Вывод справки по использованию скрипта
# -----------------------------------------------------------------------------
show_help() {
cat << EOF
Использование: ${0##*/} [ПАРАМЕТРЫ]
Параметры:
--sdk SDK_PATH Путь к Android SDK
--maestro MAESTRO_PATH Путь к исполняемому файлу Maestro
--avd AVD_NAME Имя Android Virtual Device
--flows DIR Директория с Maestro тестами
--gradle-task TASK Gradle задача для сборки APK
--apk APK_PATH Путь к APK файлу
--help, -h Показать эту справку
EOF
}
# =============================================================================
# ФУНКЦИИ ПРЕОБРАЗОВАНИЯ ПУТЕЙ
# =============================================================================
# -----------------------------------------------------------------------------
# Преобразование Windows-пути в Unix-формат (для Git Bash)
# Параметры: путь для преобразования
# Возвращает: преобразованный путь через stdout
# -----------------------------------------------------------------------------
convert_windows_path_to_unix() {
local path="$1"
if [[ "$path" =~ ^[A-Za-z]:\\ ]] || [[ "$path" =~ ^[A-Za-z]:/ ]]; then
if command -v cygpath >/dev/null 2>&1; then
cygpath "$path"
else
local drive_letter
drive_letter="$(echo "$path" | cut -c1 | tr '[:upper:]' '[:lower:]')"
local rest_path="${path:2}"
rest_path="${rest_path//\\/\/}"
echo "/$drive_letter/$rest_path"
fi
else
echo "$path"
fi
}
# -----------------------------------------------------------------------------
# Нормализация всех путей из конфигурации
# Преобразует Windows-пути в Unix-формат при необходимости
# -----------------------------------------------------------------------------
normalize_paths() {
MAESTRO_DIR="$(convert_windows_path_to_unix "$MAESTRO_DIR")"
APP_APK="$(convert_windows_path_to_unix "$APP_APK")"
# Определяем тип ОС
UNAME_OUT="$(uname -s || true)"
}
# =============================================================================
# ФУНКЦИИ НАСТРОЙКИ ANDROID SDK
# =============================================================================
# -----------------------------------------------------------------------------
# Поиск и настройка Android SDK
# Ищет SDK в стандартных местах, если не задан явно
# -----------------------------------------------------------------------------
setup_android_sdk() {
SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
if [[ -z "$SDK_ROOT" ]]; then
log "ANDROID_SDK_ROOT и ANDROID_HOME не заданы, ищем SDK..."
case "$UNAME_OUT" in
Darwin*)
SDK_ROOT="$HOME/Library/Android/sdk"
;;
Linux*)
SDK_ROOT="$HOME/Android/Sdk"
;;
MINGW*|MSYS*|CYGWIN*)
SDK_ROOT="$HOME/AppData/Local/Android/Sdk"
;;
*)
SDK_ROOT=""
;;
esac
if [[ -n "$SDK_ROOT" && -d "$SDK_ROOT" ]]; then
log "Используем путь SDK по умолчанию: $SDK_ROOT"
else
log "Android SDK не найден. Установите ANDROID_SDK_ROOT."
exit 1
fi
fi
# Преобразуем Windows-путь в Unix-формат
SDK_ROOT="$(convert_windows_path_to_unix "$SDK_ROOT")"
}
# -----------------------------------------------------------------------------
# Настройка путей к adb и эмулятору
# Добавляет расширение .exe для Windows при необходимости
# -----------------------------------------------------------------------------
setup_android_tools() {
ADB="${SDK_ROOT}/platform-tools/adb"
EMULATOR="${SDK_ROOT}/emulator/emulator"
# Добавляем .exe для Windows
case "$UNAME_OUT" in
MINGW*|MSYS*|CYGWIN*)
if [[ -f "${ADB}.exe" ]]; then ADB="${ADB}.exe"; fi
if [[ -f "${EMULATOR}.exe" ]]; then EMULATOR="${EMULATOR}.exe"; fi
;;
esac
# Проверяем доступность adb
if [[ ! -x "$ADB" && ! -f "$ADB" ]]; then
if command -v adb >/dev/null 2>&1; then
ADB="adb"
else
log "adb не найден."
exit 1
fi
fi
# Проверяем доступность эмулятора
if [[ ! -x "$EMULATOR" && ! -f "$EMULATOR" ]]; then
if command -v emulator >/dev/null 2>&1; then
EMULATOR="emulator"
else
log "emulator не найден."
exit 1
fi
fi
}
# =============================================================================
# ФУНКЦИИ НАСТРОЙКИ MAESTRO CLI
# =============================================================================
# -----------------------------------------------------------------------------
# Поиск и настройка Maestro CLI
# Проверяет наличие Maestro в PATH или по указанному пути
# -----------------------------------------------------------------------------
setup_maestro_cli() {
# Преобразуем путь Maestro, если задан
if [[ -n "${MAESTRO_BIN:-}" ]]; then
MAESTRO_BIN="$(convert_windows_path_to_unix "$MAESTRO_BIN")"
fi
if [[ -z "${MAESTRO_BIN:-}" ]]; then
# Ищем Maestro в PATH
if command -v maestro >/dev/null 2>&1; then
MAESTRO_BIN="maestro"
elif command -v maestro.bat >/dev/null 2>&1; then
MAESTRO_BIN="maestro.bat"
else
log "Maestro CLI не найден. Установите Maestro или задайте MAESTRO_BIN."
exit 1
fi
else
# Проверяем указанный путь
if [[ "$MAESTRO_BIN" == *"/"* ]]; then
if [[ ! -f "$MAESTRO_BIN" ]]; then
log "Maestro не найден по пути: $MAESTRO_BIN"
exit 1
fi
else
if ! command -v "$MAESTRO_BIN" >/dev/null 2>&1; then
log "Maestro '$MAESTRO_BIN' не найден в PATH."
exit 1
fi
fi
fi
}
# =============================================================================
# ФУНКЦИИ УПРАВЛЕНИЯ ЭМУЛЯТОРОМ
# =============================================================================
# -----------------------------------------------------------------------------
# Проверка, запущен ли уже эмулятор
# Возвращает: 0 если эмулятор запущен, 1 если нет
# -----------------------------------------------------------------------------
is_emulator_running() {
# Используем subshell и || true для защиты от set -e
if "$ADB" devices 2>/dev/null | tail -n +2 | grep -q "^emulator-"; then
return 0
else
return 1
fi
}
# -----------------------------------------------------------------------------
# Запуск эмулятора AVD в фоновом режиме
# -----------------------------------------------------------------------------
start_emulator() {
log "Запуск эмулятора: $AVD_NAME"
"$EMULATOR" -avd "$AVD_NAME" -netdelay none -netspeed full >/dev/null 2>&1 &
sleep 5
}
# -----------------------------------------------------------------------------
# Ожидание полной загрузки эмулятора
# Проверяет sys.boot_completed и init.svc.bootanim
# Завершается с ошибкой при превышении таймаута
# -----------------------------------------------------------------------------
wait_for_boot() {
log "Ожидание загрузки эмулятора (таймаут ${BOOT_TIMEOUT} сек)..."
local elapsed=0
local interval=5
while [[ $elapsed -lt $BOOT_TIMEOUT ]]; do
# Используем || true чтобы избежать выхода из-за set -e когда эмулятор ещё не появился
if "$ADB" devices 2>/dev/null | tail -n +2 | grep -q "^emulator-"; then
local boot_completed=""
local boot_anim=""
# Получаем статус загрузки с защитой от ошибок
boot_completed="$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)"
boot_anim="$("$ADB" shell getprop init.svc.bootanim 2>/dev/null | tr -d '\r' || true)"
if [[ "$boot_completed" == "1" || "$boot_anim" == "stopped" ]]; then
log "Эмулятор загружен."
return 0
fi
log "Ожидание... (boot_completed=$boot_completed, boot_anim=$boot_anim)"
else
log "Ожидание появления эмулятора в списке устройств..."
fi
sleep "$interval"
elapsed=$((elapsed + interval))
done
log "Эмулятор не загрузился за ${BOOT_TIMEOUT} секунд."
exit 1
}
# -----------------------------------------------------------------------------
# Управление запуском эмулятора
# Запускает новый эмулятор, если он ещё не запущен
# -----------------------------------------------------------------------------
ensure_emulator_running() {
if is_emulator_running; then
log "Эмулятор уже запущен."
else
log "Запуск нового эмулятора..."
start_emulator
fi
wait_for_boot
}
# =============================================================================
# ФУНКЦИИ СБОРКИ И ТЕСТИРОВАНИЯ
# =============================================================================
# -----------------------------------------------------------------------------
# Сборка APK через Gradle
# Выполняет указанную Gradle-задачу и проверяет наличие результата
# -----------------------------------------------------------------------------
build_apk() {
log "Сборка APK с помощью Gradle: $GRADLE_TASK"
(cd "$ROOT" && ./gradlew "$GRADLE_TASK")
local apk_path="$APP_APK"
if [[ "$apk_path" != /* ]]; then
apk_path="$ROOT/$apk_path"
fi
if [[ ! -f "$apk_path" ]]; then
log "APK не найден: $apk_path"
exit 1
fi
log "APK готов: $apk_path"
}
# -----------------------------------------------------------------------------
# Установка APK на эмулятор
# Параметры: путь к APK
# -----------------------------------------------------------------------------
install_apk() {
local apk_path="$1"
log "Установка APK через adb : $apk_path"
"$ADB" install -r "$apk_path" || {
log "Ошибка установки APK через adb"
stop_scrcpy_recording
exit 1
}
}
# -----------------------------------------------------------------------------
# Запуск Maestro тестов
# Создаёт отчёты в HTML формате с отладочной информацией
# -----------------------------------------------------------------------------
run_maestro_tests() {
local flows_path="$1"
local report_dir="$ROOT/maestro-report/${RUN_LABEL}-${TS}"
mkdir -p "$report_dir"
local debug_dir="$report_dir/debug"
mkdir -p "$debug_dir"
log "Запуск Maestro тестов..."
"$MAESTRO_BIN" test \
--debug-output="$debug_dir" \
--format=HTML \
--output="$report_dir/report.html" \
"$flows_path" \
2>&1 | tee "$report_dir/maestro-output.log" || {
log "====================================="
log "Maestro тесты ПРОВАЛЕНЫ!"
log "Отчёт сохранён: $report_dir"
log "HTML отчёт: $report_dir/report.html"
log "Отладочный вывод: $debug_dir"
stop_scrcpy_recording
log "====================================="
exit 1
}
# Останавливаем запись после успешного прогона
stop_scrcpy_recording
sleep 2
log "Maestro тесты пройдены!"
log "Отчёт сохранён: $report_dir"
log "HTML отчёт: $report_dir/report.html"
}
# -----------------------------------------------------------------------------
# Запуск полного цикла Maestro тестирования
# Устанавливает APK, запускает запись экрана и выполняет тесты
# -----------------------------------------------------------------------------
run_maestro() {
local flows_path="$MAESTRO_DIR"
if [[ "$flows_path" != /* ]]; then
flows_path="$ROOT/$flows_path"
fi
local apk_path="$APP_APK"
if [[ "$apk_path" != /* ]]; then
apk_path="$ROOT/$apk_path"
fi
if [[ ! -d "$flows_path" ]]; then
log "Директория Maestro не найдена: $flows_path"
exit 1
fi
log "Запуск Maestro flows из: $flows_path"
# Запуск записи экрана перед установкой APK
start_scrcpy_recording
# Установка APK
install_apk "$apk_path"
# Запуск тестов
run_maestro_tests "$flows_path"
}
# =============================================================================
# ФУНКЦИИ ВЫВОДА ИНФОРМАЦИИ
# =============================================================================
# -----------------------------------------------------------------------------
# Вывод текущей конфигурации
# Отображает все используемые пути и настройки
# -----------------------------------------------------------------------------
print_configuration() {
log "SDK_ROOT: ${SDK_ROOT:-<не задан>}"
log "Используемый adb: $ADB"
log "Используемый emulator: $EMULATOR"
log "Используемый Maestro: $MAESTRO_BIN"
log "AVD_NAME: $AVD_NAME"
log "MAESTRO_DIR: $MAESTRO_DIR"
log "GRADLE_TASK: $GRADLE_TASK"
log "APP_APK: $APP_APK"
}
# =============================================================================
# ГЛАВНАЯ ФУНКЦИЯ
# =============================================================================
# -----------------------------------------------------------------------------
# Главная точка входа скрипта
# Координирует весь процесс тестирования
# Параметры: все аргументы командной строки ($@)
# -----------------------------------------------------------------------------
main() {
# 1. Инициализация логирования
init_logging
# 2. Инициализация scrcpy
init_scrcpy
# 3. Применение значений по умолчанию
apply_defaults
# 4. Разбор аргументов командной строки
parse_arguments "$@"
# 5. Настройка метки запуска и путей (после определения AVD_NAME)
setup_run_label
# 6. Нормализация путей
normalize_paths
# 7. Настройка Android SDK и инструментов
setup_android_sdk
setup_android_tools
# 8. Настройка Maestro CLI
setup_maestro_cli
# 9. Вывод конфигурации
print_configuration
# 10. Запуск эмулятора
ensure_emulator_running
# 11. Сборка APK
build_apk
# 12. Запуск Maestro тестов
run_maestro
log "Maestro тесты успешно пройдены."
exit 0
}
# =============================================================================
# ТОЧКА ВХОДА
# =============================================================================
# Запуск главной функции с передачей всех аргументов
main "$@"
#!/bin/sh
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
echo "[pre-push] Project root: $PROJECT_ROOT"
SCRIPT="$PROJECT_ROOT/scripts/maestro_emulator_check.sh"
if [ ! -f "$SCRIPT" ]; then
echo "[pre-push] ERROR: Script not found: $SCRIPT"
exit 1
fi
echo "[pre-push] Running Maestro script..."
sh "$SCRIPT"
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "======================================"
echo "[pre-push] ❌ Push blocked: Maestro tests FAILED"
echo "======================================"
exit 1
fi
echo "======================================"
echo "[pre-push] ✅ Maestro tests passed. Push allowed."
echo "======================================"
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment