Skip to content

Commit

Permalink
Added support for blackbox Android testing
Browse files Browse the repository at this point in the history
Fixed #10
  • Loading branch information
pmeenan committed Mar 23, 2017
1 parent 4121a30 commit 011b3e7
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 31 deletions.
32 changes: 32 additions & 0 deletions internal/adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, options):
self.version = None
self.kernel = None
self.short_version = None
self.last_bytes_rx = 0
self.known_apps = {
'com.motorola.ccc.ota': {},
'com.google.android.apps.docs': {},
Expand Down Expand Up @@ -371,3 +372,34 @@ def is_device_ready(self):
logging.info("Device not ready, network not responding")
is_ready = False
return is_ready

def get_bytes_rx(self):
"""Get the incremental bytes received across all non-loopback interfaces"""
bytes_rx = 0
out = self.shell(['cat', '/proc/net/dev'], silent=True)
if out is not None:
for line in out.splitlines():
match = re.search(r'^\s*(\w+):\s+(\d+)', line)
if match:
interface = match.group(1)
if interface != 'lo':
bytes_rx += int(match.group(2))
delta = bytes_rx - self.last_bytes_rx
self.last_bytes_rx = bytes_rx
return delta

def get_video_size(self):
"""Get the current size of the video file"""
size = 0
out = self.shell(['ls', '-l', '/data/local/tmp/wpt_video.mp4'], silent=True)
match = re.search(r'[^\d]+\s+(\d+) \d+', out)
if match:
size = int(match.group(1))
return size

def screenshot(self, dest_file):
"""Capture a png screenshot of the device"""
device_path = '/data/local/tmp/wpt_screenshot.png'
self.shell(['rm', '/data/local/tmp/wpt_screenshot.png'], silent=True)
self.shell(['screencap', '-p', device_path])
self.adb(['pull', device_path, dest_file])
60 changes: 47 additions & 13 deletions internal/android_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@
import shutil
import subprocess
import time
import ujson as json

class AndroidBrowser(object):
"""Android Browser base"""
def __init__(self, adb, job, options, config):
def __init__(self, adb, options, job, config):
self.adb = adb
self.job = job
self.options = options
self.config = config
self.video_processing = None
self.tcpdump_processing = None
self.video_enabled = bool(job['video'])
self.tcpdump_enabled = bool('tcpdump' in job and job['tcpdump'])
self.tcpdump_file = None
if self.config['type'] == 'blackbox':
self.tcpdump_enabled = True
self.video_enabled = True

def prepare(self, job, task):
"""Prepare the browser and OS"""
Expand Down Expand Up @@ -85,13 +92,13 @@ def on_start_recording(self, task):
task['page_data']['browserVersion'] = out[separator + 1:].strip()
if self.tcpdump_enabled:
self.adb.start_tcpdump()
if self.job['video']:
if self.video_enabled:
self.adb.start_screenrecord()
if self.tcpdump_enabled or self.job['video']:
if self.tcpdump_enabled or self.video_enabled:
time.sleep(0.5)

def on_stop_recording(self, task):
"""Notification that we are about to start an operation that needs to be recorded"""
"""Notification that we are done with an operation that needs to be recorded"""
if self.tcpdump_enabled:
logging.debug("Stopping tcpdump")
tcpdump = os.path.join(task['dir'], task['prefix']) + '.cap'
Expand All @@ -103,8 +110,17 @@ def on_stop_recording(self, task):
shutil.copyfileobj(f_in, f_out)
if os.path.isfile(pcap_out):
os.remove(tcpdump)
self.tcpdump_file = pcap_out
path_base = os.path.join(task['dir'], task['prefix'])
slices_file = path_base + '_pcap_slices.json.gz'
pcap_parser = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'support', "pcap-parser.py")
cmd = ['python', pcap_parser, '--json', '-i', pcap_out, '-d', slices_file]
logging.debug(' '.join(cmd))
self.tcpdump_processing = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

if self.job['video']:
if self.video_enabled:
logging.debug("Stopping video capture")
task['video_file'] = os.path.join(task['dir'], task['prefix']) + '_video.mp4'
self.adb.stop_screenrecord(task['video_file'])
Expand All @@ -121,14 +137,15 @@ def on_stop_recording(self, task):
task['current_step'])
histograms = os.path.join(task['dir'], filename)
visualmetrics = os.path.join(support_path, "visualmetrics.py")
self.video_processing = subprocess.Popen(['python', visualmetrics, '-vvvv',
'-i', task['video_file'],
'-d', video_path,
'--force', '--quality',
'{0:d}'.format(self.job['iq']),
'--viewport', '--orange',
'--maxframes', '50',
'--histogram', histograms])
args = ['python', visualmetrics, '-vvvv', '-i', task['video_file'],
'-d', video_path, '--force', '--quality', '{0:d}'.format(self.job['iq']),
'--viewport', '--maxframes', '50', '--histogram', histograms]
if 'videoFlags' in self.config:
args.extend(self.config['videoFlags'])
else:
args.append('--orange')
logging.debug(' '.join(args))
self.video_processing = subprocess.Popen(args)

def wait_for_processing(self, task):
"""Wait for any background processing threads to finish"""
Expand All @@ -140,3 +157,20 @@ def wait_for_processing(self, task):
os.remove(task['video_file'])
except Exception:
pass
if self.tcpdump_processing is not None:
try:
stdout, _ = self.tcpdump_processing.communicate()
if stdout is not None:
result = json.loads(stdout)
if result:
if 'in' in result:
task['page_data']['pcapBytesIn'] = result['in']
if 'out' in result:
task['page_data']['pcapBytesOut'] = result['out']
if 'in_dup' in result:
task['page_data']['pcapBytesInDup'] = result['in_dup']
if 'tcpdump' not in self.job or not self.job['tcpdump']:
if self.tcpdump_file is not None:
os.remove(self.tcpdump_file)
except Exception:
pass
28 changes: 14 additions & 14 deletions internal/android_browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,56 +22,56 @@
"UC Mini": {
"package": "com.uc.browser.en",
"activity": "com.uc.browser.ActivityBrowser",
"videoFlags": ["--findstart", 25, "--notification"],
"videoFlags": ["--findstart", "25", "--notification"],
"directories": ["cache", "databases", "files", "app_webview", "user", "wa"],
"startupDelay": 10000,
"startupDelay": 10,
"type": "blackbox"
},
"UC Browser": {
"package": "com.UCMobile.intl",
"activity": "com.UCMobile.main.UCMobile",
"videoFlags": ["--findstart", 25, "--notification"],
"videoFlags": ["--findstart", "25", "--notification"],
"directories": ["cache", "databases", "files", "app_webview", "crash", "temp", "user", "wa"],
"startupDelay": 15000,
"startupDelay": 15,
"type": "blackbox"
},
"Opera Mini": {
"package": "com.opera.mini.native",
"activity": "com.opera.mini.android.Browser",
"videoFlags": ["--findstart", 75, "--notification", "--renderignore", 40,
"videoFlags": ["--findstart", "75", "--notification", "--renderignore", 40,
"--forceblank"],
"directories": ["cache", "databases", "files", "app_opera", "app_webview"],
"startupDelay": 10000,
"startupDelay": 10,
"type": "blackbox"
},
"Samsung Browser": {
"package": "com.sec.android.app.sbrowser",
"activity": ".SBrowserMainActivity",
"videoFlags": ["--findstart", 25, "--notification"],
"videoFlags": ["--findstart", "25", "--notification"],
"directories": ["cache", "databases", "files", "app_sbrowser", "code_cache"],
"startupDelay": 10000,
"startupDelay": 10,
"type": "blackbox"
},
"QQ Browser": {
"package": "com.tencent.mtt.intl",
"activity": "com.tencent.mtt.SplashActivity",
"videoFlags": ["--findstart", 25, "--notification"],
"videoFlags": ["--findstart", "25", "--notification"],
"directories": ["cache", "databases", "files", "app_appcache", "app_databases", "app_databases_tmp", "app_x5_share"],
"startupDelay": 10000,
"startupDelay": 10,
"type": "blackbox"
},
"Firefox": {
"package": "org.mozilla.firefox",
"activity": ".App",
"videoFlags": ["--findstart", 25, "--notification"],
"startupDelay": 10000,
"videoFlags": ["--findstart", "25", "--notification"],
"startupDelay": 10,
"type": "blackbox"
},
"Firefox Beta": {
"package": "org.mozilla.firefox_beta",
"activity": ".App",
"videoFlags": ["--findstart", 25, "--notification"],
"startupDelay": 10000,
"videoFlags": ["--findstart", "25", "--notification"],
"startupDelay": 10,
"type": "blackbox"
}
}
145 changes: 145 additions & 0 deletions internal/blackbox_android.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright 2017 Google Inc. All rights reserved.
# Use of this source code is governed by the Apache 2.0 license that can be
# found in the LICENSE file.
"""Chrome browser on Android"""
import logging
import os
import subprocess
import time
import monotonic
from .android_browser import AndroidBrowser

START_PAGE = 'data:text/html,'

class BlackBoxAndroid(AndroidBrowser):
"""Chrome browser on Android"""
def __init__(self, adb, config, options, job):
self.adb = adb
self.task = None
self.options = options
self.config = dict(config)
# pull in the APK info for the browser
if 'apk_info' in job and 'packages' in job['apk_info'] and \
self.config['package'] in job['apk_info']['packages']:
apk_info = job['apk_info']['packages'][self.config['package']]
self.config['apk_url'] = apk_info['apk_url']
self.config['md5'] = apk_info['md5'].lower()
AndroidBrowser.__init__(self, adb, options, job, self.config)

def prepare(self, job, task):
"""Prepare the profile/OS for the browser"""
self.task = task
AndroidBrowser.prepare(self, job, task)
if not task['cached']:
self.clear_profile(task)

def launch(self, job, task):
"""Launch the browser"""
# launch the browser
activity = '{0}/{1}'.format(self.config['package'], self.config['activity'])
self.adb.shell(['am', 'start', '-n', activity, '-a',
'android.intent.action.VIEW', '-d', START_PAGE])
if 'startupDelay' in self.config:
time.sleep(self.config['startupDelay'])
self.wait_for_network_idle()

def run_task(self, task):
"""Skip anything that isn't a navigate command"""
logging.debug("Running test")
end_time = monotonic.monotonic() + task['time_limit']
task['log_data'] = True
task['current_step'] = 1
task['prefix'] = task['task_prefix']
task['video_subdirectory'] = task['task_video_prefix']
if self.job['video']:
task['video_directories'].append(task['video_subdirectory'])
task['step_name'] = 'Navigate'
self.on_start_recording(task)
while len(task['script']) and monotonic.monotonic() < end_time:
command = task['script'].pop(0)
if command['command'] == 'navigate':
activity = '{0}/{1}'.format(self.config['package'], self.config['activity'])
self.adb.shell(['am', 'start', '-n', activity, '-a',
'android.intent.action.VIEW', '-d', command['target']])
self.wait_for_page_load()
self.on_stop_recording(task)
self.wait_for_processing(task)

def stop(self, job, task):
"""Stop testing"""
# kill the browser
self.adb.shell(['am', 'force-stop', self.config['package']])

def on_stop_recording(self, task):
"""Collect post-test data"""
AndroidBrowser.on_stop_recording(self, task)
png_file = os.path.join(task['dir'], task['prefix'] + '_screen.png')
self.adb.screenshot(png_file)
task['page_data']['result'] = 0
task['page_data']['visualTest'] = 1
if os.path.isfile(png_file):
if not self.job['pngss']:
jpeg_file = os.path.join(task['dir'], task['prefix'] + '_screen.jpg')
command = 'convert -quality {0:d} "{1}" "{2}"'.format(
self.job['iq'], png_file, jpeg_file)
logging.debug(command)
subprocess.call(command, shell=True)
if os.path.isfile(jpeg_file):
try:
os.remove(png_file)
except Exception:
pass

def clear_profile(self, _):
"""Clear the browser profile"""
if 'clearProfile' in self.config and self.config['clearProfile']:
self.adb.shell(['pm', 'clear', self.config['package']])
elif 'directories' in self.config:
remove = ' /data/data/{0}/'.format(self.config['package']).join(
self.config['directories'])
if len(remove):
self.adb.su('rm -r' + remove)

def wait_for_network_idle(self):
"""Wait for 5 one-second intervals that receive less than 1KB"""
logging.debug('Waiting for network idle')
end_time = monotonic.monotonic() + 60
self.adb.get_bytes_rx()
idle_count = 0
while idle_count < 5 and monotonic.monotonic() < end_time:
time.sleep(1)
bytes_rx = self.adb.get_bytes_rx()
logging.debug("Bytes received: %d", bytes_rx)
if bytes_rx > 1000:
idle_count = 0
else:
idle_count += 1

def wait_for_page_load(self):
"""Once the video starts growing, wait for it to stop"""
logging.debug('Waiting for the page to load')
# Wait for the video to start (up to 30 seconds)
end_startup = monotonic.monotonic() + 30
end_time = monotonic.monotonic() + self.task['time_limit']
last_size = self.adb.get_video_size()
video_started = False
while not video_started and monotonic.monotonic() < end_startup:
time.sleep(5)
video_size = self.adb.get_video_size()
delta = video_size - last_size
logging.debug('Video Size: %d bytes (+ %d)', video_size, delta)
last_size = video_size
if delta > 50000:
video_started = True
# Wait for the activity to stop
video_idle_count = 0
while video_idle_count <= 3 and monotonic.monotonic() < end_time:
time.sleep(5)
video_size = self.adb.get_video_size()
delta = video_size - last_size
logging.debug('Video Size: %d bytes (+ %d)', video_size, delta)
last_size = video_size
if delta > 10000:
video_idle_count = 0
else:
video_idle_count += 1
3 changes: 3 additions & 0 deletions internal/browsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ def get_browser(self, name, job):
if config['type'] == 'chrome':
from .chrome_android import ChromeAndroid
browser = ChromeAndroid(self.adb, config, self.options, job)
if config['type'] == 'blackbox':
from .blackbox_android import BlackBoxAndroid
browser = BlackBoxAndroid(self.adb, config, self.options, job)
elif 'type' in job and job['type'] == 'traceroute':
from .traceroute import Traceroute
browser = Traceroute(self.options, job)
Expand Down
Loading

0 comments on commit 011b3e7

Please sign in to comment.