~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to build/automation.py.in

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#
2
 
# This Source Code Form is subject to the terms of the Mozilla Public
3
 
# License, v. 2.0. If a copy of the MPL was not distributed with this
4
 
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
 
 
6
 
from __future__ import with_statement
7
 
import codecs
8
 
from datetime import datetime, timedelta
9
 
import itertools
10
 
import logging
11
 
import os
12
 
import re
13
 
import select
14
 
import shutil
15
 
import signal
16
 
import subprocess
17
 
import sys
18
 
import threading
19
 
import tempfile
20
 
import sqlite3
21
 
from string import Template
22
 
 
23
 
SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
24
 
sys.path.insert(0, SCRIPT_DIR)
25
 
import automationutils
26
 
 
27
 
_DEFAULT_WEB_SERVER = "127.0.0.1"
28
 
_DEFAULT_HTTP_PORT = 8888
29
 
_DEFAULT_SSL_PORT = 4443
30
 
_DEFAULT_WEBSOCKET_PORT = 9988
31
 
 
32
 
#expand _DIST_BIN = __XPC_BIN_PATH__
33
 
#expand _IS_WIN32 = len("__WIN32__") != 0
34
 
#expand _IS_MAC = __IS_MAC__ != 0
35
 
#expand _IS_LINUX = __IS_LINUX__ != 0
36
 
#ifdef IS_CYGWIN
37
 
#expand _IS_CYGWIN = __IS_CYGWIN__ == 1
38
 
#else
39
 
_IS_CYGWIN = False
40
 
#endif
41
 
#expand _IS_CAMINO = __IS_CAMINO__ != 0
42
 
#expand _BIN_SUFFIX = __BIN_SUFFIX__
43
 
#expand _PERL = __PERL__
44
 
 
45
 
#expand _DEFAULT_APP = "./" + __BROWSER_PATH__
46
 
#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
47
 
#expand _IS_TEST_BUILD = __IS_TEST_BUILD__
48
 
#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
49
 
#expand _CRASHREPORTER = __CRASHREPORTER__ == 1
50
 
 
51
 
 
52
 
if _IS_WIN32:
53
 
  import ctypes, ctypes.wintypes, time, msvcrt
54
 
else:
55
 
  import errno
56
 
 
57
 
 
58
 
# We use the logging system here primarily because it'll handle multiple
59
 
# threads, which is needed to process the output of the server and application
60
 
# processes simultaneously.
61
 
_log = logging.getLogger()
62
 
handler = logging.StreamHandler(sys.stdout)
63
 
_log.setLevel(logging.INFO)
64
 
_log.addHandler(handler)
65
 
 
66
 
 
67
 
#################
68
 
# PROFILE SETUP #
69
 
#################
70
 
 
71
 
class SyntaxError(Exception):
72
 
  "Signifies a syntax error on a particular line in server-locations.txt."
73
 
 
74
 
  def __init__(self, lineno, msg = None):
75
 
    self.lineno = lineno
76
 
    self.msg = msg
77
 
 
78
 
  def __str__(self):
79
 
    s = "Syntax error on line " + str(self.lineno)
80
 
    if self.msg:
81
 
      s += ": %s." % self.msg
82
 
    else:
83
 
      s += "."
84
 
    return s
85
 
 
86
 
 
87
 
class Location:
88
 
  "Represents a location line in server-locations.txt."
89
 
 
90
 
  def __init__(self, scheme, host, port, options):
91
 
    self.scheme = scheme
92
 
    self.host = host
93
 
    self.port = port
94
 
    self.options = options
95
 
 
96
 
class Automation(object):
97
 
  """
98
 
  Runs the browser from a script, and provides useful utilities
99
 
  for setting up the browser environment.
100
 
  """
101
 
 
102
 
  DIST_BIN = _DIST_BIN
103
 
  IS_WIN32 = _IS_WIN32
104
 
  IS_MAC = _IS_MAC
105
 
  IS_LINUX = _IS_LINUX
106
 
  IS_CYGWIN = _IS_CYGWIN
107
 
  IS_CAMINO = _IS_CAMINO
108
 
  BIN_SUFFIX = _BIN_SUFFIX
109
 
  PERL = _PERL
110
 
 
111
 
  UNIXISH = not IS_WIN32 and not IS_MAC
112
 
 
113
 
  DEFAULT_APP = _DEFAULT_APP
114
 
  CERTS_SRC_DIR = _CERTS_SRC_DIR
115
 
  IS_TEST_BUILD = _IS_TEST_BUILD
116
 
  IS_DEBUG_BUILD = _IS_DEBUG_BUILD
117
 
  CRASHREPORTER = _CRASHREPORTER
118
 
 
119
 
  # timeout, in seconds
120
 
  DEFAULT_TIMEOUT = 60.0
121
 
  DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
122
 
  DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
123
 
  DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
124
 
  DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
125
 
 
126
 
  def __init__(self):
127
 
    self.log = _log
128
 
    self.lastTestSeen = "automation.py"
129
 
    self.haveDumpedScreen = False
130
 
 
131
 
  def setServerInfo(self, 
132
 
                    webServer = _DEFAULT_WEB_SERVER, 
133
 
                    httpPort = _DEFAULT_HTTP_PORT, 
134
 
                    sslPort = _DEFAULT_SSL_PORT,
135
 
                    webSocketPort = _DEFAULT_WEBSOCKET_PORT):
136
 
    self.webServer = webServer
137
 
    self.httpPort = httpPort
138
 
    self.sslPort = sslPort
139
 
    self.webSocketPort = webSocketPort
140
 
 
141
 
  @property
142
 
  def __all__(self):
143
 
    return [
144
 
           "UNIXISH",
145
 
           "IS_WIN32",
146
 
           "IS_MAC",
147
 
           "log",
148
 
           "runApp",
149
 
           "Process",
150
 
           "addCommonOptions",
151
 
           "initializeProfile",
152
 
           "DIST_BIN",
153
 
           "DEFAULT_APP",
154
 
           "CERTS_SRC_DIR",
155
 
           "environment",
156
 
           "IS_TEST_BUILD",
157
 
           "IS_DEBUG_BUILD",
158
 
           "DEFAULT_TIMEOUT",
159
 
          ]
160
 
 
161
 
  class Process(subprocess.Popen):
162
 
    """
163
 
    Represents our view of a subprocess.
164
 
    It adds a kill() method which allows it to be stopped explicitly.
165
 
    """
166
 
 
167
 
    def __init__(self,
168
 
                 args,
169
 
                 bufsize=0,
170
 
                 executable=None,
171
 
                 stdin=None,
172
 
                 stdout=None,
173
 
                 stderr=None,
174
 
                 preexec_fn=None,
175
 
                 close_fds=False,
176
 
                 shell=False,
177
 
                 cwd=None,
178
 
                 env=None,
179
 
                 universal_newlines=False,
180
 
                 startupinfo=None,
181
 
                 creationflags=0):
182
 
      args = automationutils.wrapCommand(args)
183
 
      print "args: %s" % args
184
 
      subprocess.Popen.__init__(self, args, bufsize, executable,
185
 
                                stdin, stdout, stderr,
186
 
                                preexec_fn, close_fds,
187
 
                                shell, cwd, env,
188
 
                                universal_newlines, startupinfo, creationflags)
189
 
      self.log = _log
190
 
 
191
 
    def kill(self):
192
 
      if Automation().IS_WIN32:
193
 
        import platform
194
 
        pid = "%i" % self.pid
195
 
        if platform.release() == "2000":
196
 
          # Windows 2000 needs 'kill.exe' from the 
197
 
          #'Windows 2000 Resource Kit tools'. (See bug 475455.)
198
 
          try:
199
 
            subprocess.Popen(["kill", "-f", pid]).wait()
200
 
          except:
201
 
            self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
202
 
        else:
203
 
          # Windows XP and later.
204
 
          subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
205
 
      else:
206
 
        os.kill(self.pid, signal.SIGKILL)
207
 
 
208
 
  def readLocations(self, locationsPath = "server-locations.txt"):
209
 
    """
210
 
    Reads the locations at which the Mochitest HTTP server is available from
211
 
    server-locations.txt.
212
 
    """
213
 
 
214
 
    locationFile = codecs.open(locationsPath, "r", "UTF-8")
215
 
 
216
 
    # Perhaps more detail than necessary, but it's the easiest way to make sure
217
 
    # we get exactly the format we want.  See server-locations.txt for the exact
218
 
    # format guaranteed here.
219
 
    lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
220
 
                      r"://"
221
 
                      r"(?P<host>"
222
 
                        r"\d+\.\d+\.\d+\.\d+"
223
 
                        r"|"
224
 
                        r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
225
 
                        r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
226
 
                      r")"
227
 
                      r":"
228
 
                      r"(?P<port>\d+)"
229
 
                      r"(?:"
230
 
                      r"\s+"
231
 
                      r"(?P<options>\S+(?:,\S+)*)"
232
 
                      r")?$")
233
 
    locations = []
234
 
    lineno = 0
235
 
    seenPrimary = False
236
 
    for line in locationFile:
237
 
      lineno += 1
238
 
      if line.startswith("#") or line == "\n":
239
 
        continue
240
 
      
241
 
      match = lineRe.match(line)
242
 
      if not match:
243
 
        raise SyntaxError(lineno)
244
 
 
245
 
      options = match.group("options")
246
 
      if options:
247
 
        options = options.split(",")
248
 
        if "primary" in options:
249
 
          if seenPrimary:
250
 
            raise SyntaxError(lineno, "multiple primary locations")
251
 
          seenPrimary = True
252
 
      else:
253
 
        options = []
254
 
 
255
 
      locations.append(Location(match.group("scheme"), match.group("host"),
256
 
                                match.group("port"), options))
257
 
 
258
 
    if not seenPrimary:
259
 
      raise SyntaxError(lineno + 1, "missing primary location")
260
 
 
261
 
    return locations
262
 
 
263
 
  def setupPermissionsDatabase(self, profileDir, permissions):
264
 
    # Open database and create table
265
 
    permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
266
 
    cursor = permDB.cursor();
267
 
 
268
 
    cursor.execute("PRAGMA user_version=3");
269
 
 
270
 
    # SQL copied from nsPermissionManager.cpp
271
 
    cursor.execute("""CREATE TABLE moz_hosts (
272
 
       id INTEGER PRIMARY KEY,
273
 
       host TEXT,
274
 
       type TEXT,
275
 
       permission INTEGER,
276
 
       expireType INTEGER,
277
 
       expireTime INTEGER,
278
 
       appId INTEGER,
279
 
       isInBrowserElement INTEGER)""")
280
 
 
281
 
    # Insert desired permissions
282
 
    c = 0
283
 
    for perm in permissions.keys():
284
 
      for host,allow in permissions[perm]:
285
 
        c += 1
286
 
        cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0, 0, 0)",
287
 
                       (c, host, perm, 1 if allow else 2))
288
 
 
289
 
    # Commit and close
290
 
    permDB.commit()
291
 
    cursor.close()
292
 
 
293
 
  def setupTestApps(self, profileDir, apps):
294
 
    webappJSONTemplate = Template(""""$name": {
295
 
"origin": "$origin",
296
 
"installOrigin": "$origin",
297
 
"receipt": null,
298
 
"installTime": 132333986000,
299
 
"manifestURL": "$manifestURL",
300
 
"localId": $localId
301
 
}""")
302
 
 
303
 
    manifestTemplate = Template("""{
304
 
  "name": "$name",
305
 
  "description": "$description",
306
 
  "launch_path": "/",
307
 
  "developer": {
308
 
    "name": "Mozilla",
309
 
    "url": "https://mozilla.org/"
310
 
  },
311
 
  "permissions": [
312
 
  ],
313
 
  "locales": {
314
 
    "en-US": {
315
 
      "name": "$name",
316
 
      "description": "$description"
317
 
    }
318
 
  },
319
 
  "default_locale": "en-US",
320
 
  "icons": {
321
 
  }
322
 
}
323
 
""")
324
 
 
325
 
    # Create webapps/webapps.json
326
 
    webappsDir = os.path.join(profileDir, "webapps")
327
 
    os.mkdir(webappsDir);
328
 
 
329
 
    webappsJSON = []
330
 
    for localId, app in enumerate(apps):
331
 
      app['localId'] = localId + 1 # Has to be 1..n
332
 
      webappsJSON.append(webappJSONTemplate.substitute(app))
333
 
    webappsJSON = '{\n' + ',\n'.join(webappsJSON) + '\n}\n'
334
 
 
335
 
    webappsJSONFile = open(os.path.join(webappsDir, "webapps.json"), "a")
336
 
    webappsJSONFile.write(webappsJSON)
337
 
    webappsJSONFile.close()
338
 
 
339
 
    # Create manifest file for each app.
340
 
    for app in apps:
341
 
      manifest = manifestTemplate.substitute(app)
342
 
 
343
 
      manifestDir = os.path.join(webappsDir, app['name'])
344
 
      os.mkdir(manifestDir)
345
 
 
346
 
      manifestFile = open(os.path.join(manifestDir, "manifest.webapp"), "a")
347
 
      manifestFile.write(manifest)
348
 
      manifestFile.close()
349
 
 
350
 
  def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
351
 
    " Sets up the standard testing profile."
352
 
 
353
 
    prefs = []
354
 
    # Start with a clean slate.
355
 
    shutil.rmtree(profileDir, True)
356
 
    os.mkdir(profileDir)
357
 
 
358
 
    # Set up permissions database
359
 
    locations = self.readLocations()
360
 
    self.setupPermissionsDatabase(profileDir,
361
 
      {'allowXULXBL':[(l.host, 'noxul' not in l.options) for l in locations]});
362
 
 
363
 
    part = """\
364
 
user_pref("social.skipLoadingProviders", true);
365
 
user_pref("browser.console.showInPanel", true);
366
 
user_pref("browser.dom.window.dump.enabled", true);
367
 
user_pref("browser.firstrun.show.localepicker", false);
368
 
user_pref("browser.firstrun.show.uidiscovery", false);
369
 
user_pref("browser.startup.page", 0); // use about:blank, not browser.startup.homepage
370
 
user_pref("browser.ui.layout.tablet", 0); // force tablet UI off
371
 
user_pref("dom.allow_scripts_to_close_windows", true);
372
 
user_pref("dom.disable_open_during_load", false);
373
 
user_pref("dom.max_script_run_time", 0); // no slow script dialogs
374
 
user_pref("hangmonitor.timeout", 0); // no hang monitor
375
 
user_pref("dom.max_chrome_script_run_time", 0);
376
 
user_pref("dom.popup_maximum", -1);
377
 
user_pref("dom.send_after_paint_to_content", true);
378
 
user_pref("dom.successive_dialog_time_limit", 0);
379
 
user_pref("signed.applets.codebase_principal_support", true);
380
 
user_pref("security.warn_submit_insecure", false);
381
 
user_pref("browser.shell.checkDefaultBrowser", false);
382
 
user_pref("shell.checkDefaultClient", false);
383
 
user_pref("browser.warnOnQuit", false);
384
 
user_pref("accessibility.typeaheadfind.autostart", false);
385
 
user_pref("javascript.options.showInConsole", true);
386
 
user_pref("devtools.errorconsole.enabled", true);
387
 
user_pref("layout.debug.enable_data_xbl", true);
388
 
user_pref("browser.EULA.override", true);
389
 
user_pref("javascript.options.jit_hardening", true);
390
 
user_pref("gfx.color_management.force_srgb", true);
391
 
user_pref("network.manage-offline-status", false);
392
 
user_pref("dom.min_background_timeout_value", 1000);
393
 
user_pref("test.mousescroll", true);
394
 
user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
395
 
user_pref("network.http.prompt-temp-redirect", false);
396
 
user_pref("media.cache_size", 100);
397
 
user_pref("security.warn_viewing_mixed", false);
398
 
user_pref("app.update.enabled", false);
399
 
user_pref("app.update.staging.enabled", false);
400
 
user_pref("browser.panorama.experienced_first_run", true); // Assume experienced
401
 
user_pref("dom.w3c_touch_events.enabled", true);
402
 
user_pref("toolkit.telemetry.prompted", 2);
403
 
// Existing tests assume there is no font size inflation.
404
 
user_pref("font.size.inflation.emPerLine", 0);
405
 
user_pref("font.size.inflation.minTwips", 0);
406
 
 
407
 
// Only load extensions from the application and user profile
408
 
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
409
 
user_pref("extensions.enabledScopes", 5);
410
 
// Disable metadata caching for installed add-ons by default
411
 
user_pref("extensions.getAddons.cache.enabled", false);
412
 
// Disable intalling any distribution add-ons
413
 
user_pref("extensions.installDistroAddons", false);
414
 
 
415
 
user_pref("extensions.testpilot.runStudies", false);
416
 
 
417
 
user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
418
 
user_pref("geo.wifi.testing", true);
419
 
user_pref("geo.ignore.location_filter", true);
420
 
 
421
 
user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
422
 
 
423
 
// Make url-classifier updates so rare that they won't affect tests
424
 
user_pref("urlclassifier.updateinterval", 172800);
425
 
// Point the url-classifier to the local testing server for fast failures
426
 
user_pref("browser.safebrowsing.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
427
 
user_pref("browser.safebrowsing.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
428
 
user_pref("browser.safebrowsing.updateURL", "http://%(server)s/safebrowsing-dummy/update");
429
 
// Point update checks to the local testing server for fast failures
430
 
user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
431
 
user_pref("extensions.update.background.url", "http://%(server)s/extensions-dummy/updateBackgroundURL");
432
 
user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
433
 
user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
434
 
// Make sure opening about:addons won't hit the network
435
 
user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
436
 
// Make sure AddonRepository won't hit the network
437
 
user_pref("extensions.getAddons.maxResults", 0);
438
 
user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/repositoryGetURL");
439
 
user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
440
 
user_pref("extensions.getAddons.search.browseURL", "http://%(server)s/extensions-dummy/repositoryBrowseURL");
441
 
user_pref("extensions.getAddons.search.url", "http://%(server)s/extensions-dummy/repositorySearchURL");
442
 
 
443
 
// Make enablePrivilege continue to work for test code. :-(
444
 
user_pref("security.enablePrivilege.enable_for_tests", true);
445
 
""" % { "server" : self.webServer + ":" + str(self.httpPort) }
446
 
    prefs.append(part)
447
 
 
448
 
    if useServerLocations:
449
 
      # We need to proxy every server but the primary one.
450
 
      origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
451
 
                for l in filter(lambda l: "primary" not in l.options, locations)]
452
 
      origins = ", ".join(origins)
453
 
 
454
 
      pacURL = """data:text/plain,
455
 
function FindProxyForURL(url, host)
456
 
{
457
 
  var origins = [%(origins)s];
458
 
  var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
459
 
                         '://' +
460
 
                         '(?:[^/@]*@)?' +
461
 
                         '(.*?)' +
462
 
                         '(?::(\\\\\\\\d+))?/');
463
 
  var matches = regex.exec(url);
464
 
  if (!matches)
465
 
    return 'DIRECT';
466
 
  var isHttp = matches[1] == 'http';
467
 
  var isHttps = matches[1] == 'https';
468
 
  var isWebSocket = matches[1] == 'ws';
469
 
  var isWebSocketSSL = matches[1] == 'wss';
470
 
  if (!matches[3])
471
 
  {
472
 
    if (isHttp | isWebSocket) matches[3] = '80';
473
 
    if (isHttps | isWebSocketSSL) matches[3] = '443';
474
 
  }
475
 
  if (isWebSocket)
476
 
    matches[1] = 'http';
477
 
  if (isWebSocketSSL)
478
 
    matches[1] = 'https';
479
 
 
480
 
  var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
481
 
  if (origins.indexOf(origin) < 0)
482
 
    return 'DIRECT';
483
 
  if (isHttp)
484
 
    return 'PROXY %(remote)s:%(httpport)s';
485
 
  if (isHttps || isWebSocket || isWebSocketSSL)
486
 
    return 'PROXY %(remote)s:%(sslport)s';
487
 
  return 'DIRECT';
488
 
}""" % { "origins": origins,
489
 
         "remote":  self.webServer,
490
 
         "httpport":self.httpPort,
491
 
         "sslport": self.sslPort }
492
 
      pacURL = "".join(pacURL.splitlines())
493
 
 
494
 
      part += """
495
 
user_pref("network.proxy.type", 2);
496
 
user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
497
 
 
498
 
user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
499
 
""" % {"pacURL": pacURL}
500
 
      prefs.append(part)
501
 
    else:
502
 
      part = 'user_pref("network.proxy.type", 0);\n'
503
 
      prefs.append(part)
504
 
 
505
 
    for v in extraPrefs:
506
 
      thispref = v.split("=", 1)
507
 
      if len(thispref) < 2:
508
 
        print "Error: syntax error in --setpref=" + v
509
 
        sys.exit(1)
510
 
      part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
511
 
      prefs.append(part)
512
 
 
513
 
    # write the preferences
514
 
    prefsFile = open(profileDir + "/" + "user.js", "a")
515
 
    prefsFile.write("".join(prefs))
516
 
    prefsFile.close()
517
 
 
518
 
    apps = [
519
 
      {
520
 
        'name': 'http_example_org',
521
 
        'origin': 'http://example.org',
522
 
        'manifestURL': 'http://example.org/manifest.webapp',
523
 
        'description': 'http://example.org App'
524
 
      },
525
 
      {
526
 
        'name': 'https_example_com',
527
 
        'origin': 'https://example.com',
528
 
        'manifestURL': 'https://example.com/manifest.webapp',
529
 
        'description': 'https://example.com App'
530
 
      },
531
 
      {
532
 
        'name': 'http_test1_example_org',
533
 
        'origin': 'http://test1.example.org',
534
 
        'manifestURL': 'http://test1.example.org/manifest.webapp',
535
 
        'description': 'http://test1.example.org App'
536
 
      },
537
 
      {
538
 
        'name': 'http_test1_example_org_8000',
539
 
        'origin': 'http://test1.example.org:8000',
540
 
        'manifestURL': 'http://test1.example.org:8000/manifest.webapp',
541
 
        'description': 'http://test1.example.org:8000 App'
542
 
      },
543
 
      {
544
 
        'name': 'http_sub1_test1_example_org',
545
 
        'origin': 'http://sub1.test1.example.org',
546
 
        'manifestURL': 'http://sub1.test1.example.org/manifest.webapp',
547
 
        'description': 'http://sub1.test1.example.org App'
548
 
      },
549
 
    ];
550
 
    self.setupTestApps(profileDir, apps)
551
 
 
552
 
  def addCommonOptions(self, parser):
553
 
    "Adds command-line options which are common to mochitest and reftest."
554
 
 
555
 
    parser.add_option("--setpref",
556
 
                      action = "append", type = "string",
557
 
                      default = [],
558
 
                      dest = "extraPrefs", metavar = "PREF=VALUE",
559
 
                      help = "defines an extra user preference")  
560
 
 
561
 
  def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
562
 
    pwfilePath = os.path.join(profileDir, ".crtdbpw")
563
 
  
564
 
    pwfile = open(pwfilePath, "w")
565
 
    pwfile.write("\n")
566
 
    pwfile.close()
567
 
 
568
 
    # Create head of the ssltunnel configuration file
569
 
    sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
570
 
    sslTunnelConfig = open(sslTunnelConfigPath, "w")
571
 
  
572
 
    sslTunnelConfig.write("httpproxy:1\n")
573
 
    sslTunnelConfig.write("certdbdir:%s\n" % certPath)
574
 
    sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
575
 
    sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
576
 
    sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
577
 
 
578
 
    # Configure automatic certificate and bind custom certificates, client authentication
579
 
    locations = self.readLocations()
580
 
    locations.pop(0)
581
 
    for loc in locations:
582
 
      if loc.scheme == "https" and "nocert" not in loc.options:
583
 
        customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
584
 
        clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
585
 
        redirRE      = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
586
 
        for option in loc.options:
587
 
          match = customCertRE.match(option)
588
 
          if match:
589
 
            customcert = match.group("nickname");
590
 
            sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
591
 
                      (loc.host, loc.port, self.sslPort, customcert))
592
 
 
593
 
          match = clientAuthRE.match(option)
594
 
          if match:
595
 
            clientauth = match.group("clientauth");
596
 
            sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
597
 
                      (loc.host, loc.port, self.sslPort, clientauth))
598
 
 
599
 
          match = redirRE.match(option)
600
 
          if match:
601
 
            redirhost = match.group("redirhost")
602
 
            sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
603
 
                      (loc.host, loc.port, self.sslPort, redirhost))
604
 
 
605
 
    sslTunnelConfig.close()
606
 
 
607
 
    # Pre-create the certification database for the profile
608
 
    env = self.environment(xrePath = xrePath)
609
 
    certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
610
 
    pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
611
 
 
612
 
    status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
613
 
    if status != 0:
614
 
      return status
615
 
 
616
 
    # Walk the cert directory and add custom CAs and client certs
617
 
    files = os.listdir(certPath)
618
 
    for item in files:
619
 
      root, ext = os.path.splitext(item)
620
 
      if ext == ".ca":
621
 
        trustBits = "CT,,"
622
 
        if root.endswith("-object"):
623
 
          trustBits = "CT,,CT"
624
 
        self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
625
 
                    "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
626
 
                    env = env).wait()
627
 
      if ext == ".client":
628
 
        self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
629
 
                    pwfilePath, "-d", profileDir], 
630
 
                    env = env).wait()
631
 
 
632
 
    os.unlink(pwfilePath)
633
 
    return 0
634
 
 
635
 
  def environment(self, env = None, xrePath = None, crashreporter = True):
636
 
    if xrePath == None:
637
 
      xrePath = self.DIST_BIN
638
 
    if env == None:
639
 
      env = dict(os.environ)
640
 
 
641
 
    ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
642
 
    if self.UNIXISH or self.IS_MAC:
643
 
      envVar = "LD_LIBRARY_PATH"
644
 
      if self.IS_MAC:
645
 
        envVar = "DYLD_LIBRARY_PATH"
646
 
      else: # unixish
647
 
        env['MOZILLA_FIVE_HOME'] = xrePath
648
 
      if envVar in env:
649
 
        ldLibraryPath = ldLibraryPath + ":" + env[envVar]
650
 
      env[envVar] = ldLibraryPath
651
 
    elif self.IS_WIN32:
652
 
      env["PATH"] = env["PATH"] + ";" + ldLibraryPath
653
 
 
654
 
    if crashreporter:
655
 
      env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
656
 
      env['MOZ_CRASHREPORTER'] = '1'
657
 
    else:
658
 
      env['MOZ_CRASHREPORTER_DISABLE'] = '1'
659
 
 
660
 
    env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
661
 
    env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
662
 
    env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
663
 
    return env
664
 
 
665
 
  if IS_WIN32:
666
 
    PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
667
 
    GetLastError = ctypes.windll.kernel32.GetLastError
668
 
 
669
 
    def readWithTimeout(self, f, timeout):
670
 
      """Try to read a line of output from the file object |f|.
671
 
      |f| must be a  pipe, like the |stdout| member of a subprocess.Popen
672
 
      object created with stdout=PIPE. If no output
673
 
      is received within |timeout| seconds, return a blank line.
674
 
      Returns a tuple (line, did_timeout), where |did_timeout| is True
675
 
      if the read timed out, and False otherwise."""
676
 
      if timeout is None:
677
 
        # shortcut to allow callers to pass in "None" for no timeout.
678
 
        return (f.readline(), False)
679
 
      x = msvcrt.get_osfhandle(f.fileno())
680
 
      l = ctypes.c_long()
681
 
      done = time.time() + timeout
682
 
      while time.time() < done:
683
 
        if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
684
 
          err = self.GetLastError()
685
 
          if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
686
 
            return ('', False)
687
 
          else:
688
 
            log.error("readWithTimeout got error: %d", err)
689
 
        if l.value > 0:
690
 
          # we're assuming that the output is line-buffered,
691
 
          # which is not unreasonable
692
 
          return (f.readline(), False)
693
 
        time.sleep(0.01)
694
 
      return ('', True)
695
 
 
696
 
    def isPidAlive(self, pid):
697
 
      STILL_ACTIVE = 259
698
 
      PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
699
 
      pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
700
 
      if not pHandle:
701
 
        return False
702
 
      pExitCode = ctypes.wintypes.DWORD()
703
 
      ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
704
 
      ctypes.windll.kernel32.CloseHandle(pHandle)
705
 
      return pExitCode.value == STILL_ACTIVE
706
 
 
707
 
    def killPid(self, pid):
708
 
      PROCESS_TERMINATE = 0x0001
709
 
      pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
710
 
      if not pHandle:
711
 
        return
712
 
      success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
713
 
      ctypes.windll.kernel32.CloseHandle(pHandle)
714
 
 
715
 
  else:
716
 
 
717
 
    def readWithTimeout(self, f, timeout):
718
 
      """Try to read a line of output from the file object |f|. If no output
719
 
      is received within |timeout| seconds, return a blank line.
720
 
      Returns a tuple (line, did_timeout), where |did_timeout| is True
721
 
      if the read timed out, and False otherwise."""
722
 
      (r, w, e) = select.select([f], [], [], timeout)
723
 
      if len(r) == 0:
724
 
        return ('', True)
725
 
      return (f.readline(), False)
726
 
 
727
 
    def isPidAlive(self, pid):
728
 
      try:
729
 
        # kill(pid, 0) checks for a valid PID without actually sending a signal
730
 
        # The method throws OSError if the PID is invalid, which we catch below.
731
 
        os.kill(pid, 0)
732
 
 
733
 
        # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
734
 
        # the process terminates before we get to this point.
735
 
        wpid, wstatus = os.waitpid(pid, os.WNOHANG)
736
 
        return wpid == 0
737
 
      except OSError, err:
738
 
        # Catch the errors we might expect from os.kill/os.waitpid, 
739
 
        # and re-raise any others
740
 
        if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
741
 
          return False
742
 
        raise
743
 
 
744
 
    def killPid(self, pid):
745
 
      os.kill(pid, signal.SIGKILL)
746
 
 
747
 
  def dumpScreen(self, utilityPath):
748
 
    self.haveDumpedScreen = True;
749
 
 
750
 
    # Need to figure out what tool and whether it write to a file or stdout
751
 
    if self.UNIXISH:
752
 
      utility = [os.path.join(utilityPath, "screentopng")]
753
 
      imgoutput = 'stdout'
754
 
    elif self.IS_MAC:
755
 
      utility = ['/usr/sbin/screencapture', '-C', '-x', '-t', 'png']
756
 
      imgoutput = 'file'
757
 
    elif self.IS_WIN32:
758
 
      utility = [os.path.join(utilityPath, "screenshot.exe")]
759
 
      imgoutput = 'file'
760
 
 
761
 
    # Run the capture correctly for the type of capture
762
 
    try:
763
 
      if imgoutput == 'file':
764
 
        tmpfd, imgfilename = tempfile.mkstemp(prefix='mozilla-test-fail_')
765
 
        os.close(tmpfd)
766
 
        dumper = self.Process(utility + [imgfilename])
767
 
      elif imgoutput == 'stdout':
768
 
        dumper = self.Process(utility, bufsize=-1,
769
 
                              stdout=subprocess.PIPE, close_fds=True)
770
 
    except OSError, err:
771
 
      self.log.info("Failed to start %s for screenshot: %s",
772
 
                    utility[0], err.strerror)
773
 
      return
774
 
 
775
 
    # Check whether the capture utility ran successfully
776
 
    dumper_out, dumper_err = dumper.communicate()
777
 
    if dumper.returncode != 0:
778
 
      self.log.info("%s exited with code %d", utility, dumper.returncode)
779
 
      return
780
 
 
781
 
    try:
782
 
      if imgoutput == 'stdout':
783
 
        image = dumper_out
784
 
      elif imgoutput == 'file':
785
 
        with open(imgfilename, 'rb') as imgfile:
786
 
          image = imgfile.read()
787
 
    except IOError, err:
788
 
        self.log.info("Failed to read image from %s", imgoutput)
789
 
 
790
 
    import base64
791
 
    encoded = base64.b64encode(image)
792
 
    self.log.info("SCREENSHOT: data:image/png;base64,%s", encoded)
793
 
 
794
 
  def killAndGetStack(self, proc, utilityPath, debuggerInfo):
795
 
    """Kill the process, preferrably in a way that gets us a stack trace."""
796
 
    if not debuggerInfo:
797
 
      if self.haveDumpedScreen:
798
 
        self.log.info("Not taking screenshot here: see the one that was previously logged")
799
 
      else:
800
 
        self.dumpScreen(utilityPath)
801
 
 
802
 
    if self.CRASHREPORTER and not debuggerInfo:
803
 
      if self.UNIXISH:
804
 
        # ABRT will get picked up by Breakpad's signal handler
805
 
        os.kill(proc.pid, signal.SIGABRT)
806
 
        return
807
 
      elif self.IS_WIN32:
808
 
        # We should have a "crashinject" program in our utility path
809
 
        crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
810
 
        if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
811
 
          return
812
 
      #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
813
 
    self.log.info("Can't trigger Breakpad, just killing process")
814
 
    proc.kill()
815
 
 
816
 
  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
817
 
    """ Look for timeout or crashes and return the status after the process terminates """
818
 
    stackFixerProcess = None
819
 
    stackFixerFunction = None
820
 
    didTimeout = False
821
 
    hitMaxTime = False
822
 
    if proc.stdout is None:
823
 
      self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
824
 
    else:
825
 
      logsource = proc.stdout
826
 
 
827
 
      if self.IS_DEBUG_BUILD and (self.IS_MAC or self.IS_LINUX) and symbolsPath and os.path.exists(symbolsPath):
828
 
        # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
829
 
        # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
830
 
        sys.path.insert(0, utilityPath)
831
 
        import fix_stack_using_bpsyms as stackFixerModule
832
 
        stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
833
 
        del sys.path[0]
834
 
      elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
835
 
        # Run each line through a function in fix_macosx_stack.py (uses atos)
836
 
        sys.path.insert(0, utilityPath)
837
 
        import fix_macosx_stack as stackFixerModule
838
 
        stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
839
 
        del sys.path[0]
840
 
      elif self.IS_DEBUG_BUILD and self.IS_LINUX:
841
 
        # Run logsource through fix-linux-stack.pl (uses addr2line)
842
 
        # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
843
 
        stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
844
 
                                         stdin=logsource,
845
 
                                         stdout=subprocess.PIPE)
846
 
        logsource = stackFixerProcess.stdout
847
 
 
848
 
      (line, didTimeout) = self.readWithTimeout(logsource, timeout)
849
 
      while line != "" and not didTimeout:
850
 
        if stackFixerFunction:
851
 
          line = stackFixerFunction(line)
852
 
        self.log.info(line.rstrip().decode("UTF-8", "ignore"))
853
 
        if "TEST-START" in line and "|" in line:
854
 
          self.lastTestSeen = line.split("|")[1].strip()
855
 
        if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
856
 
          if self.haveDumpedScreen:
857
 
            self.log.info("Not taking screenshot here: see the one that was previously logged")
858
 
          else:
859
 
            self.dumpScreen(utilityPath)
860
 
 
861
 
        (line, didTimeout) = self.readWithTimeout(logsource, timeout)
862
 
        if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
863
 
          # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
864
 
          hitMaxTime = True
865
 
          self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
866
 
          self.killAndGetStack(proc, utilityPath, debuggerInfo)
867
 
      if didTimeout:
868
 
        self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
869
 
        self.killAndGetStack(proc, utilityPath, debuggerInfo)
870
 
 
871
 
    status = proc.wait()
872
 
    if status == 0:
873
 
      self.lastTestSeen = "Main app process exited normally"
874
 
    if status != 0 and not didTimeout and not hitMaxTime:
875
 
      self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
876
 
    if stackFixerProcess is not None:
877
 
      fixerStatus = stackFixerProcess.wait()
878
 
      if fixerStatus != 0 and not didTimeout and not hitMaxTime:
879
 
        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
880
 
    return status
881
 
 
882
 
  def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
883
 
    """ build the application command line """
884
 
 
885
 
    cmd = os.path.abspath(app)
886
 
    if self.IS_MAC and not self.IS_CAMINO and os.path.exists(cmd + "-bin"):
887
 
      # Prefer 'app-bin' in case 'app' is a shell script.
888
 
      # We can remove this hack once bug 673899 etc are fixed.
889
 
      cmd += "-bin"
890
 
 
891
 
    args = []
892
 
 
893
 
    if debuggerInfo:
894
 
      args.extend(debuggerInfo["args"])
895
 
      args.append(cmd)
896
 
      cmd = os.path.abspath(debuggerInfo["path"])
897
 
 
898
 
    if self.IS_MAC:
899
 
      args.append("-foreground")
900
 
 
901
 
    if self.IS_CYGWIN:
902
 
      profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
903
 
    else:
904
 
      profileDirectory = profileDir + "/"
905
 
 
906
 
    args.extend(("-no-remote", "-profile", profileDirectory))
907
 
    if testURL is not None:
908
 
      if self.IS_CAMINO:
909
 
        args.extend(("-url", testURL))
910
 
      else:
911
 
        args.append((testURL))
912
 
    args.extend(extraArgs)
913
 
    return cmd, args
914
 
 
915
 
  def checkForZombies(self, processLog):
916
 
    """ Look for hung processes """
917
 
    if not os.path.exists(processLog):
918
 
      self.log.info('INFO | automation.py | PID log not found: %s', processLog)
919
 
    else:
920
 
      self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
921
 
      processList = []
922
 
      pidRE = re.compile(r'launched child process (\d+)$')
923
 
      processLogFD = open(processLog)
924
 
      for line in processLogFD:
925
 
        self.log.info(line.rstrip())
926
 
        m = pidRE.search(line)
927
 
        if m:
928
 
          processList.append(int(m.group(1)))
929
 
      processLogFD.close()
930
 
 
931
 
      for processPID in processList:
932
 
        self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
933
 
        if self.isPidAlive(processPID):
934
 
          self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
935
 
          self.killPid(processPID)
936
 
 
937
 
  def checkForCrashes(self, profileDir, symbolsPath):
938
 
    automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
939
 
 
940
 
  def runApp(self, testURL, env, app, profileDir, extraArgs,
941
 
             runSSLTunnel = False, utilityPath = None,
942
 
             xrePath = None, certPath = None,
943
 
             debuggerInfo = None, symbolsPath = None,
944
 
             timeout = -1, maxTime = None):
945
 
    """
946
 
    Run the app, log the duration it took to execute, return the status code.
947
 
    Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
948
 
    """
949
 
 
950
 
    if utilityPath == None:
951
 
      utilityPath = self.DIST_BIN
952
 
    if xrePath == None:
953
 
      xrePath = self.DIST_BIN
954
 
    if certPath == None:
955
 
      certPath = self.CERTS_SRC_DIR
956
 
    if timeout == -1:
957
 
      timeout = self.DEFAULT_TIMEOUT
958
 
 
959
 
    # copy env so we don't munge the caller's environment
960
 
    env = dict(env);
961
 
    env["NO_EM_RESTART"] = "1"
962
 
    tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
963
 
    os.close(tmpfd)
964
 
    env["MOZ_PROCESS_LOG"] = processLog
965
 
 
966
 
    if self.IS_TEST_BUILD and runSSLTunnel:
967
 
      # create certificate database for the profile
968
 
      certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
969
 
      if certificateStatus != 0:
970
 
        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
971
 
        return certificateStatus
972
 
 
973
 
      # start ssltunnel to provide https:// URLs capability
974
 
      ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
975
 
      ssltunnelProcess = self.Process([ssltunnel, 
976
 
                               os.path.join(profileDir, "ssltunnel.cfg")], 
977
 
                               env = self.environment(xrePath = xrePath))
978
 
      self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
979
 
 
980
 
    cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
981
 
    startTime = datetime.now()
982
 
 
983
 
    if debuggerInfo and debuggerInfo["interactive"]:
984
 
      # If an interactive debugger is attached, don't redirect output,
985
 
      # don't use timeouts, and don't capture ctrl-c.
986
 
      timeout = None
987
 
      maxTime = None
988
 
      outputPipe = None
989
 
      signal.signal(signal.SIGINT, lambda sigid, frame: None)
990
 
    else:
991
 
      outputPipe = subprocess.PIPE
992
 
 
993
 
    self.lastTestSeen = "automation.py"
994
 
    proc = self.Process([cmd] + args,
995
 
                 env = self.environment(env, xrePath = xrePath,
996
 
                                   crashreporter = not debuggerInfo),
997
 
                 stdout = outputPipe,
998
 
                 stderr = subprocess.STDOUT)
999
 
    self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
1000
 
 
1001
 
    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
1002
 
    self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
1003
 
 
1004
 
    # Do a final check for zombie child processes.
1005
 
    self.checkForZombies(processLog)
1006
 
    self.checkForCrashes(profileDir, symbolsPath)
1007
 
 
1008
 
    if os.path.exists(processLog):
1009
 
      os.unlink(processLog)
1010
 
 
1011
 
    if self.IS_TEST_BUILD and runSSLTunnel:
1012
 
      ssltunnelProcess.kill()
1013
 
 
1014
 
    return status
1015
 
 
1016
 
  def getExtensionIDFromRDF(self, rdfSource):
1017
 
    """
1018
 
    Retrieves the extension id from an install.rdf file (or string).
1019
 
    """
1020
 
    from xml.dom.minidom import parse, parseString, Node
1021
 
 
1022
 
    if isinstance(rdfSource, file):
1023
 
      document = parse(rdfSource)
1024
 
    else:
1025
 
      document = parseString(rdfSource)
1026
 
 
1027
 
    # Find the <em:id> element. There can be multiple <em:id> tags
1028
 
    # within <em:targetApplication> tags, so we have to check this way.
1029
 
    for rdfChild in document.documentElement.childNodes:
1030
 
      if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
1031
 
        for descChild in rdfChild.childNodes:
1032
 
          if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
1033
 
            return descChild.childNodes[0].data
1034
 
 
1035
 
    return None
1036
 
 
1037
 
  def installExtension(self, extensionSource, profileDir, extensionID = None):
1038
 
    """
1039
 
    Copies an extension into the extensions directory of the given profile.
1040
 
    extensionSource - the source location of the extension files.  This can be either
1041
 
                      a directory or a path to an xpi file.
1042
 
    profileDir      - the profile directory we are copying into.  We will create the
1043
 
                      "extensions" directory there if it doesn't exist.
1044
 
    extensionID     - the id of the extension to be used as the containing directory for the
1045
 
                      extension, if extensionSource is a directory, i.e.
1046
 
                  this is the name of the folder in the <profileDir>/extensions/<extensionID>
1047
 
    """
1048
 
    if not os.path.isdir(profileDir):
1049
 
      self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
1050
 
      return
1051
 
 
1052
 
    installRDFFilename = "install.rdf"
1053
 
 
1054
 
    extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
1055
 
    if not os.path.isdir(extensionsRootDir):
1056
 
      os.makedirs(extensionsRootDir)
1057
 
 
1058
 
    if os.path.isfile(extensionSource):
1059
 
      reader = automationutils.ZipFileReader(extensionSource)
1060
 
 
1061
 
      for filename in reader.namelist():
1062
 
        # Sanity check the zip file.
1063
 
        if os.path.isabs(filename):
1064
 
          self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
1065
 
          return
1066
 
 
1067
 
        # We may need to dig the extensionID out of the zip file...
1068
 
        if extensionID is None and filename == installRDFFilename:
1069
 
          extensionID = self.getExtensionIDFromRDF(reader.read(filename))
1070
 
 
1071
 
      # We must know the extensionID now.
1072
 
      if extensionID is None:
1073
 
        self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
1074
 
        return
1075
 
 
1076
 
      # Make the extension directory.
1077
 
      extensionDir = os.path.join(extensionsRootDir, extensionID)
1078
 
      os.mkdir(extensionDir)
1079
 
 
1080
 
      # Extract all files.
1081
 
      reader.extractall(extensionDir)
1082
 
 
1083
 
    elif os.path.isdir(extensionSource):
1084
 
      if extensionID is None:
1085
 
        filename = os.path.join(extensionSource, installRDFFilename)
1086
 
        if os.path.isfile(filename):
1087
 
          with open(filename, "r") as installRDF:
1088
 
            extensionID = self.getExtensionIDFromRDF(installRDF)
1089
 
 
1090
 
        if extensionID is None:
1091
 
          self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
1092
 
          return
1093
 
 
1094
 
      # Copy extension tree into its own directory.
1095
 
      # "destination directory must not already exist".
1096
 
      shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
1097
 
 
1098
 
    else:
1099
 
      self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)