1# Copyright 2023 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from __future__ import annotations 6 7import logging 8import threading 9from typing import TYPE_CHECKING, Iterable, Tuple 10 11from ordered_set import OrderedSet 12 13from crossbench import exception 14 15if TYPE_CHECKING: 16 from crossbench.browsers.browser import Browser 17 from crossbench.runner.groups.session import BrowserSessionRunGroup 18 from crossbench.runner.run import Run 19 from crossbench.runner.runner import Runner 20 21 22class RunThreadGroup(threading.Thread): 23 """The main interface to start Runs. 24 - Typically only a single RunThreadGroup is used. 25 - If runs are executed in parallel, multiple RunThreadGroup are used 26 """ 27 28 def __init__(self, runs: Iterable[Run], index=0, throw: bool = False) -> None: 29 super().__init__() 30 self._index = index 31 self._exceptions = exception.Annotator(throw) 32 self._runs = tuple(runs) 33 assert self._runs, "Got unexpected empty runs list" 34 self._runner: Runner = self._runs[0].runner 35 self._total_run_count = len(self._runner.runs) 36 self._browser_sessions: OrderedSet[BrowserSessionRunGroup] = OrderedSet( 37 run.browser_session for run in self._runs) 38 self.is_dry_run: bool = False 39 self._verify_contains_all_browser_session_runs() 40 self._verify_same_runner() 41 if not self._browser_sessions: 42 raise ValueError("No browser sessions / runs") 43 44 def _verify_contains_all_browser_session_runs(self) -> None: 45 runs_set = set(self._runs) 46 for browser_session in self._browser_sessions: 47 for session_run in browser_session.runs: 48 assert session_run in runs_set, ( 49 f"BrowserSession {browser_session} is not allowed to have " 50 f"{session_run} in another RunThreadGroup.") 51 52 def _verify_same_runner(self) -> None: 53 for run in self._runs: 54 assert run.runner is self._runner, "All Runs must have the same Runner." 55 56 @property 57 def index(self) -> int: 58 return self._index 59 60 @property 61 def runner(self) -> Runner: 62 return self._runner 63 64 @property 65 def runs(self) -> Tuple[Run, ...]: 66 return tuple(self._runs) 67 68 @property 69 def browser_sessions(self) -> Tuple[BrowserSessionRunGroup, ...]: 70 return tuple(self._browser_sessions) 71 72 @property 73 def browsers(self) -> Iterable[Browser]: 74 for browser_session in self._browser_sessions: 75 yield browser_session.browser 76 77 @property 78 def exceptions(self) -> exception.Annotator: 79 return self._exceptions 80 81 @property 82 def is_success(self) -> bool: 83 return self._exceptions.is_success 84 85 def _log_run(self, run: Run): 86 logging.info("=" * 80) 87 label = "" 88 if run.is_warmup: 89 label = " | WARMUP, ignoring results" 90 logging.info("RUN %s/%s%s", run.index + 1, self._total_run_count, label) 91 logging.info(" %s", run.name) 92 logging.info("=" * 80) 93 94 def run(self) -> None: 95 for browser_session in self._browser_sessions: 96 self._run_browser_session(browser_session) 97 if not browser_session.is_success: 98 self._exceptions.extend(browser_session.exceptions) 99 self.runner.exceptions.extend(self._exceptions) 100 101 def _run_browser_session(self, 102 browser_session: BrowserSessionRunGroup) -> None: 103 if browser_session.is_single_run: 104 self._log_run(browser_session.first_run) 105 else: 106 logging.info("=" * 80) 107 with browser_session.open() as is_success: 108 if not is_success: 109 self._handle_session_startup_failure(browser_session) 110 else: 111 for run in browser_session.runs: 112 self._run_browser_session_run(browser_session, run) 113 114 def _handle_session_startup_failure( 115 self, browser_session: BrowserSessionRunGroup) -> None: 116 runs = tuple(browser_session.runs) 117 logging.info("%s: Skipping %s runs due to browser session setup errors.", 118 self, len(runs)) 119 for run in runs: 120 run.exceptions.extend(browser_session.exceptions) 121 122 def _run_browser_session_run(self, browser_session: BrowserSessionRunGroup, 123 run: Run) -> None: 124 if not browser_session.is_single_run: 125 self._log_run(run) 126 if not run.is_success: 127 logging.info("%s: Skipping %s due to setup errors.", self, run) 128 else: 129 run.run(self.is_dry_run) 130 if run.is_success: 131 run.log_results() 132 else: 133 browser_session.exceptions.extend(run.exceptions) 134