| # 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] |