def upgradeApp(self, apkName): """upgrade an App @param apkName: the .apk filename @return: integer (0 for success or 1 for failure) """ printLog("upgrading app from %s..." % apkName) return self.runAdbCmd('install', '-r ' + apkName)
def installApp(self, apkName): """install an App @param apkName: the .apk filename @return: integer (0 for success or 1 for failure) """ printLog("Installing app from %s..." % apkName) return self.runAdbCmd('install', apkName)
def do_compare(self, str_arg): """ compare a snapshot file with an expected image file(by default in PNG format). this usually follows a takesnapshot step in a testcase. if the snapshot file is identical with the expected target file, return True. otherwise return False. (Note: UI checking depends on various external factors, for example, different screen brightness value would make the snapshots different, leading to unexpected compare results. therefore comparing snapshots is no longer recommended. If you insist on automated pixel-level UI testcases, make sure you have these factors well managed) format: compare <live captured snapshot> <target prepared snapshot> e.g. 'setup' is a predefined directory to hold prepared snapshots, you may define your own directories. takesnapshot app_drawer_icon_on_screen.png compare app_drawer_icon_on_screen.png setup/designed_app_drawer_icon.png """ arg = validateString(str_arg) source, target = arg.split(' ', 1) if os.path.isfile(source): # Mar 27 @swang: if target file doesn't exist, copy source file to setup directory for later test # 2015-08-27: decided to go to fail path if not os.path.isfile(target): # copy(source, target) self.resultFlag = False raise ValueError('COMPARE FAILED: target file not found.') # if not self.__compareImage(source, target): if not filecmp.cmp(source, target): printLog(self.threadName + 'COMPARE FAILED: source file and target file DIFFER!', logging.WARNING) self.resultFlag = False else: self.resultFlag = False raise ValueError('COMPARE FAILED: source file not found.')
def getLaunchTime(fname): """ scan logcat file and retrieve activity launch time data 2015-09-01: refactored to remove the dependency to external shell script @param fname: the logcat filename @return: a list of activities and their launch time """ if not os.path.isfile(fname): return "" ALTList = [] LT_RE = re.compile("^I/ActivityManager\(\s*\d+\): Displayed {}/(\S+): \+(\S+)\s*$".format(APP_PKG_NAME)) try: with open(ADBLOG_FILE, "r") as fd: lines = filter(lambda x: not x.startswith("\n") and APP_PKG_NAME in x, fd.readlines()) for line in lines: # printLog('[getLaunchTime] current line: %s'% line) if LT_RE.match(line): activity, ltime = LT_RE.search(line).groups() ltime = Tester.__convertTime(ltime.rstrip("\n")) # Oct 23: changed method to get activity name and time # use ':' to split columns in get_ALT.sh and '+' to split # activity name and launch time ALTList.append((activity, ltime)) except Exception, e: printLog( "[getLaunchTime] Caught exception while writing launch time data to file: %s" % e.message, logging.ERROR )
def run(self): """ run tester to capture logcat, start App TestRunner instance, get launch time, filter exceptions in app log and generate test report @return: number of failed testcases @rtype: integer """ self.__reset() self.start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) # 2014-07-22, capture logcat in ASYNC mode printLog("Start capture adb logcat ...") # redirect to local drive would generate error: /system/bin/sh: can't create logcat.log: Read-only file system # using the android file system path would solve the problem # child = Popen(['adb', 'logcat', '>', '/data/data/%s' % ADBLOG_FILE]) child = Popen("adb logcat 2>&1 > %s" % ADBLOG_FILE, shell=True) # truncate file after 3 seconds to get rid of old logs time.sleep(3) self.shell.truncate_file(ADBLOG_FILE) try: tr = AppTestRunner(self.test_buildnum, self.testPool, self.device) tr.run() except (AssertionError, EnvironmentError), e: printLog("Failed to initialize test runner {}: \n{}".format(self.device.deviceId, e.message), logging.ERROR) return -1
def getDevice(self, sn='', make=''): """ get device by serial no and/or make @param sn: device serial no @param make: device make, e.g. Genymotion, Samsung etc. @return: the match device @rtype: TestDevice """ if len(self) == 0: # printLog('NO DEVICE FOUND. QUIT.', logging.ERROR) return None if len(self) == 1: return self[0] if len(sn) == 0 and len(make) == 0: printLog("More than one device is connected. Please unplug and leave just one.", logging.ERROR) return None for dc in self: print dc.make, make if len(make) > 0 and len(sn) == 0: if dc.make == make: # print 'make match' return dc elif len(sn) > 0 and len(make) == 0: if dc.deviceId == sn: return dc elif len(sn) > 0 and len(make) > 0: if dc.make == make and dc.deviceId == sn: return dc else: return dc return None
def do_takesnapshot(self, str_arg): """ take snapshot of the full screen and save it to the file with the given filename format: takesnapshot [filename] e.g. takesnapshot a1.png @param str_arg: filename (string) """ img = None fname = validateString(str_arg) try: # self.adbc.wake() printLog(self.threadName + 'taking snapshot (0,50,%d,%d) ...' % (self.scn_width, self.scn_height)) img = self.adbc.takeSnapshot(reconnect=True) # PIL code img = img.crop((0, 50, self.scn_width, self.scn_height)) img.save(fname, SNAPSHOT_IMAGE_FORMAT) # if self.scn_width>SNAPSHOT_WIDTH: # self.compressImage(fname) # os.remove(fname) # im.save(fname) printLog(self.threadName + 'snapshot saved as %s' % fname) except EnvironmentError: self.resultFlag = False if DEBUG: traceback.print_exc() finally: img = None
def __compareImage(self, file1, file2): """ compare two image files with specified difference tolerance percentage (2% by default) input: two file's name @return: True or False """ # arg=self.__validateString(str_arg) # file1, file2=arg.split(' ', 1) try: img1 = Image.open(file1) img2 = Image.open(file2) if img1.size != img2.size: return False by1 = img1.tobytes() by2 = img2.tobytes() # format r,g,b,255,r,g,b,255, 3 bytes = 1 point, 255=separator, total 4 bytes l = len(by1) / 4 # total points and same points tp = 0 sp = 0 for j in range(l): i = j * 4 tp += 1 if by1[i] == by2[i] and by1[i + 1] == by2[i + 1] and by1[i + 2] == by2[i + 2]: sp += 1 # max to 2% diff allowed if tp * 0.98 > sp: return False else: return True except Exception, e: printLog(self.threadName + "Exception in __compareImage: %s" % e.message, logging.ERROR) traceback.print_exc() return False
def __getChildView(self, parentId, childSeq): """ get the child view of the given parent view @param parentId: the parent view id @param childSeq: the node path of the child view to the parent view @return: the child view (View) """ # child_view = None # str_getChildView = "self.vc.findViewById('" + parentId + "')" # for index in childSeq: # str_getChildView += ('.children[' + str(index) + ']') # printLog(self.threadName + "executing child_view=%s" % str_getChildView) # exec 'child_view=' + str_getChildView # return child_view pv = self.vc.findViewById(parentId) if not pv: # printLog(self.threadName + '[__getChildView] could not find parent view %s' % parentId, logging.DEBUG) return None for index in childSeq: if DEBUG: printLog(self.threadName + '[__getChildView] searching child view: %s[%s]' % (pv.getId(), index), logging.DEBUG) cv = pv.children[int(index)] if cv: if DEBUG: printLog(self.threadName + '[__getChildView] found child view: %s' % cv.getId(), logging.DEBUG) pv = cv else: # printLog(self.threadName + '[__getChildView] could not find child of %s' % pv.getId(), logging.DEBUG) return None return pv
def __clickChildView(self, parentId, childSeq, offset=(0, 0)): """ 2015-05-06: included the offset parameter so that the tool can click on the right view center position when the target view does not occupy the full screen. the default offset would be {x:0, y:48} where 48 is the vertical offset caused by android status bar 2015-05-26: adjusted the default offset to (0,0) """ child_view = self.__getChildView(parentId, childSeq) if child_view: # 2014/09/25: the returned point Y coordinate does not include the status bar height # using getAbsolutePositionOfView cannot solve this issue point = child_view.getCenter() if DEBUG: printLog(self.threadName + 'AbsoluteCenterOfView: ' + str(child_view.getCenter()), logging.DEBUG) printLog(self.threadName + 'AbsolutePositionOfView: ' + str(child_view.getXY()), logging.DEBUG) # printLog(child_view.marginTop) # printLog(child_view.paddingTop) # printLog(child_view.getXY()) printLog(self.threadName + self.__getRootViewPosition(child_view), logging.DEBUG) printLog(self.threadName + '[__clickChildView] clicking device at (%d, %d) ...' % ( point[0] + offset[0], point[1] + offset[1]), logging.DEBUG) self.adbc.touch(point[0] + offset[0], point[1] + offset[1], "DOWN_AND_UP") self.resultFlag = True else: printLog(self.threadName + '[__clickChildView] view not found.', logging.ERROR) self.resultFlag = False
def do_startApp(self, str_arg=""): """ launch the App """ printLog(self.threadName + "[running command 'startApp %s']" % str_arg) self.startApp(APP_LAUNCH_ACTIVITY) self.do_sleep("5")
def fromSuite(suite): """ the factory to read the testcases included in the test suite file, and build a list of C{TestCase} @param suite: test suite name (String) @return: a testcase pool @raise: ValueError """ ts_Filename = suite + EXT_TEST_SUITE tc_list = [] try: propfile_path = path.abspath('') + sep + 'ts' + sep + ts_Filename printLog('Reading testcase from file %s...' % propfile_path, logging.DEBUG) with open(propfile_path) as fd: content = filter(lambda x: not x.startswith('#') and not x.startswith('\n'), fd.readlines()) # print content test_name_list = map(lambda x: [x.split(':', 1)[0].strip(), x.split(':', 1)[1].strip()], content) # print testlist for test in test_name_list: printLog('adding testcase %s to pool...' % test[0], logging.DEBUG) tc_filepath = TC_DIR + '/' + test[0] + EXT_TEST_CASE # dirName, tcName=scptfilename.split('/') try: # with open(scptfilename) as sf: # lines = map(lambda x: x.strip().strip('\t'), sf.readlines()) tc_list.append(TestCase.fromFile(tc_filepath, tc_name=test[0], desc=test[1])) except IOError, e: # printLog("Error open/read file %s: %s" % (scptfilename, e.message), logging.ERROR) raise EnvironmentError("Error open/read file '%s'" % tc_filepath) except ValueError: raise EnvironmentError("Missing or bad section value in '%s', Please make sure " "@TITLE, @DESC, @SETUP, @VALIDATION and @TEARDOWN are included." % tc_filepath)
def __compressImage(self, img_file): im = Image.open(img_file) printLog(self.threadName + 'compressing snapshot %s...' % img_file) ratio = float(SNAPSHOT_WIDTH) / im.size[0] height = int(im.size[1] * ratio) printLog(self.threadName + "new image size: %d*%d" % (SNAPSHOT_WIDTH, height)) os.remove(img_file) im.resize((SNAPSHOT_WIDTH, height), Image.BILINEAR).save(img_file)
def __init__(self, testcase_list): """ the constructor @param testcase_list: list of C{TestCase} (list) """ list.__init__([]) self.extend(testcase_list) printLog('%d testcase read...' % len(self))
def __dumpview(self): """ dump current screen UI layout within given time limit. added to catch the alarm exception so that main thread won't quit (2015-08-11) """ try: with time_limit(DUMP_TIMEOUT): self.vc.dump() except TimeoutException: printLog(self.threadName + "Timed out! Dump view failed.", logging.ERROR)
def releaseDevice(self, device_id): """ set device to idel state @param device_id: the device serial number """ printLog("Releasing device %s ..." % device_id, logging.INFO) for dc in self: if dc.deviceId == device_id: dc.idle = True break
def restartAdbServer(self): """ restart the ADB server running on host machine @return: 0 for success and other for failure """ printLog('Stopping adb-server...', logging.INFO) if self.runAdbCmd('kill-server', '') == 0: printLog('Starting adb-server...', logging.INFO) return self.runAdbCmd('start-server', '') else: return 1
def __init__(self): list.__init__([]) """ the list of C{TestDevice} """ for device_id in getDeviceIdList(): device = TestDevice(device_id) printLog("adding device '%s %s (%s, %s, %s)' to pool..." % (device.make, device.model, device.deviceId, device.androidVersion, device.operator), logging.DEBUG) self.append(device) printLog("Found %d device(s)." % len(self), logging.INFO)
def do_check(self, str_arg): """ check id existency(not visibility! sometimes a view does not show up, e.g. widgets in the other launcher page), if text is given, check if it is identical with what shows on the screen. 2015-08-26: merged the checkchild method with this one. format: check <id>[(child path id sequence)] [text] Note: DO NOT INCLUDE SPACE IN THE CHILD PATH e.g. check id/title_text Personalize Homescreen e.g. check id/parent(4,3,2,2) my text is text @param str_arg: it takes two formats: 1. The target id, and text to be compared (optional) 2. The unique parent id, the path from parent to target child view, the target text (optional) @return boolean """ # printLog(self.threadName + "[running command 'check %s']" % str_arg) str_arg = validateString(str_arg) # parse the args args = str_arg.split(' ', 1) viewId = args[0].strip() if len(args) < 2: target_text = '' else: target_text = args[1] try: # get the target view tv = self.__getView(viewId) if tv: if DEBUG: printLog(self.threadName + 'Found target view %s.' % viewId, logging.DEBUG) if len(target_text) > 0: # get element text, and compare it with the given text value # tmpView=self.vc.findViewWithText(ret[1].strip()) tempText = tv.getText() printLog(self.threadName + '[Text on screen: %s, expected text: %s]' % (tempText, target_text), logging.DEBUG) if tempText == target_text: # printLog('CHECK PASS! Text match.', logging.DEBUG) self.resultFlag = True else: printLog(self.threadName + 'CHECK FAILED! Text not match!', logging.ERROR) self.resultFlag = False else: printLog(self.threadName + 'Target view %s not found.' % viewId, logging.ERROR) self.resultFlag = False except Exception, e: self.resultFlag = False printLog(self.threadName + 'Exception in do_check: %s' % e.message, logging.ERROR) if DEBUG: traceback.print_exc()
def do_type(self, str_arg): """ type the given string in the screen e.g. type I'm OK @param str_arg: the text to type (string) @return: result (boolean) """ try: self.adbc.type(validateString(str_arg)) except Exception, e: printLog(self.threadName + 'TYPE FAILED: %s' % e.message) self.resultFlag = False
def scanExceptionInLog(file_name): """ scan the specified file for exceptions, and save the result to a string (2014-07-16) @param file_name: the filename @return: exception (string) """ # scan and report any exception found in given file file_path = os.path.basename(file_name) printLog('[scanExceptionInLog] Scanning file %s for exceptions...' % file_path, logging.INFO) # todo: remove dependency on grep for possible support on windows later on return check_output(r'grep -n "Exception" {}'.format(file_path), shell=True) # cmdList = ["grep", "-n", "ERROR", "-B", "1", "-A", "3", path] # return Popen(cmdList, stdout=PIPE).communicate()[0]
def __getChildrenCount(self, rootId): """ get the child count of the given root view 2015-05-26: added to support removing all notifications @param rootId: root view id @return: the children count (int) """ root = self.vc.findViewById(rootId) if root: return len(root.children()) else: printLog(self.threadName + '[__getChildrenCount] parent view not found.', logging.ERROR) return 0
def do_testMonkeyTest(self, str_arg): """ run Monkey test for the given count times (default is 500) e.g. testMonkeyTest 5000 @param str_arg: count times """ if len(str_arg) == 0: count = 500 else: count = validateDigit(str_arg) printLog(self.threadName + "run monkey test.") self.resultFlag = self.runMonkeyTest(APP_PKG_NAME, count) return self.resultFlag
def pullFile(self, src, tgt='.'): """copy a file from the source path on the device to the host machine @param src: the source file path (string) @param tgt: the target file path (string) @return: boolean (True for success or False for failure) """ if tgt == '.': tgt = './' + os.path.basename(src) self.runAdbCmd('pull', '%s %s' % (src, tgt)) if os.path.isfile(tgt): return 0 else: printLog("[pullFile] Failed to get file '%s' via adb." % src, logging.ERROR) return 1
def do_checkTextInclusion(self, str_arg): """ check if the given text is included in the message of the specified resource id. (2015-07-22) e.g. checkTextInclusion id/icon_descriptive_text icon to see your Zones. """ # printLog(self.threadName + "[running command 'checkTextInclusion %s']" % str_arg) arg = validateString(str_arg) ret = arg.split(' ', 1) if len(ret) == 1: self.resultFlag = False raise ValueError('Lacking argument of checkTextInclusion.') if iDevice.dump_view: self.__dumpview() try: viewId = ret[0].strip() printLog("finding view by id %s ..." % viewId, logging.DEBUG) tmpView = self.vc.findViewById(viewId) if not tmpView: self.resultFlag = False printLog(self.threadName + '%s is not visible.' % viewId, logging.ERROR) return else: # get element text, and compare it with the given text value tempText = tmpView.getText() printLog(self.threadName + '[text on screen: %s]' % tempText) if not ret[1].strip() in tempText.strip(): self.resultFlag = False except Exception, e: self.resultFlag = False printLog(self.threadName + 'Exception in do_check: %s' % e.message, logging.ERROR)
def do_upgradeApp(self, str_arg=""): """ sample implementation: replace app with new version @return: result (boolean) """ target = path.join( LOCAL_BUILD_ROOT_PATH, APP_VERSION, "{}-{}.apk".format(PRODUCT_SHORT_NAME, self.test_buildnum) ) if path.isfile(target): self.resultFlag = self.upgradeApp(target) else: printLog(self.threadName + "CANNOT ACCESS/FIND BUILD FILE at %s" % target, logging.ERROR) self.resultFlag = False return self.resultFlag
def getBuild(self): """ sample implementation: get build file from build server and place it in the directory specified in AppTestRunner.installApp(), which is path.join(LOCAL_BUILD_ROOT_PATH, APP_VERSION, "{}-{}.apk".format(PRODUCT_SHORT_NAME, buildnum)) @return: result (boolean) """ result = False if self.test_buildnum < 0: return True if self.test_buildnum == 0: self.test_buildnum = AppTestRunner.getLatestBuildNumber() if self.test_buildnum == 0: printLog('[getBuild] invalid build number specified or build location not accessible.', logging.ERROR) return result local_target = path.join(LOCAL_BUILD_ROOT_PATH, APP_VERSION, "{}-{}.apk".format(PRODUCT_SHORT_NAME, self.test_buildnum)) if not path.isfile(local_target): # the build file is not found locally, download it from remote build server remote_target = path.join(BUILD_ROOT_PATH, APP_VERSION, PRODUCT_NAME + '-' + str(self.test_buildnum), BUILD_FILENAME) printLog('[getBuild] Downloading build %s from %s...' % (str(self.test_buildnum), remote_target), logging.INFO) try: Shell().runShellCmd('cp {} {}'.format(remote_target, local_target)) if path.isfile(local_target): printLog('[getBuild] Build %s is downloaded.' % str(self.test_buildnum), logging.INFO) result = True except IOError, e: printLog('[getBuild] Build %s download failed: %s' % e.message, logging.ERROR)
def __connect(self): """ connect to device or exit @raise: EnvironmentError """ if DEBUG: printLog(self.threadName + '[iDevice] Connecting to device %s...' % self.deviceId, logging.INFO) try: self.adbc, serialno = ViewClient.connectToDeviceOrExit(verbose=DEBUG, serialno=self.deviceId) # print 'Connected.' if self.adbc is None: printLog(self.threadName + '[iDevice] Failed to connect to Device %s...' % self.deviceId, logging.ERROR) return # get phone's screen resolution, once connected, it is fixed self.scn_width = int(self.adbc.display['width']) self.scn_height = int(self.adbc.display['height']) # printLog(self.threadName + "[iDevice] Device %s's screen resolution is: %d * %d" % ( # self.deviceId, self.scn_width, self.scn_height), logging.DEBUG) self.adbc.wake() # printLog(self.threadName + '[iDevice] Creating View Client... ', logging.DEBUG) self.vc = ViewClient(self.adbc, serialno, autodump=False, forceviewserveruse=True) if DEBUG: printLog(self.threadName + '[iDevice] Device %s connected.' % self.deviceId, logging.INFO) # self.resultFlag = True except Exception, e: printLog(self.threadName + "[iDevice] CANNOT connect to device %s. Please check the USB cable and " "reconnect the device." % self.deviceId, logging.ERROR) # if DEBUG: # traceback.print_exc() raise EnvironmentError(e.message)
def do_clickchild(self, str_arg): """ [obsoleted] This method is merged with 'click'. Kept here for backward compatibility. click a certain child for one unique ID use it while there are multiple same name ID, but there is one unique root parent format: clickchild <root node ID> <child branch id list> e.g. clickchild id/root (0,1) 2015-08-27: merged into click method, kept here for backward compatibility 2015-08-11: using AVC will no longer require including the offset parameter --------------------------------------------------------------------------------- Below instruction is for Monkeyrunner which is DEPRECATED. 2015-05-06: updated to include the offset parameter so that the tool can click on the right view center position format: clickchild <root node ID> <child branch id list> <root node relative position offset to screen> e.g. clickchild id/root (0,1) (18,338) """ # printLog(self.threadName + "[running 'clickchild %s']" % str_arg) # arg validation arg = validateString(str_arg) if iDevice.dump_view: self.__dumpview() try: # to avoid ' ' two spaces case # suppose string like: id/button1 (5,2,3,3,3) (0,50) # i = arg.index(' ') # ids = arg[0:i] # arg = arg[i + 1:].strip() # seqs = arg[1:-1].split(',') arg_list = arg.split(' ') if len(arg_list) == 2: printLog(self.threadName + 'do_clickChild: using default offset.') node_id, seqs = arg_list self.__clickChildView(node_id, seqs[1:-1].split(',')) elif len(arg_list) == 3: # node_id, seqs, offset = arg_list # self.__clickChildView(node_id, seqs[1:-1].split(','), self.__getPointXY(offset.strip())) raise ValueError("using AVC will NO LONGER require including the offset parameter.") else: raise ValueError('missing argument.') except ValueError: printLog(self.threadName + 'do_clickChild: click failed', logging.ERROR) traceback.print_exc() self.resultFlag = False time.sleep(1)
def getDeviceIdList(): devices = Shell().getShellCmdOutput(r"adb devices") # |awk -F'\t' '{print $1}' print devices deviceIdList = [] # filter(lambda x: len(x) > 0, devices.split('\n')) # .split('\t',1)[0] connected_RE = re.compile("^\S+\t*device$") for line in devices.split('\n'): # if deviceIdList[i].strip() == 'List of devices attached': # print 'list start' # deviceIdList = deviceIdList[i+1:] # break if connected_RE.match(line): deviceIdList.append(line.split('\t', 1)[0]) if len(deviceIdList) > 0: printLog('List of devices attached: \n' + str(deviceIdList)) return deviceIdList