mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-22 03:35:31 -05:00
Add cache, fetch, retry logic to tests (#829)
* Add cache, fetch, retry logic to tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * run in parallel * add pytest-xdist * undo parallelism. Need to remove http server to enable. * woops a extra space * Pass flake8 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * spell fulfill * use decorator for fetch if not in cache * Fix --headed and limit to PlaywrightRequestError * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docs on cache * CICD caching of conda on unstable builds * fix config issues * empty commit to trigger gh-actions * restore build-unstable * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove http server, add parallel * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * temp: Bypass zip runtime test and point to v0.21.3 on CDN * suport for files in zip under /pyodide * remove test-one * self.http_server and remove content_type * domcontentloaded w no timeout on base url + longer timeout on wait_for_pyscript * Fixed #678 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * set default timeout to 60000 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * seamless --headed support * add test-integration-parallel and default for GHActions * simplify the code. Use http://fakeserver instead of localhost:8080 so that it's clearer that the browser is NOT hitting a real server, and use urllib to parse the url. Moreover, the special case for pyodide is no longer needed, it's automatically handled by the normal 'fakeserver' logic * The page-routing logic is becoming too much complicated to stay as an inner function. Move it to its own class, and add some logic to workaround a limitation of playwright which just hangs if a Python exception is raised inside it * no need to use a hash, we can use the url as the key * re-implement the retry logic. The old @retry decorator was nice but a bit too over-engineered and most importantly failed silently in case of exceptions. This new approach is less powerful but since we want to retry only two times, simple is better than complex -- and in case of exception, the exception is actually raised * improve logging Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Madhur Tandon <madhurtandon23@gmail.com> Co-authored-by: Antonio Cuni <anto.cuni@gmail.com>
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import pdb
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib
|
||||
from dataclasses import dataclass
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from playwright.sync_api import Error as PlaywrightError
|
||||
|
||||
ROOT = py.path.local(__file__).dirpath("..", "..", "..")
|
||||
BUILD = ROOT.join("pyscriptjs", "build")
|
||||
@@ -43,18 +48,17 @@ class PyScriptTest:
|
||||
PY_COMPLETE = "Python initialization complete"
|
||||
|
||||
@pytest.fixture()
|
||||
def init(self, request, tmpdir, http_server, logger, page):
|
||||
def init(self, request, tmpdir, logger, page):
|
||||
"""
|
||||
Fixture to automatically initialize all the tests in this class and its
|
||||
subclasses.
|
||||
|
||||
The magic is done by the decorator @pyest.mark.usefixtures("init"),
|
||||
The magic is done by the decorator @pytest.mark.usefixtures("init"),
|
||||
which tells pytest to automatically use this fixture for all the test
|
||||
method of this class.
|
||||
|
||||
Using the standard pytest behavior, we can request more fixtures:
|
||||
tmpdir, http_server and page; 'page' is a fixture provided by
|
||||
pytest-playwright.
|
||||
tmpdir, and page; 'page' is a fixture provided by pytest-playwright.
|
||||
|
||||
Then, we save these fixtures on the self and proceed with more
|
||||
initialization. The end result is that the requested fixtures are
|
||||
@@ -65,8 +69,12 @@ class PyScriptTest:
|
||||
# create a symlink to BUILD inside tmpdir
|
||||
tmpdir.join("build").mksymlinkto(BUILD)
|
||||
self.tmpdir.chdir()
|
||||
self.http_server = http_server
|
||||
self.logger = logger
|
||||
self.fake_server = "http://fake_server"
|
||||
self.router = SmartRouter(
|
||||
"fake_server", logger=logger, usepdb=request.config.option.usepdb
|
||||
)
|
||||
self.router.install(page)
|
||||
self.init_page(page)
|
||||
#
|
||||
# this extra print is useful when using pytest -s, else we start printing
|
||||
@@ -92,6 +100,10 @@ class PyScriptTest:
|
||||
|
||||
def init_page(self, page):
|
||||
self.page = page
|
||||
|
||||
# set default timeout to 60000 millliseconds from 30000
|
||||
page.set_default_timeout(60000)
|
||||
|
||||
self.console = ConsoleMessageCollection(self.logger)
|
||||
self._page_errors = []
|
||||
page.on("console", self.console.add_message)
|
||||
@@ -145,8 +157,8 @@ class PyScriptTest:
|
||||
def goto(self, path):
|
||||
self.logger.reset()
|
||||
self.logger.log("page.goto", path, color="yellow")
|
||||
url = f"{self.http_server}/{path}"
|
||||
self.page.goto(url)
|
||||
url = f"{self.fake_server}/{path}"
|
||||
self.page.goto(url, timeout=0)
|
||||
|
||||
def wait_for_console(self, text, *, timeout=None, check_errors=True):
|
||||
"""
|
||||
@@ -212,8 +224,8 @@ class PyScriptTest:
|
||||
doc = f"""
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="{self.http_server}/build/pyscript.css" />
|
||||
<script defer src="{self.http_server}/build/pyscript.js"></script>
|
||||
<link rel="stylesheet" href="{self.fake_server}/build/pyscript.css" />
|
||||
<script defer src="{self.fake_server}/build/pyscript.js"></script>
|
||||
{extra_head}
|
||||
</head>
|
||||
<body>
|
||||
@@ -411,3 +423,125 @@ class Color:
|
||||
start = f"\x1b[{color}m"
|
||||
end = "\x1b[00m"
|
||||
return start, end
|
||||
|
||||
|
||||
class SmartRouter:
|
||||
"""
|
||||
A smart router to be used in conjunction with playwright.Page.route.
|
||||
|
||||
Main features:
|
||||
|
||||
- it intercepts the requests to a local "fake server" and serve them
|
||||
statically from disk
|
||||
|
||||
- it intercepts the requests to the network and cache the results
|
||||
locally
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class CachedResponse:
|
||||
"""
|
||||
We cannot put playwright's APIResponse instances inside _cache, because
|
||||
they are valid only in the context of the same page. As a workaround,
|
||||
we manually save status, headers and body of each cached response.
|
||||
"""
|
||||
|
||||
status: int
|
||||
headers: dict
|
||||
body: str
|
||||
|
||||
# NOTE: this is a class attribute, which means that the cache is
|
||||
# automatically shared between all instances of Fake_Server (and thus all
|
||||
# tests of the pytest session)
|
||||
_cache = {}
|
||||
|
||||
def __init__(self, fake_server, *, logger, usepdb=False):
|
||||
"""
|
||||
fake_server: the domain name of the fake server
|
||||
"""
|
||||
self.fake_server = fake_server
|
||||
self.logger = logger
|
||||
self.usepdb = usepdb
|
||||
self.page = None
|
||||
|
||||
def install(self, page):
|
||||
"""
|
||||
Install the smart router on a page
|
||||
"""
|
||||
self.page = page
|
||||
self.page.route("**", self.router)
|
||||
|
||||
def router(self, route):
|
||||
"""
|
||||
Intercept and fulfill playwright requests.
|
||||
|
||||
NOTE!
|
||||
If we raise an exception inside router, playwright just hangs and the
|
||||
exception seems not to be propagated outside. It's very likely a
|
||||
playwright bug.
|
||||
|
||||
This means that for example pytest doesn't have any chance to
|
||||
intercept the exception and fail in a meaningful way.
|
||||
|
||||
As a workaround, we try to intercept exceptions by ourselves, print
|
||||
something reasonable on the console and abort the request (hoping that
|
||||
the test will fail cleaninly, that's the best we can do). We also try
|
||||
to respect pytest --pdb, for what it's possible.
|
||||
"""
|
||||
try:
|
||||
return self._router(route)
|
||||
except Exception:
|
||||
print("***** Error inside Fake_Server.router *****")
|
||||
info = sys.exc_info()
|
||||
print(traceback.format_exc())
|
||||
if self.usepdb:
|
||||
pdb.post_mortem(info[2])
|
||||
route.abort()
|
||||
|
||||
def log_request(self, status, kind, url):
|
||||
color = "blue" if status == 200 else "red"
|
||||
self.logger.log("request", f"{status} - {kind} - {url}", color=color)
|
||||
|
||||
def _router(self, route):
|
||||
full_url = route.request.url
|
||||
url = urllib.parse.urlparse(full_url)
|
||||
assert url.scheme in ("http", "https")
|
||||
|
||||
# requests to http://fake_server/ are served from the current dir and
|
||||
# never cached
|
||||
if url.netloc == self.fake_server:
|
||||
self.log_request(200, "fake_server", full_url)
|
||||
assert url.path[0] == "/"
|
||||
relative_path = url.path[1:]
|
||||
route.fulfill(status=200, path=relative_path)
|
||||
return
|
||||
|
||||
# network requests might be cached
|
||||
if full_url in self._cache:
|
||||
kind = "CACHED"
|
||||
resp = self._cache[full_url]
|
||||
else:
|
||||
kind = "NETWORK"
|
||||
resp = self.fetch_from_network(route.request)
|
||||
self._cache[full_url] = resp
|
||||
|
||||
self.log_request(resp.status, kind, full_url)
|
||||
route.fulfill(status=resp.status, headers=resp.headers, body=resp.body)
|
||||
|
||||
def fetch_from_network(self, request):
|
||||
# sometimes the network is flaky and if the first request doesn't
|
||||
# work, a subsequent one works. Instead of giving up immediately,
|
||||
# let's try twice
|
||||
try:
|
||||
api_response = self.page.request.fetch(request)
|
||||
except PlaywrightError:
|
||||
# sleep a bit and try again
|
||||
time.sleep(0.5)
|
||||
api_response = self.page.request.fetch(request)
|
||||
|
||||
cached_response = self.CachedResponse(
|
||||
status=api_response.status,
|
||||
headers=api_response.headers,
|
||||
body=api_response.body(),
|
||||
)
|
||||
return cached_response
|
||||
|
||||
Reference in New Issue
Block a user