testing.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. """reflex.testing - tools for testing reflex apps."""
  2. from __future__ import annotations
  3. import asyncio
  4. import contextlib
  5. import dataclasses
  6. import inspect
  7. import os
  8. import pathlib
  9. import platform
  10. import re
  11. import signal
  12. import socket
  13. import socketserver
  14. import subprocess
  15. import textwrap
  16. import threading
  17. import time
  18. import types
  19. from http.server import SimpleHTTPRequestHandler
  20. from typing import (
  21. TYPE_CHECKING,
  22. AsyncIterator,
  23. Callable,
  24. Coroutine,
  25. Optional,
  26. Type,
  27. TypeVar,
  28. Union,
  29. )
  30. import psutil
  31. import uvicorn
  32. import reflex
  33. import reflex.reflex
  34. import reflex.utils.build
  35. import reflex.utils.exec
  36. import reflex.utils.prerequisites
  37. import reflex.utils.processes
  38. from reflex.state import State, StateManagerMemory, StateManagerRedis
  39. try:
  40. from selenium import webdriver # pyright: ignore [reportMissingImports]
  41. from selenium.webdriver.remote.webdriver import ( # pyright: ignore [reportMissingImports]
  42. WebDriver,
  43. )
  44. if TYPE_CHECKING:
  45. from selenium.webdriver.remote.webelement import ( # pyright: ignore [reportMissingImports]
  46. WebElement,
  47. )
  48. has_selenium = True
  49. except ImportError:
  50. has_selenium = False
  51. DEFAULT_TIMEOUT = 10
  52. POLL_INTERVAL = 0.25
  53. FRONTEND_POPEN_ARGS = {}
  54. T = TypeVar("T")
  55. TimeoutType = Optional[Union[int, float]]
  56. if platform.system == "Windows":
  57. FRONTEND_POPEN_ARGS["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
  58. else:
  59. FRONTEND_POPEN_ARGS["start_new_session"] = True
  60. # borrowed from py3.11
  61. class chdir(contextlib.AbstractContextManager):
  62. """Non thread-safe context manager to change the current working directory."""
  63. def __init__(self, path):
  64. """Prepare contextmanager.
  65. Args:
  66. path: the path to change to
  67. """
  68. self.path = path
  69. self._old_cwd = []
  70. def __enter__(self):
  71. """Save current directory and perform chdir."""
  72. self._old_cwd.append(os.getcwd())
  73. os.chdir(self.path)
  74. def __exit__(self, *excinfo):
  75. """Change back to previous directory on stack.
  76. Args:
  77. excinfo: sys.exc_info captured in the context block
  78. """
  79. os.chdir(self._old_cwd.pop())
  80. @dataclasses.dataclass
  81. class AppHarness:
  82. """AppHarness executes a reflex app in-process for testing."""
  83. app_name: str
  84. app_source: Optional[types.FunctionType | types.ModuleType]
  85. app_path: pathlib.Path
  86. app_module_path: pathlib.Path
  87. app_module: Optional[types.ModuleType] = None
  88. app_instance: Optional[reflex.App] = None
  89. frontend_process: Optional[subprocess.Popen] = None
  90. frontend_url: Optional[str] = None
  91. frontend_output_thread: Optional[threading.Thread] = None
  92. backend_thread: Optional[threading.Thread] = None
  93. backend: Optional[uvicorn.Server] = None
  94. state_manager: Optional[StateManagerMemory | StateManagerRedis] = None
  95. _frontends: list["WebDriver"] = dataclasses.field(default_factory=list)
  96. @classmethod
  97. def create(
  98. cls,
  99. root: pathlib.Path,
  100. app_source: Optional[types.FunctionType | types.ModuleType] = None,
  101. app_name: Optional[str] = None,
  102. ) -> "AppHarness":
  103. """Create an AppHarness instance at root.
  104. Args:
  105. root: the directory that will contain the app under test.
  106. app_source: if specified, the source code from this function or module is used
  107. as the main module for the app. If unspecified, then root must already
  108. contain a working reflex app and will be used directly.
  109. app_name: provide the name of the app, otherwise will be derived from app_source or root.
  110. Returns:
  111. AppHarness instance
  112. """
  113. if app_name is None:
  114. if app_source is None:
  115. app_name = root.name.lower()
  116. else:
  117. app_name = app_source.__name__.lower()
  118. return cls(
  119. app_name=app_name,
  120. app_source=app_source,
  121. app_path=root,
  122. app_module_path=root / app_name / f"{app_name}.py",
  123. )
  124. def _initialize_app(self):
  125. os.environ["TELEMETRY_ENABLED"] = "" # disable telemetry reporting for tests
  126. self.app_path.mkdir(parents=True, exist_ok=True)
  127. if self.app_source is not None:
  128. # get the source from a function or module object
  129. source_code = textwrap.dedent(
  130. "".join(inspect.getsource(self.app_source).splitlines(True)[1:]),
  131. )
  132. with chdir(self.app_path):
  133. reflex.reflex._init(
  134. name=self.app_name,
  135. template=reflex.constants.Templates.Kind.BLANK,
  136. loglevel=reflex.constants.LogLevel.INFO,
  137. )
  138. self.app_module_path.write_text(source_code)
  139. with chdir(self.app_path):
  140. # ensure config and app are reloaded when testing different app
  141. reflex.config.get_config(reload=True)
  142. self.app_module = reflex.utils.prerequisites.get_app(reload=True)
  143. self.app_instance = self.app_module.app
  144. if isinstance(self.app_instance.state_manager, StateManagerRedis):
  145. # Create our own redis connection for testing.
  146. self.state_manager = StateManagerRedis.create(self.app_instance.state)
  147. else:
  148. self.state_manager = self.app_instance.state_manager
  149. def _get_backend_shutdown_handler(self):
  150. if self.backend is None:
  151. raise RuntimeError("Backend was not initialized.")
  152. original_shutdown = self.backend.shutdown
  153. async def _shutdown_redis(*args, **kwargs) -> None:
  154. # ensure redis is closed before event loop
  155. if self.app_instance is not None and isinstance(
  156. self.app_instance.state_manager, StateManagerRedis
  157. ):
  158. await self.app_instance.state_manager.redis.close()
  159. await original_shutdown(*args, **kwargs)
  160. return _shutdown_redis
  161. def _start_backend(self, port=0):
  162. if self.app_instance is None:
  163. raise RuntimeError("App was not initialized.")
  164. self.backend = uvicorn.Server(
  165. uvicorn.Config(
  166. app=self.app_instance.api,
  167. host="127.0.0.1",
  168. port=port,
  169. )
  170. )
  171. self.backend.shutdown = self._get_backend_shutdown_handler()
  172. self.backend_thread = threading.Thread(target=self.backend.run)
  173. self.backend_thread.start()
  174. def _start_frontend(self):
  175. # Set up the frontend.
  176. with chdir(self.app_path):
  177. config = reflex.config.get_config()
  178. config.api_url = "http://{0}:{1}".format(
  179. *self._poll_for_servers().getsockname(),
  180. )
  181. reflex.utils.build.setup_frontend(self.app_path)
  182. # Start the frontend.
  183. self.frontend_process = reflex.utils.processes.new_process(
  184. [reflex.utils.prerequisites.get_package_manager(), "run", "dev"],
  185. cwd=self.app_path / reflex.constants.Dirs.WEB,
  186. env={"PORT": "0"},
  187. **FRONTEND_POPEN_ARGS,
  188. )
  189. def _wait_frontend(self):
  190. while self.frontend_url is None:
  191. line = (
  192. self.frontend_process.stdout.readline() # pyright: ignore [reportOptionalMemberAccess]
  193. )
  194. if not line:
  195. break
  196. print(line) # for pytest diagnosis
  197. m = re.search(reflex.constants.Next.FRONTEND_LISTENING_REGEX, line)
  198. if m is not None:
  199. self.frontend_url = m.group(1)
  200. break
  201. if self.frontend_url is None:
  202. raise RuntimeError("Frontend did not start")
  203. def consume_frontend_output():
  204. while True:
  205. line = (
  206. self.frontend_process.stdout.readline() # pyright: ignore [reportOptionalMemberAccess]
  207. )
  208. if not line:
  209. break
  210. print(line)
  211. self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
  212. self.frontend_output_thread.start()
  213. def start(self) -> "AppHarness":
  214. """Start the backend in a new thread and dev frontend as a separate process.
  215. Returns:
  216. self
  217. """
  218. self._initialize_app()
  219. self._start_backend()
  220. self._start_frontend()
  221. self._wait_frontend()
  222. return self
  223. def __enter__(self) -> "AppHarness":
  224. """Contextmanager protocol for `start()`.
  225. Returns:
  226. Instance of AppHarness after calling start()
  227. """
  228. return self.start()
  229. def stop(self) -> None:
  230. """Stop the frontend and backend servers."""
  231. if self.backend is not None:
  232. self.backend.should_exit = True
  233. if self.frontend_process is not None:
  234. # https://stackoverflow.com/a/70565806
  235. frontend_children = psutil.Process(self.frontend_process.pid).children(
  236. recursive=True,
  237. )
  238. if platform.system() == "Windows":
  239. self.frontend_process.terminate()
  240. else:
  241. pgrp = os.getpgid(self.frontend_process.pid)
  242. os.killpg(pgrp, signal.SIGTERM)
  243. # kill any remaining child processes
  244. for child in frontend_children:
  245. # It's okay if the process is already gone.
  246. with contextlib.suppress(psutil.NoSuchProcess):
  247. child.terminate()
  248. _, still_alive = psutil.wait_procs(frontend_children, timeout=3)
  249. for child in still_alive:
  250. # It's okay if the process is already gone.
  251. with contextlib.suppress(psutil.NoSuchProcess):
  252. child.kill()
  253. # wait for main process to exit
  254. self.frontend_process.communicate()
  255. if self.backend_thread is not None:
  256. self.backend_thread.join()
  257. if self.frontend_output_thread is not None:
  258. self.frontend_output_thread.join()
  259. for driver in self._frontends:
  260. driver.quit()
  261. def __exit__(self, *excinfo) -> None:
  262. """Contextmanager protocol for `stop()`.
  263. Args:
  264. excinfo: sys.exc_info captured in the context block
  265. """
  266. self.stop()
  267. @staticmethod
  268. def _poll_for(
  269. target: Callable[[], T],
  270. timeout: TimeoutType = None,
  271. step: TimeoutType = None,
  272. ) -> T | bool:
  273. """Generic polling logic.
  274. Args:
  275. target: callable that returns truthy if polling condition is met.
  276. timeout: max polling time
  277. step: interval between checking target()
  278. Returns:
  279. return value of target() if truthy within timeout
  280. False if timeout elapses
  281. """
  282. if timeout is None:
  283. timeout = DEFAULT_TIMEOUT
  284. if step is None:
  285. step = POLL_INTERVAL
  286. deadline = time.time() + timeout
  287. while time.time() < deadline:
  288. success = target()
  289. if success:
  290. return success
  291. time.sleep(step)
  292. return False
  293. @staticmethod
  294. async def _poll_for_async(
  295. target: Callable[[], Coroutine[None, None, T]],
  296. timeout: TimeoutType = None,
  297. step: TimeoutType = None,
  298. ) -> T | bool:
  299. """Generic polling logic for async functions.
  300. Args:
  301. target: callable that returns truthy if polling condition is met.
  302. timeout: max polling time
  303. step: interval between checking target()
  304. Returns:
  305. return value of target() if truthy within timeout
  306. False if timeout elapses
  307. """
  308. if timeout is None:
  309. timeout = DEFAULT_TIMEOUT
  310. if step is None:
  311. step = POLL_INTERVAL
  312. deadline = time.time() + timeout
  313. while time.time() < deadline:
  314. success = await target()
  315. if success:
  316. return success
  317. await asyncio.sleep(step)
  318. return False
  319. def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket:
  320. """Poll backend server for listening sockets.
  321. Args:
  322. timeout: how long to wait for listening socket.
  323. Returns:
  324. first active listening socket on the backend
  325. Raises:
  326. RuntimeError: when the backend hasn't started running
  327. TimeoutError: when server or sockets are not ready
  328. """
  329. if self.backend is None:
  330. raise RuntimeError("Backend is not running.")
  331. backend = self.backend
  332. # check for servers to be initialized
  333. if not self._poll_for(
  334. target=lambda: getattr(backend, "servers", False),
  335. timeout=timeout,
  336. ):
  337. raise TimeoutError("Backend servers are not initialized.")
  338. # check for sockets to be listening
  339. if not self._poll_for(
  340. target=lambda: getattr(backend.servers[0], "sockets", False),
  341. timeout=timeout,
  342. ):
  343. raise TimeoutError("Backend is not listening.")
  344. return backend.servers[0].sockets[0]
  345. def frontend(self, driver_clz: Optional[Type["WebDriver"]] = None) -> "WebDriver":
  346. """Get a selenium webdriver instance pointed at the app.
  347. Args:
  348. driver_clz: webdriver.Chrome (default), webdriver.Firefox, webdriver.Safari,
  349. webdriver.Edge, etc
  350. Returns:
  351. Instance of the given webdriver navigated to the frontend url of the app.
  352. Raises:
  353. RuntimeError: when selenium is not importable or frontend is not running
  354. """
  355. if not has_selenium:
  356. raise RuntimeError(
  357. "Frontend functionality requires `selenium` to be installed, "
  358. "and it could not be imported."
  359. )
  360. if self.frontend_url is None:
  361. raise RuntimeError("Frontend is not running.")
  362. want_headless = False
  363. options = None
  364. if os.environ.get("APP_HARNESS_HEADLESS"):
  365. want_headless = True
  366. if driver_clz is None:
  367. requested_driver = os.environ.get("APP_HARNESS_DRIVER", "Chrome")
  368. driver_clz = getattr(webdriver, requested_driver)
  369. if driver_clz is webdriver.Chrome and want_headless:
  370. options = webdriver.ChromeOptions()
  371. options.add_argument("--headless=new")
  372. elif driver_clz is webdriver.Firefox and want_headless:
  373. options = webdriver.FirefoxOptions()
  374. options.add_argument("-headless")
  375. elif driver_clz is webdriver.Edge and want_headless:
  376. options = webdriver.EdgeOptions()
  377. options.add_argument("headless")
  378. driver = driver_clz(options=options) # type: ignore
  379. driver.get(self.frontend_url)
  380. self._frontends.append(driver)
  381. return driver
  382. async def get_state(self, token: str) -> State:
  383. """Get the state associated with the given token.
  384. Args:
  385. token: The state token to look up.
  386. Returns:
  387. The state instance associated with the given token
  388. Raises:
  389. RuntimeError: when the app hasn't started running
  390. """
  391. if self.state_manager is None:
  392. raise RuntimeError("state_manager is not set.")
  393. try:
  394. return await self.state_manager.get_state(token)
  395. finally:
  396. if isinstance(self.state_manager, StateManagerRedis):
  397. await self.state_manager.redis.close()
  398. async def set_state(self, token: str, **kwargs) -> None:
  399. """Set the state associated with the given token.
  400. Args:
  401. token: The state token to set.
  402. kwargs: Attributes to set on the state.
  403. Raises:
  404. RuntimeError: when the app hasn't started running
  405. """
  406. if self.state_manager is None:
  407. raise RuntimeError("state_manager is not set.")
  408. state = await self.get_state(token)
  409. for key, value in kwargs.items():
  410. setattr(state, key, value)
  411. try:
  412. await self.state_manager.set_state(token, state)
  413. finally:
  414. if isinstance(self.state_manager, StateManagerRedis):
  415. await self.state_manager.redis.close()
  416. @contextlib.asynccontextmanager
  417. async def modify_state(self, token: str) -> AsyncIterator[State]:
  418. """Modify the state associated with the given token and send update to frontend.
  419. Args:
  420. token: The state token to modify
  421. Yields:
  422. The state instance associated with the given token
  423. Raises:
  424. RuntimeError: when the app hasn't started running
  425. """
  426. if self.state_manager is None:
  427. raise RuntimeError("state_manager is not set.")
  428. if self.app_instance is None:
  429. raise RuntimeError("App is not running.")
  430. app_state_manager = self.app_instance.state_manager
  431. if isinstance(self.state_manager, StateManagerRedis):
  432. # Temporarily replace the app's state manager with our own, since
  433. # the redis connection is on the backend_thread event loop
  434. self.app_instance._state_manager = self.state_manager
  435. try:
  436. async with self.app_instance.modify_state(token) as state:
  437. yield state
  438. finally:
  439. if isinstance(self.state_manager, StateManagerRedis):
  440. self.app_instance._state_manager = app_state_manager
  441. await self.state_manager.redis.close()
  442. def poll_for_content(
  443. self,
  444. element: "WebElement",
  445. timeout: TimeoutType = None,
  446. exp_not_equal: str = "",
  447. ) -> str:
  448. """Poll element.text for change.
  449. Args:
  450. element: selenium webdriver element to check
  451. timeout: how long to poll element.text
  452. exp_not_equal: exit the polling loop when the element text does not match
  453. Returns:
  454. The element text when the polling loop exited
  455. Raises:
  456. TimeoutError: when the timeout expires before text changes
  457. """
  458. if not self._poll_for(
  459. target=lambda: element.text != exp_not_equal,
  460. timeout=timeout,
  461. ):
  462. raise TimeoutError(
  463. f"{element} content remains {exp_not_equal!r} while polling.",
  464. )
  465. return element.text
  466. def poll_for_value(
  467. self,
  468. element: "WebElement",
  469. timeout: TimeoutType = None,
  470. exp_not_equal: str = "",
  471. ) -> Optional[str]:
  472. """Poll element.get_attribute("value") for change.
  473. Args:
  474. element: selenium webdriver element to check
  475. timeout: how long to poll element value attribute
  476. exp_not_equal: exit the polling loop when the value does not match
  477. Returns:
  478. The element value when the polling loop exited
  479. Raises:
  480. TimeoutError: when the timeout expires before value changes
  481. """
  482. if not self._poll_for(
  483. target=lambda: element.get_attribute("value") != exp_not_equal,
  484. timeout=timeout,
  485. ):
  486. raise TimeoutError(
  487. f"{element} content remains {exp_not_equal!r} while polling.",
  488. )
  489. return element.get_attribute("value")
  490. def poll_for_clients(self, timeout: TimeoutType = None) -> dict[str, reflex.State]:
  491. """Poll app state_manager for any connected clients.
  492. Args:
  493. timeout: how long to wait for client states
  494. Returns:
  495. active state instances when the polling loop exited
  496. Raises:
  497. RuntimeError: when the app hasn't started running
  498. TimeoutError: when the timeout expires before any states are seen
  499. """
  500. if self.app_instance is None:
  501. raise RuntimeError("App is not running.")
  502. state_manager = self.app_instance.state_manager
  503. assert isinstance(
  504. state_manager, StateManagerMemory
  505. ), "Only works with memory state manager"
  506. if not self._poll_for(
  507. target=lambda: state_manager.states,
  508. timeout=timeout,
  509. ):
  510. raise TimeoutError("No states were observed while polling.")
  511. return state_manager.states
  512. class SimpleHTTPRequestHandlerCustomErrors(SimpleHTTPRequestHandler):
  513. """SimpleHTTPRequestHandler with custom error page handling."""
  514. def __init__(self, *args, error_page_map: dict[int, pathlib.Path], **kwargs):
  515. """Initialize the handler.
  516. Args:
  517. error_page_map: map of error code to error page path
  518. *args: passed through to superclass
  519. **kwargs: passed through to superclass
  520. """
  521. self.error_page_map = error_page_map
  522. super().__init__(*args, **kwargs)
  523. def send_error(
  524. self, code: int, message: str | None = None, explain: str | None = None
  525. ) -> None:
  526. """Send the error page for the given error code.
  527. If the code matches a custom error page, then message and explain are
  528. ignored.
  529. Args:
  530. code: the error code
  531. message: the error message
  532. explain: the error explanation
  533. """
  534. error_page = self.error_page_map.get(code)
  535. if error_page:
  536. self.send_response(code, message)
  537. self.send_header("Connection", "close")
  538. body = error_page.read_bytes()
  539. self.send_header("Content-Type", self.error_content_type)
  540. self.send_header("Content-Length", str(len(body)))
  541. self.end_headers()
  542. self.wfile.write(body)
  543. else:
  544. super().send_error(code, message, explain)
  545. class Subdir404TCPServer(socketserver.TCPServer):
  546. """TCPServer for SimpleHTTPRequestHandlerCustomErrors that serves from a subdir."""
  547. def __init__(
  548. self,
  549. *args,
  550. root: pathlib.Path,
  551. error_page_map: dict[int, pathlib.Path] | None,
  552. **kwargs,
  553. ):
  554. """Initialize the server.
  555. Args:
  556. root: the root directory to serve from
  557. error_page_map: map of error code to error page path
  558. *args: passed through to superclass
  559. **kwargs: passed through to superclass
  560. """
  561. self.root = root
  562. self.error_page_map = error_page_map or {}
  563. super().__init__(*args, **kwargs)
  564. def finish_request(self, request: socket.socket, client_address: tuple[str, int]):
  565. """Finish one request by instantiating RequestHandlerClass.
  566. Args:
  567. request: the requesting socket
  568. client_address: (host, port) referring to the client’s address.
  569. """
  570. self.RequestHandlerClass(
  571. request,
  572. client_address,
  573. self,
  574. directory=str(self.root), # type: ignore
  575. error_page_map=self.error_page_map, # type: ignore
  576. )
  577. class AppHarnessProd(AppHarness):
  578. """AppHarnessProd executes a reflex app in-process for testing.
  579. In prod mode, instead of running `next dev` the app is exported as static
  580. files and served via the builtin python http.server with custom 404 redirect
  581. handling. Additionally, the backend runs in multi-worker mode.
  582. """
  583. frontend_thread: Optional[threading.Thread] = None
  584. frontend_server: Optional[Subdir404TCPServer] = None
  585. def _run_frontend(self):
  586. web_root = self.app_path / reflex.constants.Dirs.WEB / "_static"
  587. error_page_map = {
  588. 404: web_root / "404.html",
  589. }
  590. with Subdir404TCPServer(
  591. ("", 0),
  592. SimpleHTTPRequestHandlerCustomErrors,
  593. root=web_root,
  594. error_page_map=error_page_map,
  595. ) as self.frontend_server:
  596. self.frontend_url = "http://localhost:{1}".format(
  597. *self.frontend_server.socket.getsockname()
  598. )
  599. self.frontend_server.serve_forever()
  600. def _start_frontend(self):
  601. # Set up the frontend.
  602. with chdir(self.app_path):
  603. config = reflex.config.get_config()
  604. config.api_url = "http://{0}:{1}".format(
  605. *self._poll_for_servers().getsockname(),
  606. )
  607. reflex.reflex.export(
  608. zipping=False,
  609. frontend=True,
  610. backend=False,
  611. loglevel=reflex.constants.LogLevel.INFO,
  612. )
  613. self.frontend_thread = threading.Thread(target=self._run_frontend)
  614. self.frontend_thread.start()
  615. def _wait_frontend(self):
  616. self._poll_for(lambda: self.frontend_server is not None)
  617. if self.frontend_server is None or not self.frontend_server.socket.fileno():
  618. raise RuntimeError("Frontend did not start")
  619. def _start_backend(self):
  620. if self.app_instance is None:
  621. raise RuntimeError("App was not initialized.")
  622. os.environ[reflex.constants.SKIP_COMPILE_ENV_VAR] = "yes"
  623. self.backend = uvicorn.Server(
  624. uvicorn.Config(
  625. app=self.app_instance,
  626. host="127.0.0.1",
  627. port=0,
  628. workers=reflex.utils.processes.get_num_workers(),
  629. ),
  630. )
  631. self.backend.shutdown = self._get_backend_shutdown_handler()
  632. self.backend_thread = threading.Thread(target=self.backend.run)
  633. self.backend_thread.start()
  634. def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket:
  635. try:
  636. return super()._poll_for_servers(timeout)
  637. finally:
  638. os.environ.pop(reflex.constants.SKIP_COMPILE_ENV_VAR, None)
  639. def stop(self):
  640. """Stop the frontend python webserver."""
  641. super().stop()
  642. if self.frontend_server is not None:
  643. self.frontend_server.shutdown()
  644. if self.frontend_thread is not None:
  645. self.frontend_thread.join()