Use hypothesis to test string conversions between js and python (#1339)

Co-authored-by: Roman Yurchak <rth.yurchak@gmail.com>
This commit is contained in:
Hood Chatham 2021-03-17 02:03:42 -07:00 committed by GitHub
parent f4281bf572
commit 9baffb6b7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 58 deletions

View File

@ -3,7 +3,7 @@ version: 2
defaults: &defaults
working_directory: ~/repo
docker:
- image: iodide/pyodide-env:11
- image: iodide/pyodide-env:12
environment:
- EMSDK_NUM_CORES: 4
EMCC_CORES: 4

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ firefox/
.vscode
.idea
.mypy_cache/
.hypothesis
node_modules/
build

View File

@ -9,8 +9,21 @@ RUN apt-get update \
libgconf-2-4 chromium \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 --no-cache-dir install pytest pytest-xdist pytest-instafail pytest-rerunfailures \
pytest-httpserver pytest-cov selenium PyYAML flake8 black distlib mypy "Cython<3.0"
RUN pip3 --no-cache-dir install \
black \
"cython<3.0" \
distlib \
flake8 \
hypothesis \
mypy \
pytest \
pytest-cov \
pytest-httpserver \
pytest-instafail \
pytest-rerunfailures \
pytest-xdist \
pyyaml \
selenium
# Get firefox 70.0.1 and geckodriver
RUN wget -qO- https://ftp.mozilla.org/pub/firefox/releases/70.0.1/linux-x86_64/en-US/firefox-70.0.1.tar.bz2 | tar jx \

View File

@ -227,53 +227,52 @@ class ChromeWrapper(SeleniumWrapper):
if pytest is not None:
@pytest.fixture(params=["firefox", "chrome"])
@contextlib.contextmanager
def selenium_common(request, web_server_main):
server_hostname, server_port, server_log = web_server_main
if request.param == "firefox":
cls = FirefoxWrapper
elif request.param == "chrome":
cls = ChromeWrapper
selenium = cls(
build_dir=request.config.option.build_dir,
server_port=server_port,
server_hostname=server_hostname,
server_log=server_log,
)
try:
yield selenium
finally:
selenium.driver.quit()
@pytest.fixture(params=["firefox", "chrome"], scope="function")
def selenium_standalone(request, web_server_main):
server_hostname, server_port, server_log = web_server_main
if request.param == "firefox":
cls = FirefoxWrapper
elif request.param == "chrome":
cls = ChromeWrapper
selenium = cls(
build_dir=request.config.option.build_dir,
server_port=server_port,
server_hostname=server_hostname,
server_log=server_log,
)
try:
yield selenium
finally:
print(selenium.logs)
selenium.driver.quit()
with selenium_common(request, web_server_main) as selenium:
try:
yield selenium
finally:
print(selenium.logs)
# selenium instance cached at the module level
@pytest.fixture(params=["firefox", "chrome"], scope="module")
def _selenium_cached(request, web_server_main):
# Cached selenium instance. This is a copy-paste of
# selenium_standalone to avoid fixture scope issues
server_hostname, server_port, server_log = web_server_main
if request.param == "firefox":
cls = FirefoxWrapper
elif request.param == "chrome":
cls = ChromeWrapper
selenium = cls(
build_dir=request.config.option.build_dir,
server_port=server_port,
server_hostname=server_hostname,
server_log=server_log,
)
try:
def selenium_module_scope(request, web_server_main):
with selenium_common(request, web_server_main) as selenium:
yield selenium
finally:
selenium.driver.quit()
@pytest.fixture
def selenium(_selenium_cached):
# selenium instance cached at the module level
# We want one version of this decorated as a function-scope fixture and one
# version decorated as a context manager.
def selenium_per_function(selenium_module_scope):
try:
_selenium_cached.clean_logs()
yield _selenium_cached
selenium_module_scope.clean_logs()
yield selenium_module_scope
finally:
print(_selenium_cached.logs)
print(selenium_module_scope.logs)
selenium = pytest.fixture(selenium_per_function)
# Hypothesis is unhappy with function scope fixtures. Instead, use the
# module scope fixture `selenium_module_scope` and use:
# `with selenium_context_manager(selenium_module_scope) as selenium`
selenium_context_manager = contextlib.contextmanager(selenium_per_function)
@pytest.fixture(scope="session")

View File

@ -26,7 +26,7 @@ function error() {
}
PYODIDE_IMAGE_TAG="11"
PYODIDE_IMAGE_TAG="12"
PYODIDE_PREBUILT_IMAGE_TAG="0.16.1"
DEFAULT_PYODIDE_DOCKER_IMAGE="iodide/pyodide-env:${PYODIDE_IMAGE_TAG}"
DEFAULT_PYODIDE_SYSTEM_PORT="8000"

View File

@ -1,26 +1,20 @@
# type: ignore
import platform
if platform.system() == "Emscripten":
from _pyodide_core import JsProxy, JsException, JsBuffer
from _pyodide_core import JsProxy, JsException
else:
# Can add shims here if we are so inclined.
class JsException(Exception):
class JsException(Exception): # type: ignore
"""
A wrapper around a Javascript Error to allow the Error to be thrown in Python.
"""
# Defined in jsproxy.c
class JsProxy:
class JsProxy: # type: ignore
"""A proxy to make a Javascript object behave like a Python object"""
# Defined in jsproxy.c
class JsBuffer:
"""A proxy to make it possible to call Javascript typed arrays from Python."""
# Defined in jsproxy.c
__all__ = [JsProxy, JsException]
__all__ = ["JsProxy", "JsException"]

View File

@ -4,7 +4,7 @@ import time
import contextvars
from typing import Awaitable, Callable
from typing import Callable
class WebLoop(asyncio.AbstractEventLoop):
@ -65,7 +65,7 @@ class WebLoop(asyncio.AbstractEventLoop):
"""
pass
def run_until_complete(self, future: Awaitable):
def run_until_complete(self, future):
"""Run until future is done.
If the argument is a coroutine, it is wrapped in a Task.
@ -99,7 +99,7 @@ class WebLoop(asyncio.AbstractEventLoop):
return self.call_later(delay, callback, *args, context=context)
def call_soon_threadsafe(
callback: Callable, *args, context: contextvars.Context = None
self, callback: Callable, *args, context: contextvars.Context = None
):
"""Like ``call_soon()``, but thread-safe.
@ -223,7 +223,7 @@ class WebLoop(asyncio.AbstractEventLoop):
return self._task_factory
class WebLoopPolicy(asyncio.DefaultEventLoopPolicy):
class WebLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore
"""
A simple event loop policy for managing WebLoop based event loops.
"""

View File

@ -1,5 +1,28 @@
# See also test_pyproxy, test_jsproxy, and test_python.
import pytest
from hypothesis import given
from hypothesis.strategies import text
from conftest import selenium_context_manager
@given(s=text())
def test_string_conversion(selenium_module_scope, s):
with selenium_context_manager(selenium_module_scope) as selenium:
# careful string escaping here -- hypothesis will fuzz it.
sbytes = list(s.encode())
selenium.run_js(
f"""
window.sjs = (new TextDecoder("utf8")).decode(new Uint8Array({sbytes}));
pyodide.runPython('spy = bytes({sbytes}).decode()');
"""
)
assert selenium.run_js(f"""return pyodide.runPython('spy') === sjs;""")
assert selenium.run(
"""
from js import sjs
sjs == spy
"""
)
def test_python2js(selenium):