Темная сторона SQA Automation.

15 мая 2013, 14:40

Всем доброго времени суток!

О чем пойдет речь: практическое решение задачи - как заработать кучу виртуальных денег в популярной Android игре - CSR Racing, приложив минимум усилий (< 2х часов свободного времени). Фактически, как использовать обратную сторону медали автоматизированного тестирования и наглядный пример того, что же такое на самом деле ночная автоматизация и какую выгоду можно от нее получить.

Статья ориентирована на начинающих QA специалистов, геймеров и любителей виртуального драг рейсинга, критика приветствуется :)

Итак, как же автору удалось одновременно обзавестись крутой тачкой в игре и найти критический баг.

1. Небольшое лирическое предисловие о том, как вообще родилась эта идея. Обзор объекта тестирования.

Началось все с того, что Barnes & Noble включили в свою платформу Google Play и как результат, все пользователи Nook-в, включая меня, получили возможность устанавливать любые приложения из Google Play. Затем у меня сломалась машина и, чтобы не скучать в маршрутке/метро/автобусе, я начал брать с собой рабочий Nook HD+. Быстро была найдена интересная игра автомобильной тематики - CSR Racing (в категории от 5 до 10 млн установок). Она бесплатная и обладает превосходной 3D графикой и геймплеем.

Коварство разработчиков крылось в том, что если вы не делаете in-app-purchases, то у вас имеется только 10 единиц топлива (1 единица на заезд) и для получения следующей единицы топлива необходимо подождать 7 минут. Экономика игры построена на двух принципах: вы можете получать деньги от заездов или покупать деньги за реальные доллары, причем расценки такие:

- Игровые 56 000$ - это совсем небольшая сумма. Нитро 4-го уровня стоит 72, 591$ на Ford Mustang Boss 302, т.е. потратив 10$ реальных денег, мы не сможем даже нитро купить 4-го уровня. Так не пойдет! :)

Получать деньги за победные заезды здорово - но это (на 3-м уровне) ~ 5.700 каждые 7 минут, при стоимости того же нитро в 72, 591. Капля в море.

2. Стратегия действий, для доминирования в игре.

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

3. Доступные стратегии обогащения через автоматизацию, без детального изучения объекта тестирования. 

Примечание: Я все это проделывал на Tier 3, где за проигрыш начисляют 425$. На Tier 1 эта сумма всего 60$ :) (Уточнил: можно найти рейс на Tier 1 и с 325$ призовых. Битва с 2-3 crew, что делает разумным автоматизацию проигрыша, даже на первом уровне).

1) Любая победа приносит хорошие деньги, т.к. взамен мы тратим всего лишь 1 единицу топлива. В игре все упирается в топливо. Если его нет, вам не испытать новенькую турбину в состязании. 3 раза в 24 часа можно отправить email другу "в никуда" и получить 3 ед топлива. На общем фоне не сильно меняет картину.

2) Топливо можно получать за золотые фишки, которые переодически даются за определенные ачивки, но это расточительно Нитро за 72, 591 стоит 8 золотых фишек, а заправить 10 ед топлива - 2 фишки. Жуть!. Гипотетически, используя возможности получения доптоплива, можно удлиннить победную серию.

4. Дополнительно найденная стратегия при детальном изучении объекта тестирования.

3) В результате хорошего ad-hoc тестирования пограничных значений топливного бака было обнаружено, что на последней единице топлива можно переигрывать проигранный рейс бесконечно долго. Если вы проигрываете его, единица добавленного топлива через 7 минут будет потрачена, но дальше можно будет продолжать переигрывать рейс снова и снова на "сухом баке". Что примечательно, за проигрыш вы тоже получаете деньги (бонус за удачный старт, бонус за удачное переключение, бонус за просто участие в рейсе). В рейсе с призовыми в 3000, максимум получилось выжать на проигрыше - 1000 (за счет череды удачных переключений с первой на вторую, со второй на первую и так бесконечно до финиша). Выиграть 1000 на проигрыше достаточно тяжело и требует предельной концентрации. Выиграть 500 - не проблема вообще, достаточно одной удачно переключенной передачи. Время рейса - 12 секунд. Итого получаем (с паузами на загрузку и обработку диалогов) 45 секунд на одну итерацию. 7 минут = 420 секунд, а это 9 полных итераций и, в свою очередь, это 4500 100% заработанных игровых денег, что  на 1500 больше, если просто выиграть заезд и подождать 7 минут до следующей единицы топлива. Неплохо :)

Вот тут-то ладошки и вспотели, сами потянулись написать какой-нить автотест. Только вот на чем его писать?

5. Выбор инструмента реализации.

  1. Хотел было сразу взяться за Google UiAutomator (статья в нашем блоге и документация Google), но у него ограничение по минимальной версии платформы - Android 4.1. Сразу отпадает.
  2. Старый добрый adb shell input swipe/tap. Тоже не работает на Android 4.0.3. Более того, проблема возникнет, если необходимо продолжительное нажатие (6-8 секунд удержание педали газа в начале заезда).
  3. Robotium Solo, методом черного ящикас последующим Robotium тестом. В принципе, допустимо, но придется потратиться на получения apk, создание проекта, написания теста, переподписывание apk, его повторную установку. Ну и плюс все сложности, при работе с Robotium.
  4. Monkey Runner? Блин, как я не пытался - не быть нам с Python вместе. Также проблема очень медленного старта теста.
  5. Старый добрый event-driven подход, который выручал нас с тех пор, когда у эмулятора Андроид была красненькая шкурка.

Предпочтение было отдано #5, так, как это самый простой подход не требующий даже блокнонта, можно прямо в терминале написать весь тест :D

6. Выявление маршрута автоматизации необходимого процесса.

Чтобы что-то успешно автоматизировать на продолжительном отрезке времени, необходимо найти циклическую итерацию и запихнуть ее в while (true). В нашем случае этот маршрут состоит из следующих взаимодействий с экраном:

Предусловие: запущена гонка.

  1. Нажать и держать педаль газа 6 секунд - создаем файл acceleration.sh;
  2. Переключить передачу минимум 2 раза (расчитываем время и дотягиваем его тестами), чтобы получить "Perfect shift" хотя бы раз. Это даст нам 500$ за гонку. Создаем файл shift_up.sh;
  3. Нажать на кнопку "Переиграть рейс". Создаем файл restart_race.sh;
  4. Подавить негативный диалог с вопросом о походе к механику или о необходимости попытаться на более простом уровне. Добавим этот клик в файл restart_race.sh.
  5. Вернуться к шагу 1. Создаем файл scenario.sh с бесконечным циклом.

Хм.. не самый сложный алгоритм :D

7. Реализация

Для упрощения всех кликов внутри выше описанных файлов, создадим еще один файл - click.sh - и запишем в него сигнатуру клика для нашего устройства. Можно использовать метод получения сигнатуры из этой статьи. Сигнатура служебного Nook HD+ доподлинно известна (и скорее всего сработает на большинстве 4.0.3 девайсов):

1. Touch down:                                               

sendevent /dev/input/event2 1 330 1
sendevent /dev/input/event2 3 48 14
sendevent /dev/input/event2 3 53 $2
sendevent /dev/input/event2 3 54 $3
sendevent /dev/input/event2 0 0 0

2. Touch up:

sendevent /dev/input/event2 1 330 0
sendevent /dev/input/event2 3 48 0
sendevent /dev/input/event2 3 53 $2
sendevent /dev/input/event2 3 54 $3
sendevent /dev/input/event2 0 0 0

3. Параметр $1 зарезирвируем для вида действия - нажатие экрана (Touch down) или его отпускание (Touch up).

4. Конечный click.sh (не забудте записать этот файл на девайс) принимает 3 параметра - вид нажатия, X, Y и выглядит следующим образом:

case $1 in
click_down)
  sendevent /dev/input/event2 1 330 1
  sendevent /dev/input/event2 3 48 14
  sendevent /dev/input/event2 3 53 $2
  sendevent /dev/input/event2 3 54 $3
  sendevent /dev/input/event2 0 0 0
;;
click_up)
sendevent /dev/input/event2 1 330 0
sendevent /dev/input/event2 3 48 0
sendevent /dev/input/event2 3 53 $2
sendevent /dev/input/event2 3 54 $3
sendevent /dev/input/event2 0 0 0
;;

*)
   echo Wrong parameter: $1;;
esac

Вызов нажатия в 100 200:

adb shell /data/click.sh click_down 100 200
adb shell /data/click.sh click_up 100 200

Обязательно сделать этому скрипту:

adb shell chmod 777 /data/click.sh

А также интерфейсу, который отвечает за сенсорный экран (в нашем случае event2):

adb shell chmod 777 /dev/event2

Иначе клики работать не будут.

С помощью getevent, описанного в том же артикле, можно получить координаты всех нужных кнопок и запрограммировать наши файлы-функции (далее координаты приведены для Nook HD+ в обратно-ландшафтной ориентации):

#Файл acceleration.sh

echo accelerating!
adb shell /data/./click.sh click_down 784 82
sleep 5 # имитация продолжительного удержания педали газа
adb shell /data/./click.sh click_up 784 82
# Файл shift_up.sh

echo "shift up!"
adb shell /data/./click.sh click_down 1017 473
adb shell /data/./click.sh click_up 1017 473
# Файл restart_race.sh

echo click restart

adb shell /data/./click.sh click_down 1122 1716
sleep .5
adb shell /data/./click.sh click_up 1122 1716

sleep 4 # ожидаем появление возможного негативного диалога

echo suppress possible negative dialog

adb shell /data/./click.sh click_down 913 1064
sleep .5
adb shell /data/./click.sh click_up 913 1064
# Сам сценарий авто теста scenario.sh

while true; do
  echo "-------------------------------------------------"
  echo "Iteration started at: " `date +%d-%m-%y-%H:%M:%S`
 ./acceleration.sh
  sleep 1
  ./shift_up.sh
  sleep 1.4
  ./shift_up.sh
  sleep 1.4
  ./shift_up.sh
  
  sleep 25
  
  echo "Long race sleep is over"
  
  ./restart_race.sh
  sleep 8
 done

Для Windows можно заменить все файлы, кроме того, который записывается на устройство (click.sh), на bat-файлы.

8. Анализ результатов.

1) Самое печальное, что даже под таким простым автоматическим нагрузочным тестированием был найден критический дефект в приложении, который приводит к сбою работы и неожиданному выходу из приложения (это я пытался на русском написать "краш приложения"). Более того, если продолжать игнорировать подобный дефект, через двое суток (в моем случае) это приведет к потере всего вашего накопления, достижений, машин. В общем игра загрузится так, как будто вы первый раз ее установили. Пойманный текст ошибки (вероятно не в нем причина вайпа аккаунта, но этот приводил к регулярным вылетам из игры):

W/Unity   (18032): Player race name requested but nothing or user100000 was returned, defaulting to "You"
W/Unity   (18032):  
W/Unity   (18032): (Filename: ./Runtime/ExportGenerated/AndroidManaged/UnityEngineDebug.cpp Line: 43)
W/Unity   (18032): 
W/Unity   (18032): Player race name requested but nothing or user100000 was returned, defaulting to "You"
W/Unity   (18032):  
W/Unity   (18032): (Filename: ./Runtime/ExportGenerated/AndroidManaged/UnityEngineDebug.cpp Line: 43)
W/Unity   (18032): 
F/Looper  (18032): Could not create wake pipe.  errno=24
F/libc    (18032): Fatal signal 11 (SIGSEGV) at 0xdeadbaad (code=1)
F/Looper  (18032): Could not create wake pipe.  errno=24
F/libc    (18032): Fatal signal 11 (SIGSEGV) at 0xdeadbaad (code=1)
D/DeviceManagerBroadcastReceiver(  384): Updated Battery Level: 73
D/DeviceManagerBroadcastReceiver(  384): Action: android.intent.action.BATTERY_CHANGED
D/DeviceManagerBroadcastReceiver(  384): Set Alarm: false
D/BatteryService(  230): PROCESSVALUES : update plugged:false was:false stat:3 zone:16 was:16 send low:false
I/ActivityManager(  230): Process com.naturalmotion.csrracing (pid 18032) has died.
W/ActivityManager(  230): Scheduling restart of crashed service com.naturalmotion.csrracing/com.bossalien.racer01.CSRNotificationService in 5000ms
W/ActivityManager(  230): Scheduling restart of crashed service com.naturalmotion.csrracing/com.bossalien.racer01.CSRNotificationManager$NotificationIntent in 15000ms
W/ActivityManager(  230): Force removing ActivityRecord{413efa08 com.naturalmotion.csrracing/com.bossalien.racer01.CSRPlayerActivity}: app died, no saved state
I/WindowManager(  230): WIN DEATH: Window{4188c6a0 com.naturalmotion.csrracing/com.bossalien.racer01.CSRPlayerActivity paused=false}
W/WindowManager(  230): Force-removing child win Window{4188caa0 SurfaceView paused=false} from container Window{4188c6a0 com.naturalmotion.csrracing/com.bossalien.racer01.CSRPlayerActivity paused=false}
W/WindowManager(  230): Failed looking up window
W/WindowManager(  230): java.lang.IllegalArgumentException: Requested window android.os.BinderProxy@41ef62f0 does not exist
W/WindowManager(  230):         at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7189)
W/WindowManager(  230):         at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7180)
W/WindowManager(  230):         at com.android.server.wm.WindowState$DeathRecipient.binderDied(WindowState.java:1551)
W/WindowManager(  230):         at android.os.BinderProxy.sendDeathNotice(Binder.java:417)
W/WindowManager(  230):         at dalvik.system.NativeStart.run(Native Method)
I/WindowManager(  230): WIN DEATH: null

2) Мне таки удалось заработать около 500 000$, что почти 100 реальных долларов в эквиваленте :) Данный подход был использован на Tier 3, так как на предыдущих в нем не было необходимости и стоимость запчастей была приемлемой. На Tier 3 мне удалось купить McLaren SLR за 260 000$ (для следующего Tier 4) и Ford Mustang Boss 302 за 120 000$ + большинство апгрейдов к нему. 

Видео результат (прошу прощения за низкое качество)

3) Интересный побочный вывод. Даже простейшая нагрузочная ночная автоматизация может вскрывать серьезные проблемы, а чтобы это было еще более эффективно, разумно делать так (в Ubuntu):

Открываем терминал с тремя вкладками. В первой запускаем сам сценарий. Во второй: 

adb logcat -v time > logOutput.txt (сохраняем в файл журнал ошибок с подопытного устройства)

В случае с Nook HD+ (так как я наверняка знаю, какое сообщение говорит о состоянии батареи):

D/DeviceManagerBroadcastReceiver(  384): Updated Battery Level: 73

Можно еще добавить следующую запись:

adb logcat -v time | grep "Updated Battery"

Что даст нам следующий вывод:

05-13 03:27:28.176 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56
05-13 03:27:33.285 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56
05-13 03:27:43.449 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56
05-13 03:27:48.535 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56
05-13 03:27:58.684 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56
05-13 03:28:08.848 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56

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

Самые внимательные сразу скажут:  как это можно доверять таким значениям, если устройство подключено по USB проводу к компьютеру и постоянно заряжается! И они будут правы. Чтобы обеспечить подобный замер, необходимо перевести ваш ADB в режим работы по WiFi.

При желании можно еще добавить забор скриншотов с помощью утилиты screencap (идет сейчас в поставке). Простой shell script для взятия скриншотов каждые 3 секунды (если убрать паузу, можно получить видео с 2-3 fps после сборки всех скриншотов в gif или avi):

while true; do
  adb shell /system/bin/screencap -p /sdcard/img.png #перезапись не тратит свободное место
  adb pull /sdcard/img.png "./`date`.png"
  sleep 3
done

Если вы захотите повторить это на вашем устройстве и у вас что-то не получится - пишите, разберемся. Если вам нравится подобный род деятельности и вы бы этим хотели заниматься по-серьезному - тоже пишите, разберемся =)

9. Можно ли было сделать этот тест еще круче?

Да, можно было бы еще добавить несколько простых, но классных штук:

  • С помощью любого планировщика задач стартовать тест каждые 10 минут, если текущая Activity != CSR Racing;
  • Предусмотреть маршрут входа в нужный цикл зарабатывания виртуальных денег из нулевого состояния приложения (когда мы его загружаем с домашнего экрана);
  • Оптимизировать сценарий по принципу: выиграй 9 гонок, на 10-й включай стратегию заработка на проигрыше в течении N минут (желательно меньше часа, чтобы не происходил краш приложения), затем подожди часок (чтобы заправился бензобак) и начинай снова. Такой сценарий приносил бы больше денег, так как был бы полностью автоматическим и более стабильным;
  • Ваш вариант =)

ЗЫ: за 25 лайков готов написать статью, как записать и воспроизвести пользовательское взаимодействие с Android устройством на shell event-ах (Windows & Ubuntu). Да, да, такой я алчный негодяй ^__^

Обсуждение