blob: f5712093f8205478ffb16f38bef99cbba136a0e2 [file] [log] [blame]
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This script provides needed information about the app under test.
"""
from __future__ import absolute_import
import json
import logging
import re
import subprocess
# An OS X tool to read and modify values inside of a plist structure.
_PLIST_BUDDY = '/usr/libexec/PlistBuddy'
# The unique string that identifies the iOS app under test.
_IOS_BUNDLE_ID = 'CFBundleIdentifier'
# The name of the iOS app under test i.e: Chrome Canary.
_IOS_APP_NAME = 'CFBundleDisplayName'
# The version name of the iOS app under test.
_IOS_APP_VERSION = 'CFBundleVersion'
# A regex pattern for finding the app package name from an Android apk.
_ANDROID_APP_PACKAGE_PATTERN = r'package: name=\'([a-zA-Z0-9\_\.]+)\''
# A regex pattern for finding EN app name from an Android apk.
_ANDROID_EN_APP_NAME_PATTERN = r'application-label-en.*:\'([\w\s]+)\''
# A regex pattern for finding the app name from an Android apk.
_ANDROID_APP_NAME_PATTERN = r'application: label=\'([\w\s]+)\''
# A regex pattern for finding the app version name from an Android apk.
_ANDROID_APP_VERSION_NAME_PATTERN = r'versionName=\'([a-zA-Z0-9\_\.\(\)\s]+)\''
# pylint: disable=unused-argument
def _default(self, obj):
return getattr(obj.__class__, 'to_json', _default.default)(obj)
class AppInfoError(Exception):
"""Franky error class for problems with App_info.
"""
pass
class AppInfo(object):
"""Provides the necessary info from an app.
Attributes:
app_intent: (string) The intent for launching an app in Android.
id: (string) The Bundle id for iOS & app package for Android.
name: (string) The app name i.e. Chrome Canary.
version: (string) The app version i.e. 55.0.2853.3.
app_group: (string) A group based on app id for grouping test reports.
"""
def __init__(self, app_intent=None, app_id=None, name=None,
version=None, app_group=None):
"""AppInfo constructor."""
# Based on https://stackoverflow.com/a/18561055
# need to monkey-patch custom class(not a standard type) to have json-dump.
_default.default = json.JSONEncoder.default # Save unmodified default.
json.JSONEncoder.default = _default # Replace it.
self.app_intent = app_intent
if not app_id:
raise AppInfoError('App_id (bundle id or package) not specified')
self.id = app_id
self.name = name
self.version = version
self.app_group = app_group
def to_json(self):
"""JSON-serializer."""
return {
'id': self.id,
'app_intent': self.app_intent,
'name': self.name,
'version': self.version,
'app_group': self.app_group
}
def GetIOSAppInformation(app_path):
"""Retrieves information from the iOS app under test.
Args:
app_path: A string for the absolute path to the iOS app.
Returns:
A tuple of AppInfo.
"""
app_intent = None
bundle_id = _ExecuteCommand(
_GetPlistBuddyCommand(app_path, _IOS_BUNDLE_ID)).strip()
name = _ExecuteCommand(
_GetPlistBuddyCommand(app_path, _IOS_APP_NAME)).strip()
version = _ExecuteCommand(
_GetPlistBuddyCommand(app_path, _IOS_APP_VERSION)).strip()
return AppInfo(app_intent, bundle_id, name, version, None)
def GetAndroidAppInformation(app_path, app_intent):
"""Retrieves information from the android app under test.
Args:
app_path: A string for the absolute path to the android app.
app_intent: A string representing the launch intent of an app.
Returns:
A tuple of AppInfo.
"""
aapt_output = _ExecuteCommand(['aapt', 'dump', 'badging', app_path])
if not aapt_output:
raise subprocess.CalledProcessError(1, 'No aapt output')
package = 'UNKNOWN_PACKAGE'
app_name = 'UNKNOWN_NAME'
app_version = 'UNKNOWN_VERSION'
m = re.search(_ANDROID_APP_PACKAGE_PATTERN, aapt_output)
if m:
package = m.group(1)
else:
logging.warning('Unable to get package name.')
m = re.search(_ANDROID_EN_APP_NAME_PATTERN, aapt_output)
if m:
app_name = m.group(1)
else:
m = re.search(_ANDROID_APP_NAME_PATTERN, aapt_output)
if m:
app_name = m.group(1)
else:
logging.warning('Unable to get app name.')
m = re.search(_ANDROID_APP_VERSION_NAME_PATTERN, aapt_output)
if m:
app_version = m.group(1)
else:
logging.warning('Unable to get app version.')
app_group = None if 'chrome' in package else package
return AppInfo(app_intent, package, app_name, app_version, app_group)
def _ExecuteCommand(command):
"""Executes a command as a subprocess.
Args:
command: A list of string to execute in a form that is recognized by
subprocess.Popen.
Returns:
A list of strings corresponding the output generated by running the command.
"""
process = subprocess.Popen(command, stdout=subprocess.PIPE)
output, _ = process.communicate()
return output.decode('utf-8')
def _GetPlistBuddyCommand(app_path, info_identifier):
"""Returns the PlistBuddy command for a specific app info.
Args:
app_path: A string for the absolute path to the iOS app.
info_identifier: A string for the name of the app info requested.
Returns:
A string for the app info PlistBuddy command.
"""
return [_PLIST_BUDDY, '-c', 'Print %s' % info_identifier,
'%s/Info.plist' % app_path]