Using the dm utility to interact with Android or FirefoxOS devices

I promised a few people I’d blog about this, so here you go. :)

To help with the business of making Android or FirefoxOS devices do our bidding, Mozilla Automation & Tools developed a Python library called mozdevice which allows you to control these devices either using the Android Debug Bridge protocol (which is actually not Android specific: FirefoxOS devices use it too) or the System Under Test protocol (a Mozilla-specific thing).

Anyone familiar with debugging these devices is doubtless familiar with adb, which provides a command line interface that allows you to push/pull files, run a shell, etc. To help test our python code (as well as expand the scope of what’s possible on the command line), I created a similar utility a few months ago called “dm” which provides a command-line interface to the aforementioned mozdevice code. It’s shipped as part of mozdevice, and testing it out is pretty simple if you have virtualenv installed:

virtualenv mozdevice
cd mozdevice
./bin/pip install mozdevice
source bin/activate
dm --help

I generally use this utility for two things:

  1. Testing out mozdevice code. For example, today we discovered an (unfortunate) bug in devicemanagerADB’s killProcess routine. It was easy to verify both the problem and my fix did what I expected by starting my custom build of fennec and running this command:

    dm --package-name org.mozilla.fennec_wlach killapp org.mozilla.fennec_wlach
    

    (yes, it’s a bit unfortunate that this bug occurred in the first place: devicemanagerADB really needs unit tests)

  2. Day-to-day menial tasks, like getting device info/status, capturing screenshots, etc. You can get a full list of what this utility is capable of by running –help. E.g.:

    (mozbase)wlach@eideticker:~/src/eideticker$ dm --help
    Usage: dm [options]  []
    
    device commands:
      info [os|id|uptime|systime|screen|memory|processes] - get
          information on a specified aspect of the device (if no argument
          given, print all available information)
      install  - push this package file to the device and install it
      killapp  - kills any processes with a particular name
          on device
      launchapp     - launches
          application on device
      ls  - list files on device
      ps  - get information on running processes on device
      pull  [remote] - copy file/dir from device
      push   - copy file/dir to device
      rm  - remove file from device
      rmdir  - recursively remove directory from device
      screencap  - capture screenshot of device in action
      shell  - run shell command on device
    
    Options:
      -h, --help            show this help message and exit
      -v, --verbose         Verbose output from DeviceManager
      --host=HOST           Device hostname (only if using TCP/IP)
      -p PORT, --port=PORT  Custom device port (if using SUTAgent or adb-over-tcp)
      -m DMTYPE, --dmtype=DMTYPE
                            DeviceManager type (adb or sut, defaults to adb)
      -d HWID, --hwid=HWID  HWID
      --package-name=PACKAGENAME
                            Packagename (if using DeviceManagerADB)
    

    Before you ask, yes, it’s technically possible to do much of this with the original adb utility. But (1) some of our internal stuff can’t use adb a variety of reasons and (2) some of the tasks above (e.g. launching an app, getting a screenshot) involve considerably more typing with adb than with dm. So it’s still a win.

Happy command-lining!

Catching problems early with python

Just a few quick notes on how to avoid a class of errors I’ve been seeing in Mozilla’s automation over the last year. Since python interprets code dynamically, it’s pretty easy for problems like undefined variables to slip through, especially if they’re in a codepath that isn’t frequently tested. The most recent example I found was in some cleanup-after-error code for remote mochitest/reftest, which tried to call “self.cleanup” from a standalone method.

def main():
      ...
      try:
        dm.recordLogcat()
        retVal = mochitest.runTests(options)
        logcat = dm.getLogcat()
      except:
        print "TEST-UNEXPECTED-FAIL | %s | Exception caught while running tests." % sys.exc_info()[1]
        mochitest.stopWebServer(options)
        mochitest.stopWebSocketServer(options)
        try:
            self.cleanup(None, options)
        except:
            pass

testing/mochitest/runtestsremote.py

We’re calling cleanup as if it were a class variable, but we’re not inside any class! It’s easy to see what will happen if you try to run some similar code from the python console:

>>> self.cleanup()
Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'self' is not defined

However, because we’re in a blanket try…except, we will never see an error. The cleanup code will never be called, instead the exception is immediately caught and subsequently ignored. Probably not the end of the world in this case (there are other parts of our mobile automation which will perform the same cleanup later), but it’s easy to imagine where this would be a more serious problem.

There’s two very easy ways to help stop errors like this before they hit our code:

  1. Try to avoid using a blanket try…except. In addition to catching legitimate problems which we want to ignore (in the remote case for example, devicemanager exceptions), it also catches (and thus obscures) things like syntax, name, or type errors. Instead, try just catching the specific exception you’re looking for. For example, we might rewrite the case above as:

    try:
      mochitest.cleanup(None, options)
    except devicemanager.DMError:
      print "WARNING: Device error while cleaning up"
    
  2. pyflakes, pyflakes, pyflakes. Pyflakes is a fantastic tool for analyzing your python code for common problems. It’s kind of analagous to jslint, for those of you familiar with that. Here’s what happens when we run pyflakes against the offending code:

    wlach@eideticker:~/src/mozilla-central$ pyflakes testing/mochitest/runtestsremote.py 
    testing/mochitest/runtestsremote.py:7: 'time' imported but unused
    testing/mochitest/runtestsremote.py:481: undefined name 'self'
    testing/mochitest/runtestsremote.py:500: undefined name 'self'
    

    I’ve found pyflakes to be an indispensable part of my workflow. I generally run it after making any substantial change to a python file, and certainly before pushing anything to be consumed by others.

Ultimately there’s no substitute for actually thoroughly testing your code, no matter what language you’re using. But using the right techniques and tools can certainly make your life easier. :)

[ for those wondering, a fix for the issue mentioned in this post is part of bug 801652 ]