blob: 82133f0b31207767a50b3d9cddb90e8370cdc953 [file] [log] [blame]
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import datetime as dt
import logging
import sys
from typing import TYPE_CHECKING, Any, Optional, Sequence, Union
from crossbench import helper
if TYPE_CHECKING:
from crossbench import plt
from crossbench.browsers.browser import Browser
from crossbench.exception import ExceptionAnnotationScope
from crossbench.runner.run import Run
from crossbench.runner.runner import Runner
from crossbench.runner.timing import Timing
class Actions(helper.TimeScope):
_max_end_datetime: dt.datetime
def __init__(self,
message: str,
run: Run,
runner: Optional[Runner] = None,
browser: Optional[Browser] = None,
verbose: bool = False,
measure: bool = True,
timeout: dt.timedelta = dt.timedelta()):
assert message, "Actions need a name"
super().__init__(message)
self._exception_annotation: ExceptionAnnotationScope = run.exceptions.info(
f"Action: {message}")
self._run: Run = run
self._browser: Browser = browser or run.browser
self._runner: Runner = runner or run.runner
self._is_active: bool = False
self._verbose: bool = verbose
self._measure = measure
if timeout:
self._max_end_datetime = min(dt.datetime.now() + timeout,
run.max_end_datetime())
else:
self._max_end_datetime = run.max_end_datetime()
@property
def timing(self) -> Timing:
return self._runner.timing
@property
def run(self) -> Run:
return self._run
@property
def platform(self) -> plt.Platform:
return self._run.browser_platform
def __enter__(self) -> Actions:
self._exception_annotation.__enter__()
super().__enter__()
self._is_active = True
logging.debug("Action begin: %s", self._message)
if self._verbose:
logging.info(self._message.ljust(30))
else:
# Print message that doesn't overlap with helper.Spinner
sys.stdout.write(f" {self._message.ljust(30)}\r")
return self
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
self._is_active = False
self._exception_annotation.__exit__(exc_type, exc_value, exc_traceback)
super().__exit__(exc_type, exc_value, exc_traceback)
logging.debug("Action end: %s", self._message)
if self._measure:
self.run.durations[f"actions-duration {self.message}"] = self.duration
def _assert_is_active(self) -> None:
assert self._is_active, "Actions have to be used in a with scope"
def js(self,
js_code: str,
timeout: Union[int, float, dt.timedelta] = 10,
arguments: Sequence[object] = (),
**kwargs) -> Any:
self._assert_is_active()
assert js_code, "js_code must be a valid JS script"
if kwargs:
js_code = js_code.format(**kwargs)
delta = self.timing.timeout_timedelta(timeout)
return self._browser.js(
self._runner, # pytype: disable=wrong-arg-types
js_code,
delta,
arguments=arguments)
def wait_js_condition(self, js_code: str, min_wait: Union[dt.timedelta,
float],
timeout: Union[dt.timedelta, float]) -> None:
wait_range = helper.WaitRange(
self.timing.timedelta(min_wait), self.timing.timeout_timedelta(timeout))
assert "return" in js_code, (
f"Missing return statement in js-wait code: {js_code}")
for _, time_left in wait_range.wait_with_backoff():
time_units = self.timing.units(time_left)
result = self.js(js_code, timeout=time_units, absolute_time=True)
if result:
return
assert result is False, (
f"js_code did not return a bool, but got: {result}\n"
f"js-code: {js_code}")
def show_url(self, url: str, target: Optional[str] = None) -> None:
self._assert_is_active()
if target and target != "_self":
# TODO: use target in the driver instead.
self.js(f"window.open('{url}','{target}');")
else:
self._browser.show_url(
self._runner, # pytype: disable=wrong-arg-types
url,
target=None)
def wait(
self, seconds: Union[dt.timedelta,
float] = dt.timedelta(seconds=1)) -> None:
self._assert_is_active()
self.platform.sleep(seconds)