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, # Note: when updating the docker image version,
# make sure there are no extra old versions lying around. # make sure there are no extra old versions lying around.
# (e.g. `rg -F --hidden <old_tag>`) # (e.g. `rg -F --hidden <old_tag>`)
- image: pyodide/pyodide-env:20220504-py310-chrome101-firefox100 - image: pyodide/pyodide-env:20220525-py310-chrome102-firefox100
environment: environment:
- EMSDK_NUM_CORES: 3 - EMSDK_NUM_CORES: 3
EMCC_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", "cpp-exceptions-test",
"ssl", "ssl",
"pytest", "pytest",
"tblib",
} }
CORE_SCIPY_PACKAGES = { CORE_SCIPY_PACKAGES = {

View File

@ -3,7 +3,6 @@ import pickle
import sys import sys
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from copy import deepcopy from copy import deepcopy
from traceback import TracebackException
from typing import Any, Callable, Collection from typing import Any, Callable, Collection
from pyodide_test_runner.utils import package_is_built as _package_is_built 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 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 self._pytest_assert_rewrites = pytest_assert_rewrites
def _code_template(self, args: tuple) -> str: def _code_template(self, args: tuple) -> str:
@ -186,15 +188,20 @@ class run_in_pyodide:
co = compile(mod, {self._module_filename!r}, "exec") co = compile(mod, {self._module_filename!r}, "exec")
d = {{}} d = {{}}
exec(co, d) exec(co, d)
def encode(x):
return b64encode(pickle.dumps(x)).decode()
try: try:
result = d[{self._func_name!r}](None, *args) result = d[{self._func_name!r}](None, *args)
if {self._async_func}: if {self._async_func}:
result = await result result = await result
return [0, encode(result)]
except BaseException as e: except BaseException as e:
import traceback try:
tb = traceback.TracebackException(type(e), e, e.__traceback__) from tblib import pickling_support
serialized_err = pickle.dumps(tb) pickling_support.install()
return b64encode(serialized_err).decode() except ImportError:
pass
return [1, encode(e)]
try: try:
result = await __tmp() result = await __tmp()
@ -210,27 +217,14 @@ class run_in_pyodide:
if self._pkgs: if self._pkgs:
selenium.load_package(self._pkgs) selenium.load_package(self._pkgs)
result = selenium.run_async(code) r = selenium.run_async(code)
[status, result] = r
if result: result = pickle.loads(b64decode(result))
err: TracebackException = pickle.loads(b64decode(result)) if status:
err.stack.pop(0) # Get rid of __tmp in traceback raise result
self._fail(err) else:
return result
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)
def _generate_pyodide_ast( def _generate_pyodide_ast(
self, module_ast: ast.Module, funcname: str, func_line_no: int self, module_ast: ast.Module, funcname: str, func_line_no: int

View File

@ -1,6 +1,4 @@
import asyncio import asyncio
from dataclasses import dataclass, field
from typing import Any
import pytest import pytest
from pyodide_test_runner.decorator import run_in_pyodide 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)) return asyncio.new_event_loop().run_until_complete(eval_code_async(code))
@dataclass def test_local1():
class local_mocks_cls: with pytest.raises(AssertionError, match="assert 6 == 7"):
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):
example_func1(selenium_mock) example_func1(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
def test_local2(local_mocks): def test_local2():
example_func1(selenium_mock) with pytest.raises(AssertionError, match="assert 6 == 7"):
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n") 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) async_example_func(selenium_mock)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
def test_local_inner_function(): def test_local_inner_function():
@ -129,7 +103,7 @@ def example_decorator_func(selenium):
pass pass
def test_local4(local_mocks): def test_local4():
example_decorator_func(selenium_mock) example_decorator_func(selenium_mock)
assert example_decorator_func.dec_info == [ assert example_decorator_func.dec_info == [
("testdec1", "a"), ("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): class selenium_mock_fail_load_package(selenium_mock):
@staticmethod @staticmethod
def load_package(*args, **kwargs): def load_package(*args, **kwargs):
raise OSError("STOP!") raise OSError("STOP!")
def test_local_fail_load_package(local_mocks): def test_local_fail_load_package():
exc = None exc = None
try: try:
example_func1(selenium_mock_fail_load_package) 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) example_func1(selenium)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n") with pytest.raises(AssertionError, match="assert 6 == 7"):
example_func2(selenium) example_func2(selenium)
local_mocks.check_err(AssertionError, "AssertionError: assert 6 == 7\n")
@run_in_pyodide @run_in_pyodide

View File

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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
PYODIDE_IMAGE_REPO="pyodide" 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" PYODIDE_PREBUILT_IMAGE_TAG="0.20.0"
DEFAULT_PYODIDE_DOCKER_IMAGE="${PYODIDE_IMAGE_REPO}/pyodide-env:${PYODIDE_IMAGE_TAG}" DEFAULT_PYODIDE_DOCKER_IMAGE="${PYODIDE_IMAGE_REPO}/pyodide-env:${PYODIDE_IMAGE_TAG}"
DEFAULT_PYODIDE_SYSTEM_PORT="none" DEFAULT_PYODIDE_SYSTEM_PORT="none"