# 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 copy import json import os import pathlib import re import sys from mozlog.commandline import setup_logging from talos import test, utils from talos.cmdline import parse_args class ConfigurationError(Exception): pass DEFAULTS = dict( # args to pass to browser extra_args=[], buildid="testbuildid", init_url="getInfo.html", env={"NO_EM_RESTART": "1"}, # base data for all tests. Note that any None here will end up converted to # an empty string in useBaseTestDefaults. basetest=dict( cycles=1, profile_path="${talos}/base_profile", responsiveness=False, gecko_profile=False, gecko_profile_interval=1, gecko_profile_entries=100000, resolution=1, mainthread=False, shutdown=False, timeout=3600, tpchrome=True, tpcycles=10, tpmozafterpaint=False, tphero=False, pdfpaint=True, fnbpaint=False, firstpaint=False, format_pagename=True, userready=False, testeventmap=[], base_vs_ref=False, tppagecycles=1, tploadnocache=False, tpscrolltest=False, win_counters=[], linux_counters=[], mac_counters=[], xperf_counters=[], setup=None, cleanup=None, preferences={}, pine=True, ), ) # keys to generated self.config that are global overrides to tests GLOBAL_OVERRIDES = ( "cycles", "gecko_profile", "gecko_profile_interval", "gecko_profile_entries", "gecko_profile_features", "gecko_profile_threads", "gecko_profile_extra_threads", "tpcycles", "tppagecycles", "tpmanifest", "tptimeout", "tpmozafterpaint", "tphero", "fnbpaint", "pdfpaint", "firstpaint", "userready", ) CONF_VALIDATORS = [] def validator(func): """ A decorator that register configuration validators to execute against the configuration. They will be executed in the order of declaration. """ CONF_VALIDATORS.append(func) return func def convert_url(config, url): webserver = config["webserver"] if not webserver: return url if "://" in url: # assume a fully qualified url return url if ".html" in url: url = "http://%s/%s" % (webserver, url) return url @validator def fix_xperf(config): # BBB: remove doubly-quoted xperf values from command line # (needed for buildbot) # https://bugzilla.mozilla.org/show_bug.cgi?id=704654#c43 if config["xperf_path"]: xperf_path = config["xperf_path"] quotes = ('"', "'") for quote in quotes: if xperf_path.startswith(quote) and xperf_path.endswith(quote): config["xperf_path"] = xperf_path[1:-1] break @validator def set_webserver(config): # pick a free port import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("", 0)) port = sock.getsockname()[1] sock.close() config["webserver"] = "127.0.0.1:%d" % port @validator def update_prefs(config): config.setdefault("preferences", {}) # update prefs from command line prefs = config.pop("extraPrefs") if prefs: for arg in prefs: k, v = arg.split("=", 1) config["preferences"][k] = utils.parse_pref(v) @validator def fix_init_url(config): if "init_url" in config: config["init_url"] = convert_url(config, config["init_url"]) @validator def determine_local_symbols_path(config): if "symbols_path" not in config: return # use objdir/dist/crashreporter-symbols for symbolsPath if none provided if ( not config["symbols_path"] and config["develop"] and "MOZ_DEVELOPER_OBJ_DIR" in os.environ ): config["symbols_path"] = os.path.join( os.environ["MOZ_DEVELOPER_OBJ_DIR"], "dist", "crashreporter-symbols" ) def get_active_tests(config): activeTests = config.pop("activeTests").strip().split(":") # ensure tests are available availableTests = test.test_dict() if not set(activeTests).issubset(availableTests): missing = [i for i in activeTests if i not in availableTests] raise ConfigurationError("No definition found for test(s): %s" % missing) return activeTests def get_global_overrides(config): global_overrides = {} for key in GLOBAL_OVERRIDES: # get global overrides for all tests value = config[key] if value is not None: global_overrides[key] = value if key != "gecko_profile": config.pop(key) return global_overrides def setup_pdfpaint_test(config, test_instance): # Get the root of the location of the PDFs artifact if os.environ.get("MOZ_FETCHES_DIR", None): pdfs_root = pathlib.Path(os.environ.get("MOZ_FETCHES_DIR"), "talos-pdfs") else: pdfs_root = pathlib.Path(os.environ.get("MOZBUILD_PATH"), "talos-pdfs") if not pdfs_root.exists(): raise Exception(f"Cannot find webserver root: {pdfs_root}") pdfpaint_manifest_path = pathlib.Path(pdfs_root, "pdfpaint.manifest") test_manifest_path = pathlib.Path(pdfs_root, "test_manifest.json") test_manifest = json.loads(test_manifest_path.read_text(encoding="utf8")) # If a pdfpaint test was specified, prevent any chunking chunk_number = config.get("pdfpaint_chunk", None) pdfpaint_test = None if config.get("pdfpaint_name") is not None: chunk_number = None pdfpaint_test = config["pdfpaint_name"] # Gather all the pdf files that can be used in the test, and write # all the pdfs to be tested to the manifest file start_ind = 0 end_ind = None pdfs_per_chunk = 100 if chunk_number is not None: start_ind = pdfs_per_chunk * (chunk_number - 1) end_ind = pdfs_per_chunk * chunk_number pdf_files = set() for pdf_info in test_manifest: if pdf_info.get("password", None) is not None: # PDFs that require passwords cause timeouts continue pdf_name = pathlib.Path(pdf_info["file"]).name if ( config.get("pdfpaint_name", None) is not None and config["pdfpaint_name"] != pdf_name ): # If a user passed a name of a pdf, skip all the others continue pdf_files.add(pdf_name) if start_ind > len(pdf_files): raise ConfigurationError( f"Chunk {chunk_number} contains no PDFs to test. " f"For {len(pdf_files)} PDFs, the max chunk is " f"{int((len(pdf_files)-1)/pdfs_per_chunk)+1}." ) with pdfpaint_manifest_path.open("w") as f: for pdf_file in sorted(list(pdf_files))[start_ind:end_ind]: print(f"http://localhost/tests/pdfpaint/pdfs/{pdf_file}{os.linesep}") f.write(f"http://localhost/tests/pdfpaint/pdfs/{pdf_file}{os.linesep}") # Make a symbolic link to the mozbuild pdf folder since the talos # webserver depends on having the doc root as the talos folder for getInfo.html symlink_dest = pathlib.Path(__file__).parent / "tests" / "pdfpaint" / "pdfs" symlink_dest.unlink(missing_ok=True) symlink_dest.symlink_to(pdfs_root, target_is_directory=True) test_instance.tpmanifest = str(pdfpaint_manifest_path) # Increase the pagecycles for each pdf to 5 if we're running chunks, otherwise # it can take a very long time to complete testing of all pdfs if chunk_number is not None or pdfpaint_test is not None: print("Setting pdfpaint tppagecycles to 5") test_instance.tppagecycles = 5 def get_test_host(manifest_line): match = re.match(r"^http://localhost/page_load_test/tp5n/([^/]+)/", manifest_line) host = match.group(1) return host + "-talos" def build_manifest(config, is_multidomain, manifestName): # read manifest lines with open(manifestName, "r") as fHandle: manifestLines = fHandle.readlines() # write modified manifest lines with open(manifestName + ".develop", "w") as newHandle: for line in manifestLines: new_host = get_test_host(line) if is_multidomain else config["webserver"] newline = line.replace("localhost", new_host) newline = newline.replace("page_load_test", "tests") newHandle.write(newline) newManifestName = manifestName + ".develop" # return new manifest return newManifestName def get_test(config, global_overrides, counters, test_instance): mozAfterPaint = getattr(test_instance, "tpmozafterpaint", None) hero = getattr(test_instance, "tphero", None) firstPaint = getattr(test_instance, "firstpaint", None) userReady = getattr(test_instance, "userready", None) firstNonBlankPaint = getattr(test_instance, "fnbpaint", None) pdfPaint = getattr(test_instance, "pdfpaint", None) test_instance.update(**global_overrides) # update original value of mozAfterPaint, this could be 'false', # so check for None if mozAfterPaint is not None: test_instance.tpmozafterpaint = mozAfterPaint if firstNonBlankPaint is not None: test_instance.fnbpaint = firstNonBlankPaint if firstPaint is not None: test_instance.firstpaint = firstPaint if userReady is not None: test_instance.userready = userReady if hero is not None: test_instance.tphero = hero if pdfPaint is not None: test_instance.pdfpaint = pdfPaint if test_instance.pdfpaint: setup_pdfpaint_test(config, test_instance) # fix up url url = getattr(test_instance, "url", None) if url: test_instance.url = utils.interpolate(convert_url(config, url)) # fix up tpmanifest tpmanifest = getattr(test_instance, "tpmanifest", None) if tpmanifest: is_multidomain = getattr(test_instance, "multidomain", False) test_instance.tpmanifest = build_manifest( config, is_multidomain, utils.interpolate(tpmanifest) ) # add any counters if counters: keys = ( "linux_counters", "mac_counters", "win_counters", "xperf_counters", ) for key in keys: if key not in test_instance.keys: # only populate attributes that will be output continue if not isinstance(getattr(test_instance, key, None), list): setattr(test_instance, key, []) _counters = getattr(test_instance, key) _counters.extend( [counter for counter in counters if counter not in _counters] ) return dict(test_instance.items()) @validator def tests(config): counters = set() global_overrides = get_global_overrides(config) activeTests = get_active_tests(config) test_dict = test.test_dict() tests = [] for test_name in activeTests: test_class = test_dict[test_name] tests.append(get_test(config, global_overrides, counters, test_class())) config["tests"] = tests def get_browser_config(config): required = ( "extensions", "browser_path", "browser_wait", "extra_args", "buildid", "env", "init_url", "webserver", ) optional = { "bcontroller_config": "${talos}/bcontroller.json", "child_process": "plugin-container", "debug": False, "debugger": None, "debugger_args": None, "develop": False, "fission": True, "process": "", "framework": "talos", "repository": None, "sourcestamp": None, "symbols_path": None, "test_timeout": 1200, "xperf_path": None, "error_filename": None, "no_upload_results": False, "subtests": None, "preferences": {}, } browser_config = dict(title=config["title"]) browser_config.update(dict([(i, config[i]) for i in required])) browser_config.update(dict([(i, config.get(i, j)) for i, j in optional.items()])) return browser_config def suites_conf(): import json with open(os.path.join(os.path.dirname(utils.here), "talos.json")) as f: return json.load(f)["suites"] def get_config(argv=None): argv = argv or sys.argv[1:] cli_opts = parse_args(argv=argv) if cli_opts.suite: # read the suite config, update the args try: suite_conf = suites_conf()[cli_opts.suite] except KeyError: raise ConfigurationError("No such suite: %r" % cli_opts.suite) argv += ["-a", ":".join(suite_conf["tests"])] # talos_options in the suite config should not override command line # options, so we prepend argv with talos_options so that, when parsed, # the command line options will clobber the suite config options. argv = suite_conf.get("talos_options", []) + argv # args needs to be reparsed now elif not cli_opts.activeTests: raise ConfigurationError("--activeTests or --suite required!") if cli_opts.pdfpaint_chunk is not None and cli_opts.pdfpaint_chunk < 1: raise ConfigurationError( "pdfpaint chunk must be a positive integer greater than or equal to 1" ) cli_opts = parse_args(argv=argv) setup_logging("talos", cli_opts, {"tbpl": sys.stdout}) config = copy.deepcopy(DEFAULTS) config.update(cli_opts.__dict__) for validate in CONF_VALIDATORS: validate(config) # remove None Values for k, v in config.copy().items(): if v is None: del config[k] return config def get_configs(argv=None): config = get_config(argv=argv) browser_config = get_browser_config(config) return config, browser_config if __name__ == "__main__": cfgs = get_configs() print(cfgs[0]) print() print(cfgs[1])