1 # ***** BEGIN LICENSE BLOCK *****
2 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 # The contents of this file are subject to the Mozilla Public License Version
5 # 1.1 (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
7 # http://www.mozilla.org/MPL/
9 # Software distributed under the License is distributed on an "AS IS" basis,
10 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 # for the specific language governing rights and limitations under the
14 # The Original Code is Test Automation Framework.
16 # The Initial Developer of the Original Code is Joel Maher.
18 # Portions created by the Initial Developer are Copyright (C) 2009
19 # the Initial Developer. All Rights Reserved.
22 # Joel Maher <joel.maher@gmail.com> (Original Developer)
24 # Alternatively, the contents of this file may be used under the terms of
25 # either the GNU General Public License Version 2 or later (the "GPL"), or
26 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 # in which case the provisions of the GPL or the LGPL are applicable instead
28 # of those above. If you wish to allow use of your version of this file only
29 # under the terms of either the GPL or the LGPL, and not to allow others to
30 # use your version of this file under the terms of the MPL, indicate your
31 # decision by deleting the provisions above and replace them with the notice
32 # and other provisions required by the GPL or the LGPL. If you do not delete
33 # the provisions above, a recipient may use your version of this file under
34 # the terms of any one of the MPL, the GPL or the LGPL.
36 # ***** END LICENSE BLOCK *****
44 from threading
import Thread
48 class FileError(Exception):
49 " Signifies an error which occurs while doing a file operation."
51 def __init__(self
, msg
= ''):
63 tempRoot
= os
.getcwd()
66 prompt_regex
= '.*' + base_prompt
+ prompt_sep
68 def __init__(self
, host
, port
= 27020):
74 def sendCMD(self
, cmdline
, newline
= True, sleep
= 0):
75 promptre
= re
.compile(self
.prompt_regex
+ '$')
77 # TODO: any commands that don't output anything and quit need to match this RE
78 pushre
= re
.compile('^push .*$')
82 if (self
._sock
== None):
85 print "reconnecting socket"
86 self
._sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
91 print "unable to create socket"
95 self
._sock
.connect((self
.host
, int(self
.port
)))
101 if (self
.debug
>= 2):
102 print "unable to connect socket"
106 if (cmd
== 'quit'): break
107 if newline
: cmd
+= '\r\n'
111 if (self
.debug
>= 4): print "send cmd: " + str(cmd
)
118 if (pushre
.match(cmd
) or cmd
== 'rebt'):
120 elif noQuit
== False:
121 time
.sleep(int(sleep
))
123 while (found
== False):
124 if (self
.debug
>= 4): print "recv'ing..."
127 temp
= self
._sock
.recv(1024)
133 lines
= temp
.split('\n')
135 if (promptre
.match(line
)):
139 time
.sleep(int(sleep
))
152 # take a data blob and strip instances of the prompt '$>\x00'
153 def stripPrompt(self
, data
):
154 promptre
= re
.compile(self
.prompt_regex
+ '.*')
156 lines
= data
.split('\n')
159 while (promptre
.match(line
)):
160 pieces
= line
.split(self
.prompt_sep
)
161 index
= pieces
.index('$>')
163 line
= self
.prompt_sep
.join(pieces
)
168 return '\n'.join(retVal
)
171 def pushFile(self
, localname
, destname
):
172 if (self
.debug
>= 2): print "in push file with: " + localname
+ ", and: " + destname
173 if (self
.validateFile(destname
, localname
) == True):
174 if (self
.debug
>= 2): print "files are validated"
177 if self
.mkDirs(destname
) == None:
178 print "unable to make dirs: " + destname
181 if (self
.debug
>= 2): print "sending: push " + destname
183 # sleep 5 seconds / MB
184 filesize
= os
.path
.getsize(localname
)
185 sleepsize
= 1024 * 1024
186 sleepTime
= (int(filesize
/ sleepsize
) * 5) + 2
187 f
= open(localname
, 'rb')
190 retVal
= self
.sendCMD(['push ' + destname
+ '\r\n', data
], newline
= False, sleep
= sleepTime
)
192 if (self
.debug
>= 2): print "Error in sendCMD, not validating push"
195 if (self
.validateFile(destname
, localname
) == False):
196 if (self
.debug
>= 2): print "file did not copy as expected"
201 def mkDir(self
, name
):
202 return self
.sendCMD(['mkdr ' + name
])
205 # make directory structure on the device
206 def mkDirs(self
, filename
):
207 parts
= filename
.split('/')
210 if (part
== parts
[-1]): break
213 if (self
.mkDir(name
) == None):
214 print "failed making directory: " + str(name
)
218 # push localDir from host to remoteDir on the device
219 def pushDir(self
, localDir
, remoteDir
):
220 if (self
.debug
>= 2): print "pushing directory: " + localDir
+ " to " + remoteDir
221 for root
, dirs
, files
in os
.walk(localDir
):
222 parts
= root
.split(localDir
)
224 remoteRoot
= remoteDir
+ '/' + parts
[1]
225 remoteName
= remoteRoot
+ '/' + file
226 if (parts
[1] == ""): remoteRoot
= remoteDir
227 if (self
.pushFile(os
.path
.join(root
, file), remoteName
) == None):
229 self
.removeFile(remoteName
)
231 if (self
.pushFile(os
.path
.join(root
, file), remoteName
) == None):
236 def dirExists(self
, dirname
):
237 match
= ".*" + dirname
+ "$"
238 dirre
= re
.compile(match
)
239 data
= self
.sendCMD(['cd ' + dirname
, 'cwd', 'quit'], sleep
= 1)
242 retVal
= self
.stripPrompt(data
)
243 data
= retVal
.split('\n')
251 # Because we always have / style paths we make this a lot easier with some
253 def fileExists(self
, filepath
):
254 s
= filepath
.split('/')
255 containingpath
= '/'.join(s
[:-1])
256 listfiles
= self
.listFiles(containingpath
)
262 # list files on the device, requires cd to directory first
263 def listFiles(self
, rootdir
):
264 if (self
.dirExists(rootdir
) == False):
266 data
= self
.sendCMD(['cd ' + rootdir
, 'ls', 'quit'], sleep
=1)
269 retVal
= self
.stripPrompt(data
)
270 return retVal
.split('\n')
272 def removeFile(self
, filename
):
273 if (self
.debug
>= 2): print "removing file: " + filename
274 return self
.sendCMD(['rm ' + filename
, 'quit'])
277 # does a recursive delete of directory on the device: rm -Rf remoteDir
278 def removeDir(self
, remoteDir
):
279 self
.sendCMD(['rmdr ' + remoteDir
], sleep
= 5)
282 def getProcessList(self
):
283 data
= self
.sendCMD(['ps'], sleep
= 3)
287 retVal
= self
.stripPrompt(data
)
288 lines
= retVal
.split('\n')
291 if (line
.strip() != ''):
292 pidproc
= line
.strip().split()
293 if (len(pidproc
) == 2):
294 files
+= [[pidproc
[0], pidproc
[1]]]
295 elif (len(pidproc
) == 3):
296 #android returns <userID> <procID> <procName>
297 files
+= [[pidproc
[1], pidproc
[2], pidproc
[0]]]
300 def getMemInfo(self
):
301 data
= self
.sendCMD(['mems', 'quit'])
304 retVal
= self
.stripPrompt(data
)
305 # TODO: this is hardcoded for now
306 fhandle
= open("memlog.txt", 'a')
308 fhandle
.write(retVal
)
311 def fireProcess(self
, appname
):
312 if (self
.debug
>= 2): print "FIRE PROC: '" + appname
+ "'"
314 if (self
.processExist(appname
) != ''):
315 print "WARNING: process %s appears to be running already\n" % appname
317 self
.sendCMD(['exec ' + appname
])
319 #NOTE: we sleep for 30 seconds to allow the application to startup
322 self
.process
= self
.processExist(appname
)
323 if (self
.debug
>= 4): print "got pid: " + str(self
.process
) + " for process: " + str(appname
)
325 def launchProcess(self
, cmd
, outputFile
= "process.txt", cwd
= ''):
326 if (outputFile
== "process.txt"):
327 outputFile
= self
.getDeviceRoot() + '/' + "process.txt"
329 cmdline
= subprocess
.list2cmdline(cmd
)
330 self
.fireProcess(cmdline
+ " > " + outputFile
)
335 #hardcoded: sleep interval of 5 seconds, timeout of 10 minutes
336 def communicate(self
, process
, timeout
= 600):
341 while total_time
< timeout
:
343 if (not self
.poll(process
)):
346 total_time
+= interval
348 if (timed_out
== True):
351 return [self
.getFile(process
, "temp.txt"), None]
354 def poll(self
, process
):
356 if (self
.processExist(process
) == None):
365 # iterates process list and returns pid if exists, otherwise ''
366 def processExist(self
, appname
):
369 pieces
= appname
.split(' ')
370 parts
= pieces
[0].split('/')
372 procre
= re
.compile('.*' + app
+ '.*')
374 procList
= self
.getProcessList()
375 if (procList
== None):
378 for proc
in procList
:
379 if (procre
.match(proc
[1])):
385 def killProcess(self
, appname
):
386 if (self
.sendCMD(['kill ' + appname
]) == None):
391 def getTempDir(self
):
393 data
= self
.sendCMD(['tmpd', 'quit'])
396 return self
.stripPrompt(data
).strip('\n')
399 # copy file from device (remoteFile) to host (localFile)
400 def getFile(self
, remoteFile
, localFile
= ''):
402 localFile
= os
.path
.join(self
.tempRoot
, "temp.txt")
404 promptre
= re
.compile(self
.prompt_regex
+ '.*')
405 data
= self
.sendCMD(['cat ' + remoteFile
, 'quit'], sleep
= 5)
408 retVal
= self
.stripPrompt(data
)
409 fhandle
= open(localFile
, 'wb')
410 fhandle
.write(retVal
)
415 # copy directory structure from device (remoteDir) to host (localDir)
416 def getDirectory(self
, remoteDir
, localDir
):
417 if (self
.debug
>= 2): print "getting files in '" + remoteDir
+ "'"
418 filelist
= self
.listFiles(remoteDir
)
419 if (filelist
== None):
421 if (self
.debug
>= 3): print filelist
422 if not os
.path
.exists(localDir
):
423 os
.makedirs(localDir
)
425 # TODO: is this a comprehensive file regex?
426 isFile
= re
.compile('^([a-zA-Z0-9_\-\. ]+)\.([a-zA-Z0-9]+)$')
428 if (isFile
.match(f
)):
429 if (self
.getFile(remoteDir
+ '/' + f
, os
.path
.join(localDir
, f
)) == None):
432 if (self
.getDirectory(remoteDir
+ '/' + f
, os
.path
.join(localDir
, f
)) == None):
436 # true/false check if the two files have the same md5 sum
437 def validateFile(self
, remoteFile
, localFile
):
438 remoteHash
= self
.getRemoteHash(remoteFile
)
439 localHash
= self
.getLocalHash(localFile
)
441 if (remoteHash
== localHash
):
447 # return the md5 sum of a remote file
448 def getRemoteHash(self
, filename
):
449 data
= self
.sendCMD(['hash ' + filename
, 'quit'], sleep
= 1)
452 retVal
= self
.stripPrompt(data
)
454 retVal
= retVal
.strip('\n')
455 if (self
.debug
>= 3): print "remote hash returned: '" + retVal
+ "'"
459 # return the md5 sum of a file on the host
460 def getLocalHash(self
, filename
):
461 file = open(filename
, 'rb')
466 mdsum
= hashlib
.md5()
471 data
= file.read(1024)
477 hexval
= mdsum
.hexdigest()
478 if (self
.debug
>= 3): print "local hash returned: '" + hexval
+ "'"
481 # Gets the device root for the testing area on the device
482 # For all devices we will use / type slashes and depend on the device-agent
483 # to sort those out. The agent will return us the device location where we
484 # should store things, we will then create our /tests structure relative to
485 # that returned path.
486 # Structure on the device is as follows:
488 # /<fennec>|<firefox> --> approot
493 def getDeviceRoot(self
):
494 if (not self
.deviceRoot
):
495 data
= self
.sendCMD(['testroot'], sleep
= 1)
498 self
.deviceRoot
= self
.stripPrompt(data
).strip('\n') + '/tests'
500 if (not self
.dirExists(self
.deviceRoot
)):
501 self
.mkDir(self
.deviceRoot
)
503 return self
.deviceRoot
505 # Either we will have /tests/fennec or /tests/firefox but we will never have
506 # both. Return the one that exists
507 def getAppRoot(self
):
508 if (self
.dirExists(self
.getDeviceRoot() + '/fennec')):
509 return self
.getDeviceRoot() + '/fennec'
510 elif (self
.dirExists(self
.getDeviceRoot() + '/firefox')):
511 return self
.getDeviceRoot() + '/firefox'
513 return 'org.mozilla.fennec'
515 # Gets the directory location on the device for a specific test type
516 # Type is one of: xpcshell|reftest|mochitest
517 def getTestRoot(self
, type):
518 if (re
.search('xpcshell', type, re
.I
)):
519 self
.testRoot
= self
.getDeviceRoot() + '/xpcshell'
520 elif (re
.search('?(i)reftest', type)):
521 self
.testRoot
= self
.getDeviceRoot() + '/reftest'
522 elif (re
.search('?(i)mochitest', type)):
523 self
.testRoot
= self
.getDeviceRoot() + '/mochitest'
526 # Sends a specific process ID a signal code and action.
527 # For Example: SIGINT and SIGDFL to process x
528 def signal(self
, processID
, signalType
, signalAction
):
529 # currently not implemented in device agent - todo
532 # Get a return code from process ending -- needs support on device-agent
534 def getReturnCode(self
, processID
):
535 # todo make this real
538 def unpackFile(self
, filename
):
540 parts
= filename
.split('/')
542 if self
.fileExists(filename
):
543 dir = '/'.join(parts
[:-1])
544 elif self
.fileExists('/' + filename
):
546 elif self
.fileExists(self
.getDeviceRoot() + '/' + filename
):
547 dir = self
.getDeviceRoot() + '/' + filename
551 return self
.sendCMD(['cd ' + dir, 'unzp ' + filename
])
554 def reboot(self
, wait
= False):
555 self
.sendCMD(['rebt'])
562 if self
.listFiles('/') != None:
564 print "sleeping another 10 seconds"
566 timeout
= timeout
- 10
571 # validate localDir from host to remoteDir on the device
572 def validateDir(self
, localDir
, remoteDir
):
573 if (self
.debug
>= 2): print "validating directory: " + localDir
+ " to " + remoteDir
574 for root
, dirs
, files
in os
.walk(localDir
):
575 parts
= root
.split(localDir
)
577 remoteRoot
= remoteDir
+ '/' + parts
[1]
578 remoteRoot
= remoteRoot
.replace('/', '/')
579 if (parts
[1] == ""): remoteRoot
= remoteDir
580 remoteName
= remoteRoot
+ '/' + file
581 if (self
.validateFile(remoteName
, os
.path
.join(root
, file)) <> True):
585 #TODO: make this simpler by doing a single directive at a time
586 # Returns information about the device:
587 # Directive indicates the information you want to get, your choices are:
588 # os - name of the os
589 # id - unique id of the device
590 # uptime - uptime of the device
591 # systime - system time of the device
592 # screen - screen resolution
593 # memory - memory stats
594 # process - list of running processes (same as ps)
595 # disk - total, free, available bytes on disk
596 # power - power status (charge, battery temp)
598 def getInfo(self
, directive
):
600 if (directive
in ('os','id','uptime','systime','screen','memory','process',
602 data
= self
.sendCMD(['info ' + directive
, 'quit'], sleep
= 1)
605 data
= self
.sendCMD(['info', 'quit'], sleep
= 1)
610 data
= self
.stripPrompt(data
)
614 result
[directive
] = data
.split('\n')
615 for i
in range(len(result
[directive
])):
616 if (len(result
[directive
][i
]) != 0):
617 result
[directive
][i
] = result
[directive
][i
].strip()
619 # Get rid of any empty attributes
620 result
[directive
].remove('')
623 lines
= data
.split('\n')
624 result
['id'] = lines
[0]
625 result
['os'] = lines
[1]
626 result
['systime'] = lines
[2]
627 result
['uptime'] = lines
[3]
628 result
['screen'] = lines
[4]
629 result
['memory'] = lines
[5]
630 if (lines
[6] == 'Power status'):
633 tmp
.append(line
[7 + i
])
634 result
['power'] = tmp
637 # Linenum is the line where the process list begins
639 for j
in range(len(lines
) - linenum
):
640 if (lines
[j
+ linenum
].strip() != ''):
641 procline
= lines
[j
+ linenum
].split('\t')
643 if len(procline
) == 2:
644 tmp
.append([procline
[0], procline
[1]])
645 elif len(procline
) == 3:
646 # Android has <userid> <procid> <procname>
647 # We put the userid to achieve a common format
648 tmp
.append([procline
[1], procline
[2], procline
[0]])
649 result
['process'] = tmp