1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3# You can obtain one at http://mozilla.org/MPL/2.0/.
4from __future__ import absolute_import, print_function
5
6import copy
7import os
8import re
9import sys
10
11from mozlog.commandline import setup_logging
12from talos import utils, test
13from talos.cmdline import parse_args
14
15
16class ConfigurationError(Exception):
17    pass
18
19
20DEFAULTS = dict(
21    # args to pass to browser
22    extra_args=[],
23    buildid="testbuildid",
24    init_url="getInfo.html",
25    env={"NO_EM_RESTART": "1"},
26    # base data for all tests. Note that any None here will end up converted to
27    # an empty string in useBaseTestDefaults.
28    basetest=dict(
29        cycles=1,
30        profile_path="${talos}/base_profile",
31        responsiveness=False,
32        gecko_profile=False,
33        gecko_profile_interval=1,
34        gecko_profile_entries=100000,
35        resolution=1,
36        mainthread=False,
37        shutdown=False,
38        timeout=3600,
39        tpchrome=True,
40        tpcycles=10,
41        tpmozafterpaint=False,
42        tphero=False,
43        pdfpaint=True,
44        fnbpaint=False,
45        firstpaint=False,
46        format_pagename=True,
47        userready=False,
48        testeventmap=[],
49        base_vs_ref=False,
50        tppagecycles=1,
51        tploadnocache=False,
52        tpscrolltest=False,
53        win_counters=[],
54        w7_counters=[],
55        linux_counters=[],
56        mac_counters=[],
57        xperf_counters=[],
58        setup=None,
59        cleanup=None,
60        preferences={},
61    ),
62)
63
64
65# keys to generated self.config that are global overrides to tests
66GLOBAL_OVERRIDES = (
67    "cycles",
68    "gecko_profile",
69    "gecko_profile_interval",
70    "gecko_profile_entries",
71    "gecko_profile_features",
72    "gecko_profile_threads",
73    "tpcycles",
74    "tppagecycles",
75    "tpmanifest",
76    "tptimeout",
77    "tpmozafterpaint",
78    "tphero",
79    "fnbpaint",
80    "pdfpaint",
81    "firstpaint",
82    "userready",
83)
84
85
86CONF_VALIDATORS = []
87
88
89def validator(func):
90    """
91    A decorator that register configuration validators to execute against the
92    configuration.
93
94    They will be executed in the order of declaration.
95    """
96    CONF_VALIDATORS.append(func)
97    return func
98
99
100def convert_url(config, url):
101    webserver = config["webserver"]
102    if not webserver:
103        return url
104
105    if "://" in url:
106        # assume a fully qualified url
107        return url
108
109    if ".html" in url:
110        url = "http://%s/%s" % (webserver, url)
111
112    return url
113
114
115@validator
116def fix_xperf(config):
117    # BBB: remove doubly-quoted xperf values from command line
118    # (needed for buildbot)
119    # https://bugzilla.mozilla.org/show_bug.cgi?id=704654#c43
120    win7_path = "c:/Program Files/Microsoft Windows Performance Toolkit/xperf.exe"
121    if config["xperf_path"]:
122        xperf_path = config["xperf_path"]
123        quotes = ('"', "'")
124        for quote in quotes:
125            if xperf_path.startswith(quote) and xperf_path.endswith(quote):
126                config["xperf_path"] = xperf_path[1:-1]
127                break
128        if not os.path.exists(config["xperf_path"]):
129            # look for old win7 path
130            if not os.path.exists(win7_path):
131                raise ConfigurationError(
132                    "xperf.exe cannot be found at the path specified"
133                )
134            config["xperf_path"] = win7_path
135
136
137@validator
138def set_webserver(config):
139    # pick a free port
140    import socket
141
142    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
143    sock.bind(("", 0))
144    port = sock.getsockname()[1]
145    sock.close()
146
147    config["webserver"] = "127.0.0.1:%d" % port
148
149
150@validator
151def update_prefs(config):
152    config.setdefault("preferences", {})
153
154    # update prefs from command line
155    prefs = config.pop("extraPrefs")
156    if prefs:
157        for arg in prefs:
158            k, v = arg.split("=", 1)
159            config["preferences"][k] = utils.parse_pref(v)
160
161
162@validator
163def fix_init_url(config):
164    if "init_url" in config:
165        config["init_url"] = convert_url(config, config["init_url"])
166
167
168@validator
169def determine_local_symbols_path(config):
170    if "symbols_path" not in config:
171        return
172
173    # use objdir/dist/crashreporter-symbols for symbolsPath if none provided
174    if (
175        not config["symbols_path"]
176        and config["develop"]
177        and "MOZ_DEVELOPER_OBJ_DIR" in os.environ
178    ):
179        config["symbols_path"] = os.path.join(
180            os.environ["MOZ_DEVELOPER_OBJ_DIR"], "dist", "crashreporter-symbols"
181        )
182
183
184def get_counters(config):
185    counters = set()
186    return counters
187
188
189def get_active_tests(config):
190    activeTests = config.pop("activeTests").strip().split(":")
191
192    # ensure tests are available
193    availableTests = test.test_dict()
194    if not set(activeTests).issubset(availableTests):
195        missing = [i for i in activeTests if i not in availableTests]
196        raise ConfigurationError("No definition found for test(s): %s" % missing)
197
198    return activeTests
199
200
201def get_global_overrides(config):
202    global_overrides = {}
203    for key in GLOBAL_OVERRIDES:
204        # get global overrides for all tests
205        value = config[key]
206        if value is not None:
207            global_overrides[key] = value
208        if key != "gecko_profile":
209            config.pop(key)
210
211    return global_overrides
212
213
214def get_test_host(manifest_line):
215    match = re.match(r"^http://localhost/page_load_test/tp5n/([^/]+)/", manifest_line)
216    host = match.group(1)
217    return host + "-talos"
218
219
220def build_manifest(config, is_multidomain, manifestName):
221    # read manifest lines
222    with open(manifestName, "r") as fHandle:
223        manifestLines = fHandle.readlines()
224
225    # write modified manifest lines
226    with open(manifestName + ".develop", "w") as newHandle:
227        for line in manifestLines:
228            new_host = get_test_host(line) if is_multidomain else config["webserver"]
229            newline = line.replace("localhost", new_host)
230            newline = newline.replace("page_load_test", "tests")
231            newHandle.write(newline)
232
233    newManifestName = manifestName + ".develop"
234
235    # return new manifest
236    return newManifestName
237
238
239def get_test(config, global_overrides, counters, test_instance):
240    mozAfterPaint = getattr(test_instance, "tpmozafterpaint", None)
241    hero = getattr(test_instance, "tphero", None)
242    firstPaint = getattr(test_instance, "firstpaint", None)
243    userReady = getattr(test_instance, "userready", None)
244    firstNonBlankPaint = getattr(test_instance, "fnbpaint", None)
245    pdfPaint = getattr(test_instance, "pdfpaint", None)
246
247    test_instance.update(**global_overrides)
248
249    # update original value of mozAfterPaint, this could be 'false',
250    # so check for None
251    if mozAfterPaint is not None:
252        test_instance.tpmozafterpaint = mozAfterPaint
253    if firstNonBlankPaint is not None:
254        test_instance.fnbpaint = firstNonBlankPaint
255    if firstPaint is not None:
256        test_instance.firstpaint = firstPaint
257    if userReady is not None:
258        test_instance.userready = userReady
259    if hero is not None:
260        test_instance.tphero = hero
261    if pdfPaint is not None:
262        test_instance.pdfpaint = pdfPaint
263
264    # fix up url
265    url = getattr(test_instance, "url", None)
266    if url:
267        test_instance.url = utils.interpolate(convert_url(config, url))
268
269    # fix up tpmanifest
270    tpmanifest = getattr(test_instance, "tpmanifest", None)
271    if tpmanifest:
272        is_multidomain = getattr(test_instance, "multidomain", False)
273        test_instance.tpmanifest = build_manifest(
274            config, is_multidomain, utils.interpolate(tpmanifest)
275        )
276
277    # add any counters
278    if counters:
279        keys = (
280            "linux_counters",
281            "mac_counters",
282            "win_counters",
283            "w7_counters",
284            "xperf_counters",
285        )
286        for key in keys:
287            if key not in test_instance.keys:
288                # only populate attributes that will be output
289                continue
290            if not isinstance(getattr(test_instance, key, None), list):
291                setattr(test_instance, key, [])
292            _counters = getattr(test_instance, key)
293            _counters.extend(
294                [counter for counter in counters if counter not in _counters]
295            )
296    return dict(test_instance.items())
297
298
299@validator
300def tests(config):
301    counters = get_counters(config)
302    global_overrides = get_global_overrides(config)
303    activeTests = get_active_tests(config)
304    test_dict = test.test_dict()
305
306    tests = []
307    for test_name in activeTests:
308        test_class = test_dict[test_name]
309        tests.append(get_test(config, global_overrides, counters, test_class()))
310    config["tests"] = tests
311
312
313def get_browser_config(config):
314    required = (
315        "extensions",
316        "browser_path",
317        "browser_wait",
318        "extra_args",
319        "buildid",
320        "env",
321        "init_url",
322        "webserver",
323    )
324    optional = {
325        "bcontroller_config": "${talos}/bcontroller.json",
326        "child_process": "plugin-container",
327        "debug": False,
328        "debugger": None,
329        "debugger_args": None,
330        "develop": False,
331        "enable_webrender": False,
332        "enable_fission": False,
333        "process": "",
334        "framework": "talos",
335        "repository": None,
336        "sourcestamp": None,
337        "symbols_path": None,
338        "test_timeout": 1200,
339        "xperf_path": None,
340        "error_filename": None,
341        "no_upload_results": False,
342        "stylothreads": 0,
343        "subtests": None,
344        "preferences": {},
345    }
346    browser_config = dict(title=config["title"])
347    browser_config.update(dict([(i, config[i]) for i in required]))
348    browser_config.update(dict([(i, config.get(i, j)) for i, j in optional.items()]))
349    return browser_config
350
351
352def suites_conf():
353    import json
354
355    with open(os.path.join(os.path.dirname(utils.here), "talos.json")) as f:
356        return json.load(f)["suites"]
357
358
359def get_config(argv=None):
360    argv = argv or sys.argv[1:]
361    cli_opts = parse_args(argv=argv)
362    if cli_opts.suite:
363        # read the suite config, update the args
364        try:
365            suite_conf = suites_conf()[cli_opts.suite]
366        except KeyError:
367            raise ConfigurationError("No such suite: %r" % cli_opts.suite)
368        argv += ["-a", ":".join(suite_conf["tests"])]
369        # talos_options in the suite config should not override command line
370        # options, so we prepend argv with talos_options so that, when parsed,
371        # the command line options will clobber the suite config options.
372        argv = suite_conf.get("talos_options", []) + argv
373        # args needs to be reparsed now
374    elif not cli_opts.activeTests:
375        raise ConfigurationError("--activeTests or --suite required!")
376
377    cli_opts = parse_args(argv=argv)
378    setup_logging("talos", cli_opts, {"tbpl": sys.stdout})
379    config = copy.deepcopy(DEFAULTS)
380    config.update(cli_opts.__dict__)
381    for validate in CONF_VALIDATORS:
382        validate(config)
383    # remove None Values
384    for k, v in config.copy().items():
385        if v is None:
386            del config[k]
387    return config
388
389
390def get_configs(argv=None):
391    config = get_config(argv=argv)
392    browser_config = get_browser_config(config)
393    return config, browser_config
394
395
396if __name__ == "__main__":
397    cfgs = get_configs()
398    print(cfgs[0])
399    print()
400    print(cfgs[1])
401