Use tblib to pickle errors in `run_in_pyodide` (#2619)

This commit is contained in:
Hood Chatham 2022-05-27 12:28:46 -07:00 committed by GitHub
parent 806e5dff61
commit d818f410c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 53 additions and 75 deletions

View File

@ -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

14
packages/tblib/meta.yaml Normal file
View File

@ -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

View File

@ -76,6 +76,7 @@ CORE_PACKAGES = {
"cpp-exceptions-test",
"ssl",
"pytest",
"tblib",
}
CORE_SCIPY_PACKAGES = {

View File

@ -3,7 +3,6 @@ import pickle
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
@ -169,6 +168,9 @@ class run_in_pyodide:
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:
@ -186,15 +188,20 @@ class run_in_pyodide:
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()
@ -210,27 +217,14 @@ class run_in_pyodide:
if self._pkgs:
selenium.load_package(self._pkgs)
result = selenium.run_async(code)
r = selenium.run_async(code)
[status, result] = r
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.
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

View File

@ -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
@ -53,43 +51,19 @@ class selenium_mock:
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 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)
@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):
def test_local1():
with pytest.raises(AssertionError, match="assert 6 == 7"):
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_local2():
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func2(selenium_mock)
def test_local3(local_mocks):
def test_local3():
with pytest.raises(AssertionError, match="assert 6 == 7"):
async_example_func(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
def test_local_inner_function():
@ -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"),
@ -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)
@ -169,13 +138,12 @@ def test_local_fail_load_package(local_mocks):
)
def test_selenium(selenium, local_mocks):
def test_selenium(selenium):
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func1(selenium)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func2(selenium)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
@run_in_pyodide

View File

@ -15,5 +15,6 @@
pytest-rerunfailures
pytest-xdist
selenium==4.1.0
tblib
# maintenance
bump2version

View File

@ -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"