commit d93c8a92267d146b9838e5f54261de44240cffe5
parent 93fe6627e2fa68ce7cfbabdc0b75a72a434972d8
Author: Sam Sneddon <gsnedders@apple.com>
Date: Wed, 3 Dec 2025 14:41:33 +0000
Bug 1999064 [wpt PR 55950] - Make wptrunner.product.Product have a full constructor, a=testonly
Automatic update from web-platform-tests
Make wptrunner.product.Product have a full constructor
This opens us up to having Product objects that don't come from our
existing code path, and provides clearer documentation for what the
interface of a Product is meant to be.
--
wpt-commits: 638e3323132fc6f469ffe83535d36c200b52f61a
wpt-pr: 55950
Diffstat:
6 files changed, 304 insertions(+), 39 deletions(-)
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/wktr.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/wktr.py
@@ -18,7 +18,7 @@ from ..executors.executorwktr import ( # noqa: F401
)
-__wptrunner__ = {"product": "WebKitTestRunner",
+__wptrunner__ = {"product": "wktr",
"check_args": "check_args",
"browser": "WKTRBrowser",
"executor": {
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/deprecated.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/deprecated.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+import sys
+from typing import TYPE_CHECKING, TypeVar
+
+if sys.version_info >= (3, 13):
+ from warnings import deprecated as deprecated
+elif TYPE_CHECKING:
+ from typing_extensions import deprecated as deprecated
+else:
+ _T = TypeVar("_T")
+
+ class deprecated:
+ def __init__(
+ self,
+ message: str,
+ /,
+ *,
+ category: type[Warning] | None = DeprecationWarning,
+ stacklevel: int = 1,
+ ) -> None:
+ self.message = message
+ self.category = category
+ self.stacklevel = stacklevel
+
+ def __call__(self, f: _T) -> _T:
+ return f
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/products.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
@@ -1,12 +1,117 @@
-# mypy: allow-untyped-defs
+from __future__ import annotations
+
import importlib
+import warnings
+from dataclasses import dataclass
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Protocol,
+ TypedDict,
+ overload,
+)
from .browsers import product_list
+from .deprecated import deprecated
+
+if TYPE_CHECKING:
+ import sys
+ from types import ModuleType
+
+ from mozlog.structuredlog import StructuredLogger
+ from wptserve.config import Config
+
+ from .browsers import base as browsers_base
+ from .environment import TestEnvironment
+ from .executors.base import TestExecutor
+ from .testloader import Subsuite
+
+ if sys.version_info >= (3, 9):
+ from collections.abc import Mapping, Sequence
+ else:
+ from typing import Mapping, Sequence
+
+ if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+ else:
+ from typing_extensions import TypeAlias
+
+
+JSON: TypeAlias = "Mapping[str, 'JSON'] | Sequence['JSON'] | str | int | float | bool | None"
+
+
+class CheckArgs(Protocol):
+ def __call__(self, **kwargs: Any) -> None:
+ ...
+
+
+class EnvExtras(Protocol):
+ def __call__(self, **kwargs: Any) -> Sequence[object]:
+ ...
+
+
+class BrowserKwargs(Protocol):
+ def __call__(
+ self,
+ logger: StructuredLogger,
+ test_type: str,
+ run_info_data: Mapping[str, JSON],
+ *,
+ config: Config,
+ subsuite: Subsuite,
+ **kwargs: Any,
+ ) -> Mapping[str, object]:
+ ...
+
+
+class ExecutorKwargs(Protocol):
+ def __call__(
+ self,
+ logger: StructuredLogger,
+ test_type: str,
+ test_environment: TestEnvironment,
+ run_info_data: Mapping[str, JSON],
+ *,
+ subsuite: Subsuite,
+ **kwargs: Any,
+ ) -> Mapping[str, object]:
+ ...
+
+
+class RunInfoExtras(Protocol):
+ def __call__(
+ self, logger: StructuredLogger, **kwargs: Any
+ ) -> Mapping[str, JSON]:
+ ...
+
+
+class TimeoutMultiplier(Protocol):
+ def __call__(
+ self, test_type: str, run_info_data: Mapping[str, JSON], **kwargs: Any
+ ) -> float:
+ ...
+
+class _WptrunnerModuleDictOptional(TypedDict, total=False):
+ run_info_extras: str
+ update_properties: str
-def product_module(config, product):
+
+class WptrunnerModuleDict(_WptrunnerModuleDictOptional):
+ product: str
+ browser: str | Mapping[str | None, str]
+ check_args: str
+ browser_kwargs: str
+ executor_kwargs: str
+ env_options: str
+ env_extras: str
+ timeout_multiplier: str
+ executor: Mapping[str, str]
+
+
+def _product_module(product: str) -> ModuleType:
if product not in product_list:
- raise ValueError("Unknown product %s" % product)
+ raise ValueError(f"Unknown product {product!r}")
module = importlib.import_module("wptrunner.browsers." + product)
if not hasattr(module, "__wptrunner__"):
@@ -15,35 +120,150 @@ def product_module(config, product):
return module
+def default_run_info_extras(logger: StructuredLogger, **kwargs: Any) -> Mapping[str, JSON]:
+ return {}
+
+
+_legacy_product_msg = "Use Product.from_product_name(name) instead of Product(config, name)"
+
+
+@dataclass
class Product:
- def __init__(self, config, product):
- module = product_module(config, product)
- data = module.__wptrunner__
- self.name = product
- if isinstance(data["browser"], str):
- self._browser_cls = {None: getattr(module, data["browser"])}
+ name: str
+ browser_classes: Mapping[str | None, type[browsers_base.Browser]]
+ check_args: CheckArgs
+ get_browser_kwargs: BrowserKwargs
+ get_executor_kwargs: ExecutorKwargs
+ env_options: Mapping[str, Any]
+ get_env_extras: EnvExtras
+ get_timeout_multiplier: TimeoutMultiplier
+ executor_classes: Mapping[str, type[TestExecutor]]
+ run_info_extras: RunInfoExtras
+ update_properties: tuple[Sequence[str], Mapping[str, Sequence[str]]]
+
+ @overload
+ @deprecated(_legacy_product_msg, category=None)
+ def __init__(
+ self,
+ config: object,
+ legacy_name: str,
+ /,
+ *,
+ _do_not_use_allow_legacy_name_call: bool = False,
+ ) -> None:
+ ...
+
+ @overload
+ def __init__(
+ self,
+ name: str,
+ *,
+ browser_classes: Mapping[str | None, type[browsers_base.Browser]],
+ check_args: CheckArgs,
+ get_browser_kwargs: BrowserKwargs,
+ get_executor_kwargs: ExecutorKwargs,
+ env_options: Mapping[str, Any],
+ get_env_extras: EnvExtras,
+ get_timeout_multiplier: TimeoutMultiplier,
+ executor_classes: Mapping[str, type[TestExecutor]],
+ run_info_extras: None | RunInfoExtras = None,
+ update_properties: None | tuple[Sequence[str], Mapping[str, Sequence[str]]] = None,
+ ) -> None:
+ ...
+
+ def __init__(
+ self,
+ name: object,
+ _legacy_name: None | str = None,
+ *,
+ browser_classes: None | Mapping[str | None, type[browsers_base.Browser]] = None,
+ check_args: None | CheckArgs = None,
+ get_browser_kwargs: None | BrowserKwargs = None,
+ get_executor_kwargs: None | ExecutorKwargs = None,
+ env_options: None | Mapping[str, Any] = None,
+ get_env_extras: None | EnvExtras = None,
+ get_timeout_multiplier: None | TimeoutMultiplier = None,
+ executor_classes: None | Mapping[str, type[TestExecutor]] = None,
+ run_info_extras: None | RunInfoExtras = None,
+ update_properties: None | tuple[Sequence[str], Mapping[str, Sequence[str]]] = None,
+ _do_not_use_allow_legacy_name_call: bool = False,
+ ) -> None:
+ if _legacy_name is None:
+ assert isinstance(name, str)
else:
- self._browser_cls = {key: getattr(module, value)
- for key, value in data["browser"].items()}
- self.check_args = getattr(module, data["check_args"])
- self.get_browser_kwargs = getattr(module, data["browser_kwargs"])
- self.get_executor_kwargs = getattr(module, data["executor_kwargs"])
- self.env_options = getattr(module, data["env_options"])()
- self.get_env_extras = getattr(module, data["env_extras"])
- self.run_info_extras = (getattr(module, data["run_info_extras"])
- if "run_info_extras" in data else lambda product, **kwargs:{})
- self.get_timeout_multiplier = getattr(module, data["timeout_multiplier"])
-
- self.executor_classes = {}
- for test_type, cls_name in data["executor"].items():
- cls = getattr(module, cls_name)
- self.executor_classes[test_type] = cls
-
- self.update_properties = (getattr(module, data["update_properties"])()
- if "update_properties" in data else (["product"], {}))
-
-
- def get_browser_cls(self, test_type):
+ if not _do_not_use_allow_legacy_name_call:
+ warnings.warn(_legacy_product_msg, category=DeprecationWarning, stacklevel=2)
+
+ module = _product_module(_legacy_name)
+ data: WptrunnerModuleDict = module.__wptrunner__
+
+ name = data["product"]
+ if name != _legacy_name:
+ msg = f"Product {_legacy_name!r} calls itself {name!r}, which differs"
+ raise ValueError(msg)
+ browser_classes = (
+ {None: getattr(module, data["browser"])}
+ if isinstance(data["browser"], str)
+ else {
+ key: getattr(module, value)
+ for key, value in data["browser"].items()
+ }
+ )
+ check_args = getattr(module, data["check_args"])
+ get_browser_kwargs = getattr(module, data["browser_kwargs"])
+ get_executor_kwargs = getattr(module, data["executor_kwargs"])
+ env_options = getattr(module, data["env_options"])()
+ get_env_extras = getattr(module, data["env_extras"])
+ get_timeout_multiplier = getattr(module, data["timeout_multiplier"])
+ executor_classes = {
+ test_type: getattr(module, cls_name)
+ for test_type, cls_name in data["executor"].items()
+ }
+ run_info_extras = (
+ getattr(module, data["run_info_extras"])
+ if "run_info_extras" in data
+ else None
+ )
+ update_properties = (
+ getattr(module, data["update_properties"])()
+ if "update_properties" in data
+ else None
+ )
+
+ assert browser_classes is not None
+ assert check_args is not None
+ assert get_browser_kwargs is not None
+ assert get_executor_kwargs is not None
+ assert env_options is not None
+ assert get_env_extras is not None
+ assert get_timeout_multiplier is not None
+ assert executor_classes is not None
+
+ self.name = name
+ self._browser_cls = browser_classes
+ self.check_args = check_args
+ self.get_browser_kwargs = get_browser_kwargs
+ self.get_executor_kwargs = get_executor_kwargs
+ self.env_options = env_options
+ self.get_env_extras = get_env_extras
+ self.get_timeout_multiplier = get_timeout_multiplier
+ self.executor_classes = executor_classes
+
+ if run_info_extras is not None:
+ self.run_info_extras = run_info_extras
+ else:
+ self.run_info_extras = default_run_info_extras
+
+ if update_properties is not None:
+ self.update_properties = update_properties
+ else:
+ self.update_properties = (["product"], {})
+
+ @classmethod
+ def from_product_name(cls, name: str) -> Product:
+ return cls(None, name, _do_not_use_allow_legacy_name_call=True)
+
+ def get_browser_cls(self, test_type: str) -> type[browsers_base.Browser]:
if test_type in self._browser_cls:
return self._browser_cls[test_type]
return self._browser_cls[None]
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/browsers/test_webkitgtk.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/browsers/test_webkitgtk.py
@@ -30,7 +30,7 @@ def test_webkitgtk_certificate_domain_list(product):
if product not in ["epiphany", "webkit", "webkitgtk_minibrowser"]:
pytest.skip("%s doesn't support certificate_domain_list" % product)
- product_data = products.Product({}, product)
+ product_data = products.Product.from_product_name(product)
cert_file = "/home/user/wpt/tools/certs/cacert.pem"
valid_domains_test = ["a.example.org", "b.example.org", "example.org",
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_products.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_products.py
@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs, allow-untyped-calls
+import warnings
from os.path import join, dirname
from unittest import mock
@@ -19,24 +20,41 @@ environment.do_delayed_imports(None, test_paths)
@active_products("product")
def test_load_active_product(product):
"""test we can successfully load the product of the current testenv"""
- products.Product({}, product)
+ products.Product.from_product_name(product)
# test passes if it doesn't throw
@all_products("product")
def test_load_all_products(product):
"""test every product either loads or throws ImportError"""
- try:
- products.Product({}, product)
- except ImportError:
- pass
+ with warnings.catch_warnings():
+ # This acts to ensure that we don't get a DeprecationWarning here.
+ warnings.filterwarnings(
+ "error",
+ message=r"Use Product\.from_product_name",
+ category=DeprecationWarning,
+ )
+ try:
+ products.Product.from_product_name(product)
+ except ImportError:
+ pass
+
+
+@all_products("product")
+def test_load_all_products_deprecated(product):
+ """test every product causes a DeprecationWarning"""
+ with pytest.deprecated_call(match=r"Use Product\.from_product_name"):
+ try:
+ products.Product({}, product)
+ except ImportError:
+ pass
@active_products("product", marks={
"sauce": pytest.mark.skip("needs env extras kwargs"),
})
def test_server_start_config(product):
- product_data = products.Product({}, product)
+ product_data = products.Product.from_product_name(product)
env_extras = product_data.get_env_extras()
diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptcommandline.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptcommandline.py
@@ -452,7 +452,7 @@ def set_from_config(kwargs):
kwargs["config"] = config.read(kwargs["config_path"])
- kwargs["product"] = products.Product(kwargs["config"], kwargs["product"])
+ kwargs["product"] = products.Product.from_product_name(kwargs["product"])
keys = {"paths": [("prefs", "prefs_root", "path"),
("run_info", "run_info", "path"),