Skip to content

Commit

Permalink
Use tblib to pickle errors in run_in_pyodide (pyodide#2619)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane authored May 27, 2022
1 parent 806e5df commit d818f41
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defaults: &defaults
# Note: when updating the docker image version,
# make sure there are no extra old versions lying around.
# (e.g. `rg -F --hidden <old_tag>`)
- image: pyodide/pyodide-env:20220504-py310-chrome101-firefox100
- image: pyodide/pyodide-env:20220525-py310-chrome102-firefox100
environment:
- EMSDK_NUM_CORES: 3
EMCC_CORES: 3
Expand Down
14 changes: 14 additions & 0 deletions packages/tblib/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package:
name: tblib
version: 1.7.0
source:
url: https://files.pythonhosted.org/packages/f8/cd/2fad4add11c8837e72f50a30e2bda30e67a10d70462f826b291443a55c7d/tblib-1.7.0-py2.py3-none-any.whl
sha256: 289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23
test:
imports:
- tblib
about:
home: https://github.com/ionelmc/python-tblib
PyPI: https://pypi.org/project/tblib
summary: Traceback serialization library.
license: BSD-2-Clause
1 change: 1 addition & 0 deletions pyodide-build/pyodide_build/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def find_matching_wheels(wheel_paths: Iterable[Path]) -> Iterator[Path]:
"cpp-exceptions-test",
"ssl",
"pytest",
"tblib",
}

CORE_SCIPY_PACKAGES = {
Expand Down
44 changes: 19 additions & 25 deletions pyodide-test-runner/pyodide_test_runner/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import sys
from base64 import b64decode, b64encode
from copy import deepcopy
from traceback import TracebackException
from typing import Any, Callable, Collection

from pyodide_test_runner.utils import package_is_built as _package_is_built
Expand Down Expand Up @@ -169,6 +168,9 @@ def __init__(
REWRITTEN_MODULE_ASTS if pytest_assert_rewrites else ORIGINAL_MODULE_ASTS
)

if package_is_built("tblib"):
self._pkgs.append("tblib")

self._pytest_assert_rewrites = pytest_assert_rewrites

def _code_template(self, args: tuple) -> str:
Expand All @@ -186,15 +188,20 @@ async def __tmp():
co = compile(mod, {self._module_filename!r}, "exec")
d = {{}}
exec(co, d)
def encode(x):
return b64encode(pickle.dumps(x)).decode()
try:
result = d[{self._func_name!r}](None, *args)
if {self._async_func}:
result = await result
return [0, encode(result)]
except BaseException as e:
import traceback
tb = traceback.TracebackException(type(e), e, e.__traceback__)
serialized_err = pickle.dumps(tb)
return b64encode(serialized_err).decode()
try:
from tblib import pickling_support
pickling_support.install()
except ImportError:
pass
return [1, encode(e)]
try:
result = await __tmp()
Expand All @@ -210,27 +217,14 @@ def _run_test(self, selenium: SeleniumType, args: tuple):
if self._pkgs:
selenium.load_package(self._pkgs)

result = selenium.run_async(code)

if result:
err: TracebackException = pickle.loads(b64decode(result))
err.stack.pop(0) # Get rid of __tmp in traceback
self._fail(err)

def _fail(self, err: TracebackException):
"""
Fail the test with a helpful message.
r = selenium.run_async(code)
[status, result] = r

Separated out for test mock purposes.
"""
msg = "Error running function in pyodide\n\n" + "".join(err.format(chain=True))
if self._pytest_not_built:
msg += (
"\n"
"Note: pytest not available in Pyodide. We could generate a"
"better traceback if pytest were available."
)
pytest.fail(msg, pytrace=False)
result = pickle.loads(b64decode(result))
if status:
raise result
else:
return result

def _generate_pyodide_ast(
self, module_ast: ast.Module, funcname: str, func_line_no: int
Expand Down
64 changes: 16 additions & 48 deletions pyodide-test-runner/pyodide_test_runner/tests/test_decorator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import asyncio
from dataclasses import dataclass, field
from typing import Any

import pytest
from pyodide_test_runner.decorator import run_in_pyodide
Expand Down Expand Up @@ -53,43 +51,19 @@ def run_async(code: str):
return asyncio.new_event_loop().run_until_complete(eval_code_async(code))


@dataclass
class local_mocks_cls:
exc_list: list[Any] = field(default_factory=list)
def test_local1():
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func1(selenium_mock)

def check_err(self, ty, msg):
try:
assert self.exc_list
err = self.exc_list[0]
assert err
assert "".join(err.format_exception_only()) == msg
finally:
del self.exc_list[0]

def _patched_fail(self, exc):
self.exc_list.append(exc)
def test_local2():
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func2(selenium_mock)


@pytest.fixture
def local_mocks(monkeypatch):
mocks = local_mocks_cls()
monkeypatch.setattr(run_in_pyodide, "_fail", mocks._patched_fail)
return mocks


def test_local1(local_mocks):
example_func1(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")


def test_local2(local_mocks):
example_func1(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")


def test_local3(local_mocks):
async_example_func(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
def test_local3():
with pytest.raises(AssertionError, match="assert 6 == 7"):
async_example_func(selenium_mock)


def test_local_inner_function():
Expand Down Expand Up @@ -129,7 +103,7 @@ def example_decorator_func(selenium):
pass


def test_local4(local_mocks):
def test_local4():
example_decorator_func(selenium_mock)
assert example_decorator_func.dec_info == [
("testdec1", "a"),
Expand All @@ -138,18 +112,13 @@ def test_local4(local_mocks):
]


def test_local5(local_mocks):
example_func1(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")


class selenium_mock_fail_load_package(selenium_mock):
@staticmethod
def load_package(*args, **kwargs):
raise OSError("STOP!")


def test_local_fail_load_package(local_mocks):
def test_local_fail_load_package():
exc = None
try:
example_func1(selenium_mock_fail_load_package)
Expand All @@ -169,13 +138,12 @@ def test_local_fail_load_package(local_mocks):
)


def test_selenium(selenium, local_mocks):
example_func1(selenium)

local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
def test_selenium(selenium):
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func1(selenium)

example_func2(selenium)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func2(selenium)


@run_in_pyodide
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
pytest-rerunfailures
pytest-xdist
selenium==4.1.0
tblib
# maintenance
bump2version
2 changes: 1 addition & 1 deletion run_docker
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash

PYODIDE_IMAGE_REPO="pyodide"
PYODIDE_IMAGE_TAG="20220504-py310-chrome101-firefox100"
PYODIDE_IMAGE_TAG="20220525-py310-chrome102-firefox100"
PYODIDE_PREBUILT_IMAGE_TAG="0.20.0"
DEFAULT_PYODIDE_DOCKER_IMAGE="${PYODIDE_IMAGE_REPO}/pyodide-env:${PYODIDE_IMAGE_TAG}"
DEFAULT_PYODIDE_SYSTEM_PORT="none"
Expand Down

0 comments on commit d818f41

Please sign in to comment.