Allow to measure battery and network consumption on real device

This commit is contained in:
Lukasz Fryc 2018-11-07 13:26:32 +01:00
parent 50200404ce
commit eed610c46e
No known key found for this signature in database
GPG Key ID: 58BB9E7DA4DA6459
14 changed files with 434 additions and 14 deletions

View File

@ -0,0 +1,110 @@
FROM ubuntu:16.04
LABEL maintainer "Lukasz Fryc <lukasz@status.im>"
WORKDIR /root
#==================
# General Packages
#------------------
# openjdk-8-jdk
# Java
# ca-certificates
# SSL client
# tzdata
# Timezone
# zip
# Make a zip file
# unzip
# Unzip zip file
# curl
# Transfer data from or to a server
# wget
# Network downloader
# libqt5webkit5
# Web content engine (Fix issue in Android)
# libgconf-2-4
# Required package for chrome and chromedriver to run on Linux
# xvfb
# X virtual framebuffer
#==================
RUN apt-get -qqy update && \
apt-get -qqy --no-install-recommends install \
openjdk-8-jdk \
ca-certificates \
tzdata \
zip \
unzip \
curl \
wget \
libqt5webkit5 \
libgconf-2-4 \
xvfb \
build-essential \
&& rm -rf /var/lib/apt/lists/*
#===============
# Set JAVA_HOME
#===============
ENV JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64/jre" \
PATH=$PATH:$JAVA_HOME/bin
#=====================
# Install Android SDK
#=====================
ARG SDK_VERSION=sdk-tools-linux-4333796
ARG ANDROID_BUILD_TOOLS_VERSION=28.0.1
ARG ANDROID_PLATFORM_VERSION="android-27"
ENV SDK_VERSION=$SDK_VERSION \
ANDROID_BUILD_TOOLS_VERSION=$ANDROID_BUILD_TOOLS_VERSION \
ANDROID_HOME=/root
RUN wget -O tools.zip https://dl.google.com/android/repository/${SDK_VERSION}.zip && \
unzip tools.zip && rm tools.zip && \
chmod a+x -R $ANDROID_HOME && \
chown -R root:root $ANDROID_HOME
ENV PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin
# https://askubuntu.com/questions/885658/android-sdk-repositories-cfg-could-not-be-loaded
RUN mkdir -p ~/.android && \
touch ~/.android/repositories.cfg && \
echo y | sdkmanager "platform-tools" && \
echo y | sdkmanager "build-tools;$ANDROID_BUILD_TOOLS_VERSION" && \
echo y | sdkmanager "platforms;$ANDROID_PLATFORM_VERSION"
ENV PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools
#====================================
# Install latest nodejs, npm, appiuma
#====================================
# ARG APPIUM_VERSION=1.7.2
# ENV APPIUM_VERSION=$APPIUM_VERSION
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
RUN apt-get -qqy install nodejs
RUN npm set maxsockets 3 && \
npm install -g appium@1.7.2 --unsafe-perm=true --allow-root
RUN apt-get remove --purge -y npm && \
apt-get autoremove --purge -y && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
apt-get clean
#==================================
# Fix Issue with timezone mismatch
#==================================
# ENV TZ="US/Pacific"
# RUN echo "${TZ}" > /etc/timezone
#===============
# Expose Ports
#---------------
# 4723
# Appium port
#===============
EXPOSE 4723
COPY entry_point.sh /root/
RUN chmod +x /root/entry_point.sh
ENTRYPOINT /root/entry_point.sh

View File

@ -0,0 +1,66 @@
## Instructions
Use device with Android 7.1 or newer.
1. Install needed tools
- Install Docker https://docs.docker.com/install/
- Install Python 3
Go to https://realpython.com/installing-python and follow the instructions. For macOS with Homebrew, you can run `brew install python3` to install it.
2. Build Appium docker image
- Make sure Docker app is running
- Build Appium image to have environment in which tests will be executed
```
$ cd appium/docker
$ docker build -t "appium/appium:local" .
```
Make sure all build steps were successful. You should see:
```
Successfully tagged appium/appium:local
```
3. Find out IP address of the device
On device, open wifi settings and click on the current network. Write down the IP address as it will be needed for running the battery test wirelessly.
4. Run the battery test
- Make sure the device is sufficiently charged
- Disconnect the device USB cable
- Create a directory that will be shared with the docker container. E.g. `mkdir /Users/lukas/Desktop/shared`
- Put .apk to test into the shared directory. E.g. `mv StatusIm-181109-092655-009d97-e2e.apk /Users/lukas/Desktop/shared`
- Run a test with using appium container
- `--device_ip` - IP address of the device (see 3.)
- `--apk=StatusIm-181109-092655-009d97-e2e.apk` - name of the apk file
- `--docker=True` - run the tests using appium docker container
- `--docker_shared_volume=/Users/lukas/Desktop/shared` - path to the shared directory
- `--bugreport=True` - generate bugreport file
```
$ cd status-react/test/appium
$ python3 -m pytest --apk=StatusIm-181109-092655-009d97-e2e.apk --device_ip=192.168.0.104 --bugreport=True --docker=true --docker_shared_volume=/Users/lukas/Desktop/shared tests/atomic/transactions/test_wallet.py::TestTransactionWalletSingleDevice::test_send_eth_from_wallet_to_contact
```
- Follow the instructions shown in the command line
**In case of issues:**
Find out the Appium container id: `docker ps` and get the latest logs: `docker logs ebdf1761f51f` where `ebdf1761f51f` is the container id.
5. Analyse test results
The test generates Android bugreport that is saved in shared directory. It contains data about battery, cpu, memory, network usage, and more. See [energy-efficient-bok](https://github.com/status-im/energy-efficient-bok/blob/master/QA_Android.md) or [Battery Historian](https://developer.android.com/studio/profile/battery-historian) to learn how to analyse it.
## Issues with adb connect
To run the battery test without charging the device, adb needs to connect to it wirelessly. To avoid wifi connection issues, you can connect to the device via bluetooth.
macOS instructions:
- On device, enable bluetooth on your device in Android settings -> connections -> bluetooth
- On device, go to mobile hotspots and tethering and enable bluetooth tethering
- On macOS, go to the bluetooth settings and connect with the device (click "Connect to Network")

View File

@ -0,0 +1,6 @@
#!/bin/bash
APPIUM_LOG="/var/log/appium.log"
CMD="appium --log $APPIUM_LOG"
$CMD

View File

@ -47,3 +47,5 @@ toolz==0.9.0
urllib3==1.22
yarl==0.12.0
zbarlight==1.2
docker
influxdb

View File

@ -0,0 +1,111 @@
import datetime
import logging
import re
import subprocess
import time
import docker
from docker.errors import NotFound
DOCKER_SHARED_VOLUME_PATH = '/root/shared_volume'
class DeviceStats(object):
def __init__(self):
self.total_battery_capacity_mah = None
# Est power used (mAh) by the app
self.estimated_power_usage_mah = None
class AppiumContainer:
def start_appium_container(self, shared_volume):
docker_client = docker.from_env()
try:
self.container = docker_client.containers.get("appium")
self.container.restart()
except NotFound:
self.container = docker_client.containers.run("appium/appium:local", detach=True, name="appium",
ports={'4723/tcp': 4723},
volumes={shared_volume: {
'bind': DOCKER_SHARED_VOLUME_PATH, 'mode': 'rw'}})
logging.info("Running Appium container. ID: %s" % self.container.short_id)
def exec_run(self, cmd):
return self.container.exec_run(cmd, stdout=True, stderr=True, stdin=True)
def connect_device(self, device_ip):
device_address = device_ip + ':5555'
connected_state = "connected to %s" % device_address
cmd = self.exec_run(['adb', 'connect', device_address])
if connected_state in cmd.output.decode("utf-8"):
logging.info("adb is already connected with the device")
else:
logging.info("Connecting the device with adb..")
# Restart adb on host machine
subprocess.call(['adb', 'kill-server'])
input("Connect USB cable to the device and press ENTER..")
# Reset USB on host machine
subprocess.call(['adb', 'usb'])
time.sleep(5) # wait until adb usb is restarted
# Set the target device to listen for a TCP/IP connection on port 5555 on host machine
subprocess.call(['adb', 'tcpip', '5555'])
# restarting in TCP mode port: 5555
input("Now, disconnect the USB cable and press ENTER..")
# Connect to the device in docker container
self.exec_run(['adb', 'connect', device_address])
input("Please check your device and allow for USB debugging if necessary. Then press ENTER to continue "
"the test..\n")
def reset_battery_stats(self):
logging.info("Resetting device stats..")
self.exec_run(['adb', 'shell', 'dumpsys', 'batterystats', '--reset'])
def generate_bugreport(self, report_name):
now = datetime.datetime.now()
bugreport_name = "bugreport_%s_%s" % (report_name, now.strftime("%Y-%m-%d_%H-%M-%S"))
print("\nGenerating device report from the test..")
self.exec_run(['adb', 'bugreport', "/%s/%s" % (DOCKER_SHARED_VOLUME_PATH, bugreport_name)])
print("Device report saved in the shared volume as %s.zip" % bugreport_name)
def get_device_stats(self):
stats = DeviceStats()
# Find process uid
uid_line = self.exec_run(['adb', 'shell', 'ps', '|', 'grep', 'im.status.ethereum']).output \
.decode('utf-8')
# Match first word which is the uid
match = re.match(r'(?:^|(?:[.!?]\s))(\w+)', uid_line, re.M | re.I)
uid = match.group(1).replace('_', '')
# Battery stats
batterystats = self.exec_run(['adb', 'shell', 'dumpsys', 'batterystats', 'im.status.ethereum']).output \
.decode('utf-8').splitlines()
battery_usage_line = [s for s in batterystats if "Uid %s" % uid in s][0]
match = re.match(r'.* Uid %s: ([^\s]+)' % uid, battery_usage_line, re.M | re.I)
stats.estimated_power_usage_mah = float(match.group(1))
capacity_line = [s for s in batterystats if "Capacity" in s][0]
match = re.match(r'.* Capacity: (.*), Computed drain: (.*), .*', capacity_line, re.M | re.I)
stats.total_battery_capacity_mah = float(match.group(1))
stats.total_computed_drain_mah = float(match.group(2))
# Wi-Fi stats
wifi_stats = self.exec_run(['adb', 'shell', 'dumpsys', 'batterystats', 'im.status.ethereum', '|', 'grep',
'Wi-Fi\ total']).output.decode('utf-8')
stats.wifi_received = wifi_stats.split()[3].replace(',', '')
stats.wifi_sent = wifi_stats.split()[5]
# OS stats
stats.os_version = self.exec_run(['adb', 'shell', 'getprop', 'ro.build.version.release'])\
.output.decode('utf-8').rstrip("\n")
stats.device_model = self.exec_run(['adb', 'shell', 'getprop', 'ro.product.model'])\
.output.decode('utf-8').rstrip("\n")
return stats
def stop_container(self):
self.container.stop()

View File

@ -0,0 +1,39 @@
import datetime
from influxdb import InfluxDBClient
def convert_to_mb(value):
number = float(value[:-2])
unit = value[-2:]
if unit == 'KB':
number = number / 1000
return number
class DeviceStatsDB:
def __init__(self, host, port, username, password, database):
self.client = InfluxDBClient(host, port, username, password, database)
def save_stats(self, build_name, test_name, test_group, test_passed, device_stats):
json_body = [
{
"measurement": "device",
"time": datetime.datetime.utcnow().isoformat(),
"fields": {
"battery_used_mah": device_stats.estimated_power_usage_mah,
"wifi_sent": convert_to_mb(device_stats.wifi_sent),
"wifi_received": convert_to_mb(device_stats.wifi_received),
},
"tags": {
"test_name": test_name,
"test_group": test_group,
"build_name": build_name,
"device_model": device_stats.device_model,
"device_os_version": device_stats.os_version,
"test_passed": test_passed
}
}
]
self.client.write_points(json_body)

View File

@ -2,6 +2,7 @@ import asyncio
import logging
from datetime import datetime
from support.appium_container import AppiumContainer
from support.test_data import TestSuiteData
@ -24,6 +25,7 @@ def debug(text: str):
test_suite_data = TestSuiteData()
appium_container = AppiumContainer()
common_password = 'qwerty'
unique_password = 'unique' + get_current_time()

View File

@ -10,6 +10,7 @@ class TestCreateAccount(SingleDeviceTestCase):
@marks.testrail_id(5300)
@marks.critical
@marks.battery_consumption
def test_create_account(self):
sign_in = SignInView(self.driver, skip_popups=False)
sign_in.accept_agreements()

View File

@ -201,6 +201,7 @@ class TestProfileSingleDevice(SingleDeviceTestCase):
@marks.testrail_id(5382)
@marks.high
@marks.battery_consumption
def test_contact_profile_view(self):
sign_in_view = SignInView(self.driver)
sign_in_view.create_user()

View File

@ -13,6 +13,7 @@ class TestRecoverAccountSingleDevice(SingleDeviceTestCase):
@marks.testrail_id(5301)
@marks.critical
@marks.battery_consumption
def test_recover_account(self):
sign_in = SignInView(self.driver)
home = sign_in.create_user()

View File

@ -474,6 +474,7 @@ class TestMessagesOneToOneChatSingle(SingleDeviceTestCase):
@marks.testrail_id(5328)
@marks.critical
@marks.battery_consumption
def test_send_emoji(self):
sign_in = SignInView(self.driver)
home = sign_in.create_user()

View File

@ -1,20 +1,21 @@
import asyncio
import logging
import pytest
import sys
import re
import subprocess
import asyncio
from support.message_reliability_report import create_one_to_one_chat_report, create_public_chat_report
from support.api.network_api import NetworkApi
from os import environ
from appium import webdriver
import sys
from abc import ABCMeta, abstractmethod
from selenium.common.exceptions import WebDriverException
from tests import test_suite_data, start_threads
from os import environ
import pytest
from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import WebDriverException
from support.api.network_api import NetworkApi
from support.github_report import GithubHtmlReport
from support.message_reliability_report import create_one_to_one_chat_report, create_public_chat_report
from tests import test_suite_data, start_threads, appium_container
class AbstractTestCase:
@ -81,11 +82,16 @@ class AbstractTestCase:
@property
def capabilities_local(self):
desired_caps = dict()
desired_caps['app'] = pytest.config.getoption('apk')
if pytest.config.getoption('docker'):
# apk is in shared volume directory
apk = '/root/shared_volume/%s' % pytest.config.getoption('apk')
else:
apk = pytest.config.getoption('apk')
desired_caps['app'] = apk
desired_caps['deviceName'] = 'nexus_5'
desired_caps['platformName'] = 'Android'
desired_caps['appiumVersion'] = '1.7.2'
desired_caps['platformVersion'] = '7.1'
desired_caps['platformVersion'] = pytest.config.getoption('platform_version')
desired_caps['newCommandTimeout'] = 600
desired_caps['fullReset'] = False
desired_caps['unicodeKeyboard'] = True
@ -152,21 +158,28 @@ class Driver(webdriver.Remote):
class SingleDeviceTestCase(AbstractTestCase):
def setup_method(self, method, max_duration=1800):
if pytest.config.getoption('docker'):
appium_container.start_appium_container(pytest.config.getoption('docker_shared_volume'))
appium_container.connect_device(pytest.config.getoption('device_ip'))
(executor, capabilities) = (self.executor_sauce_lab, self.capabilities_sauce_lab) if \
self.environment == 'sauce' else (self.executor_local, self.capabilities_local)
capabilities['maxDuration'] = max_duration
self.driver = Driver(executor, capabilities)
test_suite_data.current_test.testruns[-1].jobs[self.driver.session_id] = 1
self.driver.implicitly_wait(self.implicitly_wait)
if pytest.config.getoption('docker'):
appium_container.reset_battery_stats()
def teardown_method(self, method):
if self.environment == 'sauce':
self.print_sauce_lab_info(self.driver)
try:
self.add_alert_text_to_report(self.driver)
self.driver.quit()
if pytest.config.getoption('docker'):
appium_container.stop_container()
except (WebDriverException, AttributeError):
pass
finally:

View File

@ -2,8 +2,10 @@ import requests
import pytest
import re
from _pytest.runner import runtestprotocol
from support.device_stats_db import DeviceStatsDB
from support.test_rerun import should_rerun_test
from tests import test_suite_data
from tests import test_suite_data, appium_container
from datetime import datetime
from os import environ
from io import BytesIO
@ -35,6 +37,10 @@ def pytest_addoption(parser):
action='store',
default='sauce',
help='Specify environment: local/sauce/api')
parser.addoption('--platform_version',
action='store',
default='7.1',
help='Android device platform version')
parser.addoption('--log',
action='store',
default=False,
@ -90,6 +96,45 @@ def pytest_addoption(parser):
default=600,
help='Running time in seconds')
# running tests using appium docker instance
parser.addoption('--docker',
action='store',
default=False,
help='Are you using the appium docker container to run the tests?')
parser.addoption('--docker_shared_volume',
action='store',
default=None,
help='Path to a directory with .apk that will be shared with docker instance. Test reports will be also saved there')
parser.addoption('--device_ip',
action='store',
default=None,
help='Android device IP address used for battery tests')
parser.addoption('--bugreport',
action='store',
default=False,
help='Should generate bugreport for each test?')
parser.addoption('--stats_db_host',
action='store',
default=None,
help='Host address for device stats database')
parser.addoption('--stats_db_port',
action='store',
default=8086,
help='Port for device stats db')
parser.addoption('--stats_db_username',
action='store',
default=None,
help='Username for device stats db')
parser.addoption('--stats_db_password',
action='store',
default=None,
help='Password for device stats db')
parser.addoption('--stats_db_database',
action='store',
default='example9',
help='Database name for device stats db')
def is_master(config):
return not hasattr(config, 'slaveinput')
@ -160,6 +205,26 @@ def pytest_runtest_makereport(item, call):
current_test.testruns[-1].error = error
if is_sauce_env:
update_sauce_jobs(current_test.name, current_test.testruns[-1].jobs, report.passed)
if pytest.config.getoption('docker'):
device_stats = appium_container.get_device_stats()
if pytest.config.getoption('bugreport'):
appium_container.generate_bugreport(item.name)
build_name = pytest.config.getoption('apk')
# Find type of tests that are run on the device
if 'battery_consumption' in item.keywords._markers:
test_group = 'battery_consumption'
else:
test_group = None
device_stats_db = DeviceStatsDB(
item.config.getoption('stats_db_host'),
item.config.getoption('stats_db_port'),
item.config.getoption('stats_db_username'),
item.config.getoption('stats_db_password'),
item.config.getoption('stats_db_database'),
)
device_stats_db.save_stats(build_name, item.name, test_group, not report.failed, device_stats)
def update_sauce_jobs(test_name, job_ids, passed):

View File

@ -21,3 +21,5 @@ wallet_modal = pytest.mark.wallet_modal
sign_in = pytest.mark.sign_in
skip = pytest.mark.skip
logcat = pytest.mark.logcat
battery_consumption = pytest.mark.battery_consumption