summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/harness/wptrunner/wptcommandline.py
blob: 20ca81a58f421cb803978db8a5ec6a09d3e15ad2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import argparse
import ast
import os
import sys
from collections import OrderedDict
from distutils.spawn import find_executable

import config
import wpttest


def abs_path(path):
    return os.path.abspath(os.path.expanduser(path))


def url_or_path(path):
    import urlparse

    parsed = urlparse.urlparse(path)
    if len(parsed.scheme) > 2:
        return path
    else:
        return abs_path(path)


def require_arg(kwargs, name, value_func=None):
    if value_func is None:
        value_func = lambda x: x is not None

    if not name in kwargs or not value_func(kwargs[name]):
        print >> sys.stderr, "Missing required argument %s" % name
        sys.exit(1)


def create_parser(product_choices=None):
    from mozlog import commandline

    import products

    if product_choices is None:
        config_data = config.load()
        product_choices = products.products_enabled(config_data)

    parser = argparse.ArgumentParser(description="""Runner for web-platform-tests tests.""",
                                     usage="""%(prog)s [OPTION]... [TEST]...

TEST is either the full path to a test file to run, or the URL of a test excluding
scheme host and port.""")
    parser.add_argument("--manifest-update", action="store_true", default=False,
                        help="Regenerate the test manifest.")

    parser.add_argument("--timeout-multiplier", action="store", type=float, default=None,
                        help="Multiplier relative to standard test timeout to use")
    parser.add_argument("--run-by-dir", type=int, nargs="?", default=False,
                        help="Split run into groups by directories. With a parameter,"
                        "limit the depth of splits e.g. --run-by-dir=1 to split by top-level"
                        "directory")
    parser.add_argument("--processes", action="store", type=int, default=None,
                        help="Number of simultaneous processes to use")

    parser.add_argument("--no-capture-stdio", action="store_true", default=False,
                        help="Don't capture stdio and write to logging")

    mode_group = parser.add_argument_group("Mode")
    mode_group.add_argument("--list-test-groups", action="store_true",
                            default=False,
                            help="List the top level directories containing tests that will run.")
    mode_group.add_argument("--list-disabled", action="store_true",
                            default=False,
                            help="List the tests that are disabled on the current platform")

    test_selection_group = parser.add_argument_group("Test Selection")
    test_selection_group.add_argument("--test-types", action="store",
                                      nargs="*", default=wpttest.enabled_tests,
                                      choices=wpttest.enabled_tests,
                                      help="Test types to run")
    test_selection_group.add_argument("--include", action="append",
                                      help="URL prefix to include")
    test_selection_group.add_argument("--exclude", action="append",
                                      help="URL prefix to exclude")
    test_selection_group.add_argument("--include-manifest", type=abs_path,
                                      help="Path to manifest listing tests to include")
    test_selection_group.add_argument("--tag", action="append", dest="tags",
                                      help="Labels applied to tests to include in the run. Labels starting dir: are equivalent to top-level directories.")

    debugging_group = parser.add_argument_group("Debugging")
    debugging_group.add_argument('--debugger', const="__default__", nargs="?",
                                 help="run under a debugger, e.g. gdb or valgrind")
    debugging_group.add_argument('--debugger-args', help="arguments to the debugger")
    debugging_group.add_argument("--repeat", action="store", type=int, default=1,
                                 help="Number of times to run the tests")
    debugging_group.add_argument("--repeat-until-unexpected", action="store_true", default=None,
                                 help="Run tests in a loop until one returns an unexpected result")
    debugging_group.add_argument('--pause-after-test', action="store_true", default=None,
                                 help="Halt the test runner after each test (this happens by default if only a single test is run)")
    debugging_group.add_argument('--no-pause-after-test', dest="pause_after_test", action="store_false",
                                 help="Don't halt the test runner irrespective of the number of tests run")

    debugging_group.add_argument('--pause-on-unexpected', action="store_true",
                                 help="Halt the test runner when an unexpected result is encountered")

    debugging_group.add_argument("--symbols-path", action="store", type=url_or_path,
                                 help="Path or url to symbols file used to analyse crash minidumps.")
    debugging_group.add_argument("--stackwalk-binary", action="store", type=abs_path,
                                 help="Path to stackwalker program used to analyse minidumps.")

    debugging_group.add_argument("--pdb", action="store_true",
                                 help="Drop into pdb on python exception")

    config_group = parser.add_argument_group("Configuration")
    config_group.add_argument("--binary", action="store",
                              type=abs_path, help="Binary to run tests against")
    config_group.add_argument('--binary-arg',
                              default=[], action="append", dest="binary_args",
                              help="Extra argument for the binary (servo)")
    config_group.add_argument("--webdriver-binary", action="store", metavar="BINARY",
                              type=abs_path, help="WebDriver server binary to use")

    config_group.add_argument("--metadata", action="store", type=abs_path, dest="metadata_root",
                              help="Path to root directory containing test metadata"),
    config_group.add_argument("--tests", action="store", type=abs_path, dest="tests_root",
                              help="Path to root directory containing test files"),
    config_group.add_argument("--run-info", action="store", type=abs_path,
                              help="Path to directory containing extra json files to add to run info")
    config_group.add_argument("--product", action="store", choices=product_choices,
                              default=None, help="Browser against which to run tests")
    config_group.add_argument("--config", action="store", type=abs_path, dest="config",
                              help="Path to config file")

    build_type = parser.add_mutually_exclusive_group()
    build_type.add_argument("--debug-build", dest="debug", action="store_true",
                            default=None,
                            help="Build is a debug build (overrides any mozinfo file)")
    build_type.add_argument("--release-build", dest="debug", action="store_false",
                            default=None,
                            help="Build is a release (overrides any mozinfo file)")


    chunking_group = parser.add_argument_group("Test Chunking")
    chunking_group.add_argument("--total-chunks", action="store", type=int, default=1,
                                help="Total number of chunks to use")
    chunking_group.add_argument("--this-chunk", action="store", type=int, default=1,
                                help="Chunk number to run")
    chunking_group.add_argument("--chunk-type", action="store", choices=["none", "equal_time", "hash", "dir_hash"],
                                default=None, help="Chunking type to use")

    ssl_group = parser.add_argument_group("SSL/TLS")
    ssl_group.add_argument("--ssl-type", action="store", default=None,
                        choices=["openssl", "pregenerated", "none"],
                        help="Type of ssl support to enable (running without ssl may lead to spurious errors)")

    ssl_group.add_argument("--openssl-binary", action="store",
                        help="Path to openssl binary", default="openssl")
    ssl_group.add_argument("--certutil-binary", action="store",
                        help="Path to certutil binary for use with Firefox + ssl")

    ssl_group.add_argument("--ca-cert-path", action="store", type=abs_path,
                        help="Path to ca certificate when using pregenerated ssl certificates")
    ssl_group.add_argument("--host-key-path", action="store", type=abs_path,
                        help="Path to host private key when using pregenerated ssl certificates")
    ssl_group.add_argument("--host-cert-path", action="store", type=abs_path,
                        help="Path to host certificate when using pregenerated ssl certificates")

    gecko_group = parser.add_argument_group("Gecko-specific")
    gecko_group.add_argument("--prefs-root", dest="prefs_root", action="store", type=abs_path,
                             help="Path to the folder containing browser prefs")
    gecko_group.add_argument("--disable-e10s", dest="gecko_e10s", action="store_false", default=True,
                             help="Run tests without electrolysis preferences")
    gecko_group.add_argument("--stackfix-dir", dest="stackfix_dir", action="store",
                             help="Path to directory containing assertion stack fixing scripts")

    b2g_group = parser.add_argument_group("B2G-specific")
    b2g_group.add_argument("--b2g-no-backup", action="store_true", default=False,
                           help="Don't backup device before testrun with --product=b2g")

    servo_group = parser.add_argument_group("Servo-specific")
    servo_group.add_argument("--user-stylesheet",
                             default=[], action="append", dest="user_stylesheets",
                             help="Inject a user CSS stylesheet into every test.")
    servo_group.add_argument("--servo-backend",
                             default="cpu", choices=["cpu", "webrender"],
                             help="Rendering backend to use with Servo.")


    parser.add_argument("test_list", nargs="*",
                        help="List of URLs for tests to run, or paths including tests to run. "
                             "(equivalent to --include)")

    commandline.add_logging_group(parser)
    return parser


def set_from_config(kwargs):
    if kwargs["config"] is None:
        config_path = config.path()
    else:
        config_path = kwargs["config"]

    kwargs["config_path"] = config_path

    kwargs["config"] = config.read(kwargs["config_path"])

    keys = {"paths": [("prefs", "prefs_root", True),
                      ("run_info", "run_info", True)],
            "web-platform-tests": [("remote_url", "remote_url", False),
                                   ("branch", "branch", False),
                                   ("sync_path", "sync_path", True)],
            "SSL": [("openssl_binary", "openssl_binary", True),
                    ("certutil_binary", "certutil_binary", True),
                    ("ca_cert_path", "ca_cert_path", True),
                    ("host_cert_path", "host_cert_path", True),
                    ("host_key_path", "host_key_path", True)]}

    for section, values in keys.iteritems():
        for config_value, kw_value, is_path in values:
            if kw_value in kwargs and kwargs[kw_value] is None:
                if not is_path:
                    new_value = kwargs["config"].get(section, config.ConfigDict({})).get(config_value)
                else:
                    new_value = kwargs["config"].get(section, config.ConfigDict({})).get_path(config_value)
                kwargs[kw_value] = new_value

    kwargs["test_paths"] = get_test_paths(kwargs["config"])

    if kwargs["tests_root"]:
        if "/" not in kwargs["test_paths"]:
            kwargs["test_paths"]["/"] = {}
        kwargs["test_paths"]["/"]["tests_path"] = kwargs["tests_root"]

    if kwargs["metadata_root"]:
        if "/" not in kwargs["test_paths"]:
            kwargs["test_paths"]["/"] = {}
        kwargs["test_paths"]["/"]["metadata_path"] = kwargs["metadata_root"]

    kwargs["suite_name"] = kwargs["config"].get("web-platform-tests", {}).get("name", "web-platform-tests")


def get_test_paths(config):
    # Set up test_paths
    test_paths = OrderedDict()

    for section in config.iterkeys():
        if section.startswith("manifest:"):
            manifest_opts = config.get(section)
            url_base = manifest_opts.get("url_base", "/")
            test_paths[url_base] = {
                "tests_path": manifest_opts.get_path("tests"),
                "metadata_path": manifest_opts.get_path("metadata")}

    return test_paths


def exe_path(name):
    if name is None:
        return

    path = find_executable(name)
    if os.access(path, os.X_OK):
        return path


def check_args(kwargs):
    set_from_config(kwargs)

    for test_paths in kwargs["test_paths"].itervalues():
        if not ("tests_path" in test_paths and
                "metadata_path" in test_paths):
            print "Fatal: must specify both a test path and metadata path"
            sys.exit(1)
        for key, path in test_paths.iteritems():
            name = key.split("_", 1)[0]

            if not os.path.exists(path):
                print "Fatal: %s path %s does not exist" % (name, path)
                sys.exit(1)

            if not os.path.isdir(path):
                print "Fatal: %s path %s is not a directory" % (name, path)
                sys.exit(1)

    if kwargs["product"] is None:
        kwargs["product"] = "firefox"

    if kwargs["test_list"]:
        if kwargs["include"] is not None:
            kwargs["include"].extend(kwargs["test_list"])
        else:
            kwargs["include"] = kwargs["test_list"]

    if kwargs["run_info"] is None:
        kwargs["run_info"] = kwargs["config_path"]

    if kwargs["this_chunk"] > 1:
        require_arg(kwargs, "total_chunks", lambda x: x >= kwargs["this_chunk"])

    if kwargs["chunk_type"] is None:
        if kwargs["total_chunks"] > 1:
            kwargs["chunk_type"] = "dir_hash"
        else:
            kwargs["chunk_type"] = "none"

    if kwargs["processes"] is None:
        kwargs["processes"] = 1

    if kwargs["debugger"] is not None:
        import mozdebug
        if kwargs["debugger"] == "__default__":
            kwargs["debugger"] = mozdebug.get_default_debugger_name()
        debug_info = mozdebug.get_debugger_info(kwargs["debugger"],
                                                kwargs["debugger_args"])
        if debug_info and debug_info.interactive:
            if kwargs["processes"] != 1:
                kwargs["processes"] = 1
            kwargs["no_capture_stdio"] = True
        kwargs["debug_info"] = debug_info
    else:
        kwargs["debug_info"] = None

    if kwargs["binary"] is not None:
        if not os.path.exists(kwargs["binary"]):
            print >> sys.stderr, "Binary path %s does not exist" % kwargs["binary"]
            sys.exit(1)

    if kwargs["ssl_type"] is None:
        if None not in (kwargs["ca_cert_path"], kwargs["host_cert_path"], kwargs["host_key_path"]):
            kwargs["ssl_type"] = "pregenerated"
        elif exe_path(kwargs["openssl_binary"]) is not None:
            kwargs["ssl_type"] = "openssl"
        else:
            kwargs["ssl_type"] = "none"

    if kwargs["ssl_type"] == "pregenerated":
        require_arg(kwargs, "ca_cert_path", lambda x:os.path.exists(x))
        require_arg(kwargs, "host_cert_path", lambda x:os.path.exists(x))
        require_arg(kwargs, "host_key_path", lambda x:os.path.exists(x))

    elif kwargs["ssl_type"] == "openssl":
        path = exe_path(kwargs["openssl_binary"])
        if path is None:
            print >> sys.stderr, "openssl-binary argument missing or not a valid executable"
            sys.exit(1)
        kwargs["openssl_binary"] = path

    if kwargs["ssl_type"] != "none" and kwargs["product"] == "firefox":
        path = exe_path(kwargs["certutil_binary"])
        if path is None:
            print >> sys.stderr, "certutil-binary argument missing or not a valid executable"
            sys.exit(1)
        kwargs["certutil_binary"] = path

    return kwargs


def check_args_update(kwargs):
    set_from_config(kwargs)

    if kwargs["product"] is None:
        kwargs["product"] = "firefox"


def create_parser_update(product_choices=None):
    from mozlog.structured import commandline

    import products

    if product_choices is None:
        config_data = config.load()
        product_choices = products.products_enabled(config_data)

    parser = argparse.ArgumentParser("web-platform-tests-update",
                                     description="Update script for web-platform-tests tests.")
    parser.add_argument("--product", action="store", choices=product_choices,
                        default=None, help="Browser for which metadata is being updated")
    parser.add_argument("--config", action="store", type=abs_path, help="Path to config file")
    parser.add_argument("--metadata", action="store", type=abs_path, dest="metadata_root",
                        help="Path to the folder containing test metadata"),
    parser.add_argument("--tests", action="store", type=abs_path, dest="tests_root",
                        help="Path to web-platform-tests"),
    parser.add_argument("--sync-path", action="store", type=abs_path,
                        help="Path to store git checkout of web-platform-tests during update"),
    parser.add_argument("--remote_url", action="store",
                        help="URL of web-platfrom-tests repository to sync against"),
    parser.add_argument("--branch", action="store", type=abs_path,
                        help="Remote branch to sync against")
    parser.add_argument("--rev", action="store", help="Revision to sync to")
    parser.add_argument("--no-patch", action="store_true",
                        help="Don't create an mq patch or git commit containing the changes.")
    parser.add_argument("--sync", dest="sync", action="store_true", default=False,
                        help="Sync the tests with the latest from upstream")
    parser.add_argument("--ignore-existing", action="store_true", help="When updating test results only consider results from the logfiles provided, not existing expectations.")
    parser.add_argument("--continue", action="store_true", help="Continue a previously started run of the update script")
    parser.add_argument("--abort", action="store_true", help="Clear state from a previous incomplete run of the update script")
    # Should make this required iff run=logfile
    parser.add_argument("run_log", nargs="*", type=abs_path,
                        help="Log file from run of tests")
    commandline.add_logging_group(parser)
    return parser


def create_parser_reduce(product_choices=None):
    parser = create_parser(product_choices)
    parser.add_argument("target", action="store", help="Test id that is unstable")
    return parser


def parse_args():
    parser = create_parser()
    rv = vars(parser.parse_args())
    check_args(rv)
    return rv


def parse_args_update():
    parser = create_parser_update()
    rv = vars(parser.parse_args())
    check_args_update(rv)
    return rv


def parse_args_reduce():
    parser = create_parser_reduce()
    rv = vars(parser.parse_args())
    check_args(rv)
    return rv