2018-03-30 14:51:13 +00:00
|
|
|
"""
|
|
|
|
Various common utilities for testing.
|
|
|
|
"""
|
|
|
|
import pathlib
|
2022-08-08 02:24:34 +00:00
|
|
|
import re
|
2022-02-21 22:27:03 +00:00
|
|
|
import sys
|
2021-03-22 08:16:16 +00:00
|
|
|
|
2022-02-21 22:27:03 +00:00
|
|
|
import pytest
|
2022-01-24 01:47:04 +00:00
|
|
|
|
2019-06-19 18:26:08 +00:00
|
|
|
ROOT_PATH = pathlib.Path(__file__).parents[0].resolve()
|
2022-04-11 23:01:40 +00:00
|
|
|
DIST_PATH = ROOT_PATH / "dist"
|
2018-10-02 08:39:25 +00:00
|
|
|
|
2021-05-03 18:51:11 +00:00
|
|
|
sys.path.append(str(ROOT_PATH / "pyodide-build"))
|
2021-09-21 06:46:44 +00:00
|
|
|
sys.path.append(str(ROOT_PATH / "src" / "py"))
|
2018-10-04 11:24:57 +00:00
|
|
|
|
2022-08-03 04:34:25 +00:00
|
|
|
import pytest_pyodide.runner
|
2022-07-31 10:00:45 +00:00
|
|
|
from pytest_pyodide.utils import package_is_built as _package_is_built
|
2018-10-04 11:24:57 +00:00
|
|
|
|
2022-07-22 02:59:51 +00:00
|
|
|
# There are a bunch of global objects that occasionally enter the hiwire cache
|
|
|
|
# but never leave. The refcount checks get angry about them if they aren't preloaded.
|
|
|
|
# We need to go through and touch them all once to keep everything okay.
|
2022-08-03 04:34:25 +00:00
|
|
|
pytest_pyodide.runner.INITIALIZE_SCRIPT = """
|
2022-07-22 02:59:51 +00:00
|
|
|
pyodide.globals.get;
|
|
|
|
pyodide._api.pyodide_code.eval_code;
|
|
|
|
pyodide._api.pyodide_code.eval_code_async;
|
|
|
|
pyodide._api.pyodide_code.find_imports;
|
|
|
|
pyodide._api.pyodide_ffi.register_js_module;
|
|
|
|
pyodide._api.pyodide_ffi.unregister_js_module;
|
|
|
|
pyodide._api.importlib.invalidate_caches;
|
|
|
|
pyodide._api.package_loader.unpack_buffer;
|
|
|
|
pyodide._api.package_loader.get_dynlibs;
|
|
|
|
pyodide._api.package_loader.sub_resource_hash;
|
|
|
|
pyodide.runPython("");
|
|
|
|
pyodide.pyimport("pyodide.ffi.wrappers").destroy();
|
|
|
|
"""
|
|
|
|
|
2018-10-04 11:24:57 +00:00
|
|
|
|
2021-03-22 08:16:16 +00:00
|
|
|
def pytest_addoption(parser):
|
|
|
|
group = parser.getgroup("general")
|
|
|
|
group.addoption(
|
|
|
|
"--run-xfail",
|
|
|
|
action="store_true",
|
|
|
|
help="If provided, tests marked as xfail will be run",
|
|
|
|
)
|
2022-07-11 00:52:38 +00:00
|
|
|
group.addoption(
|
|
|
|
"--skip-passed",
|
|
|
|
action="store_true",
|
|
|
|
help=(
|
|
|
|
"If provided, tests that passed on the last run will be skipped. "
|
|
|
|
"CAUTION: this will skip tests even if tests are modified"
|
|
|
|
),
|
|
|
|
)
|
2021-03-22 08:16:16 +00:00
|
|
|
|
2020-06-28 18:24:40 +00:00
|
|
|
|
2022-08-08 02:24:34 +00:00
|
|
|
def maybe_skip_test(item, delayed=False):
|
|
|
|
"""If necessary skip test at the fixture level, to avoid
|
|
|
|
loading the selenium_standalone fixture which takes a long time.
|
|
|
|
"""
|
|
|
|
browsers = "|".join(["firefox", "chrome", "node"])
|
|
|
|
is_common_test = str(item.fspath).endswith("test_packages_common.py")
|
|
|
|
|
|
|
|
skip_msg = None
|
|
|
|
# Testing a package. Skip the test if the package is not built.
|
|
|
|
match = re.match(
|
|
|
|
r".*/packages/(?P<name>[\w\-]+)/test_[\w\-]+\.py", str(item.parent.fspath)
|
|
|
|
)
|
|
|
|
if match and not is_common_test:
|
|
|
|
package_name = match.group("name")
|
|
|
|
if not package_is_built(package_name) and re.match(
|
2022-08-30 07:25:13 +00:00
|
|
|
rf"test_[\w\-\.]+\[({browsers})[^\]]*\]", item.name
|
2022-08-08 02:24:34 +00:00
|
|
|
):
|
|
|
|
skip_msg = f"package '{package_name}' is not built."
|
|
|
|
|
|
|
|
# Common package import test. Skip it if the package is not built.
|
|
|
|
if skip_msg is None and is_common_test and item.name.startswith("test_import"):
|
2022-08-30 07:25:13 +00:00
|
|
|
match = re.match(rf"test_import\[({browsers})-(?P<name>[\w\-\.]+)\]", item.name)
|
2022-08-08 02:24:34 +00:00
|
|
|
if match:
|
|
|
|
package_name = match.group("name")
|
|
|
|
if not package_is_built(package_name):
|
|
|
|
# If the test is going to be skipped remove the
|
|
|
|
# selenium_standalone as it takes a long time to initialize
|
|
|
|
skip_msg = f"package '{package_name}' is not built."
|
|
|
|
else:
|
|
|
|
raise AssertionError(
|
|
|
|
f"Couldn't parse package name from {item.name}. This should not happen!"
|
|
|
|
)
|
|
|
|
|
|
|
|
# TODO: also use this hook to skip doctests we cannot run (or run them
|
|
|
|
# inside the selenium wrapper)
|
|
|
|
|
|
|
|
if skip_msg is not None:
|
|
|
|
if delayed:
|
|
|
|
item.add_marker(pytest.mark.skip(reason=skip_msg))
|
|
|
|
else:
|
|
|
|
pytest.skip(skip_msg)
|
|
|
|
|
|
|
|
|
2021-03-22 08:16:16 +00:00
|
|
|
def pytest_configure(config):
|
2021-04-19 11:39:22 +00:00
|
|
|
"""Monkey patch the function cwd_relative_nodeid
|
|
|
|
|
|
|
|
returns the description of a test for the short summary table. Monkey patch
|
|
|
|
it to reduce the verbosity of the test names in the table. This leaves
|
|
|
|
enough room to see the information about the test failure in the summary.
|
2021-03-22 08:16:16 +00:00
|
|
|
"""
|
2022-05-08 07:52:08 +00:00
|
|
|
global CONFIG
|
|
|
|
|
2021-03-22 08:16:16 +00:00
|
|
|
old_cwd_relative_nodeid = config.cwd_relative_nodeid
|
2018-03-30 14:51:13 +00:00
|
|
|
|
2021-03-22 08:16:16 +00:00
|
|
|
def cwd_relative_nodeid(*args):
|
|
|
|
result = old_cwd_relative_nodeid(*args)
|
|
|
|
result = result.replace("src/tests/", "")
|
|
|
|
result = result.replace("packages/", "")
|
|
|
|
result = result.replace("::test_", "::")
|
|
|
|
return result
|
|
|
|
|
|
|
|
config.cwd_relative_nodeid = cwd_relative_nodeid
|
2018-03-30 14:51:13 +00:00
|
|
|
|
2022-05-08 07:52:08 +00:00
|
|
|
pytest.pyodide_dist_dir = config.getoption("--dist-dir")
|
|
|
|
|
2018-03-30 14:51:13 +00:00
|
|
|
|
2021-11-21 17:26:33 +00:00
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
|
|
"""Called after collect is completed.
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
config : pytest config
|
|
|
|
items : list of collected items
|
|
|
|
"""
|
2022-07-11 00:52:38 +00:00
|
|
|
prev_test_result = {}
|
|
|
|
if config.getoption("--skip-passed"):
|
|
|
|
cache = config.cache
|
|
|
|
prev_test_result = cache.get("cache/lasttestresult", {})
|
|
|
|
|
2021-11-21 17:26:33 +00:00
|
|
|
for item in items:
|
2022-07-11 00:52:38 +00:00
|
|
|
if prev_test_result.get(item.nodeid) in ("passed", "warnings", "skip_passed"):
|
|
|
|
item.add_marker(pytest.mark.skip(reason="previously passed"))
|
|
|
|
continue
|
|
|
|
|
2022-08-08 02:24:34 +00:00
|
|
|
maybe_skip_test(item, delayed=True)
|
2021-07-20 08:48:27 +00:00
|
|
|
|
2018-07-09 19:09:49 +00:00
|
|
|
|
2022-07-11 00:52:38 +00:00
|
|
|
# Save test results to a cache
|
|
|
|
# Code adapted from: https://github.com/pytest-dev/pytest/blob/main/src/_pytest/pastebin.py
|
|
|
|
@pytest.hookimpl(trylast=True)
|
|
|
|
def pytest_terminal_summary(terminalreporter):
|
|
|
|
tr = terminalreporter
|
|
|
|
cache = tr.config.cache
|
|
|
|
assert cache
|
|
|
|
|
|
|
|
test_result = {}
|
|
|
|
for status in tr.stats:
|
|
|
|
if status in ("warnings", "deselected"):
|
|
|
|
continue
|
|
|
|
|
|
|
|
for test in tr.stats[status]:
|
2022-07-28 23:54:36 +00:00
|
|
|
|
|
|
|
if test.when != "call": # discard results from setup/teardown
|
|
|
|
continue
|
|
|
|
|
2022-07-11 00:52:38 +00:00
|
|
|
try:
|
|
|
|
if test.longrepr and test.longrepr[2] in "previously passed":
|
|
|
|
test_result[test.nodeid] = "skip_passed"
|
|
|
|
else:
|
|
|
|
test_result[test.nodeid] = test.outcome
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
|
|
|
|
cache.set("cache/lasttestresult", test_result)
|
|
|
|
|
|
|
|
|
2021-03-24 23:32:26 +00:00
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
|
|
def pytest_runtest_call(item):
|
|
|
|
"""We want to run extra verification at the start and end of each test to
|
|
|
|
check that we haven't leaked memory. According to pytest issue #5044, it's
|
|
|
|
not possible to "Fail" a test from a fixture (no matter what you do, pytest
|
|
|
|
sets the test status to "Error"). The approach suggested there is hook
|
|
|
|
pytest_runtest_call as we do here. To get access to the selenium fixture, we
|
2022-03-08 05:51:20 +00:00
|
|
|
imitate the definition of pytest_pyfunc_call:
|
2021-03-24 23:32:26 +00:00
|
|
|
https://github.com/pytest-dev/pytest/blob/6.2.2/src/_pytest/python.py#L177
|
|
|
|
|
|
|
|
Pytest issue #5044:
|
|
|
|
https://github.com/pytest-dev/pytest/issues/5044
|
|
|
|
"""
|
2022-05-08 07:52:08 +00:00
|
|
|
browser = None
|
2021-08-01 14:12:14 +00:00
|
|
|
for fixture in item._fixtureinfo.argnames:
|
|
|
|
if fixture.startswith("selenium"):
|
2022-05-08 07:52:08 +00:00
|
|
|
browser = item.funcargs[fixture]
|
2021-08-01 14:12:14 +00:00
|
|
|
break
|
2022-05-25 20:34:40 +00:00
|
|
|
|
2022-08-03 04:34:25 +00:00
|
|
|
if not browser or not browser.pyodide_loaded:
|
2021-03-24 23:32:26 +00:00
|
|
|
yield
|
2022-05-25 20:34:40 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
trace_pyproxies = pytest.mark.skip_pyproxy_check.mark not in item.own_markers
|
|
|
|
trace_hiwire_refs = (
|
|
|
|
trace_pyproxies and pytest.mark.skip_refcount_check.mark not in item.own_markers
|
|
|
|
)
|
|
|
|
yield from extra_checks_test_wrapper(browser, trace_hiwire_refs, trace_pyproxies)
|
2021-03-24 23:32:26 +00:00
|
|
|
|
|
|
|
|
2022-05-08 07:52:08 +00:00
|
|
|
def extra_checks_test_wrapper(browser, trace_hiwire_refs, trace_pyproxies):
|
2021-09-20 21:51:31 +00:00
|
|
|
"""Extra conditions for test to pass:
|
|
|
|
1. No explicit request for test to fail
|
|
|
|
2. No leaked JsRefs
|
|
|
|
3. No leaked PyProxys
|
|
|
|
"""
|
2022-05-08 07:52:08 +00:00
|
|
|
browser.clear_force_test_fail()
|
|
|
|
init_num_keys = browser.get_num_hiwire_keys()
|
2021-05-28 21:19:32 +00:00
|
|
|
if trace_pyproxies:
|
2022-05-08 07:52:08 +00:00
|
|
|
browser.enable_pyproxy_tracing()
|
|
|
|
init_num_proxies = browser.get_num_proxies()
|
2021-03-24 23:32:26 +00:00
|
|
|
a = yield
|
2021-07-20 08:48:27 +00:00
|
|
|
try:
|
|
|
|
# If these guys cause a crash because the test really screwed things up,
|
|
|
|
# we override the error message with the better message returned by
|
|
|
|
# a.result() in the finally block.
|
2022-05-08 07:52:08 +00:00
|
|
|
browser.disable_pyproxy_tracing()
|
|
|
|
browser.restore_state()
|
2021-07-20 08:48:27 +00:00
|
|
|
finally:
|
|
|
|
# if there was an error in the body of the test, flush it out by calling
|
|
|
|
# get_result (we don't want to override the error message by raising a
|
|
|
|
# different error here.)
|
|
|
|
a.get_result()
|
2022-05-08 07:52:08 +00:00
|
|
|
if browser.force_test_fail:
|
2021-09-20 21:51:31 +00:00
|
|
|
raise Exception("Test failure explicitly requested but no error was raised.")
|
2021-07-10 22:05:29 +00:00
|
|
|
if trace_pyproxies and trace_hiwire_refs:
|
2022-05-08 07:52:08 +00:00
|
|
|
delta_proxies = browser.get_num_proxies() - init_num_proxies
|
|
|
|
delta_keys = browser.get_num_hiwire_keys() - init_num_keys
|
2021-07-10 22:05:29 +00:00
|
|
|
assert (delta_proxies, delta_keys) == (0, 0) or delta_keys < 0
|
2021-06-07 07:23:47 +00:00
|
|
|
if trace_hiwire_refs:
|
2022-05-08 07:52:08 +00:00
|
|
|
delta_keys = browser.get_num_hiwire_keys() - init_num_keys
|
2021-07-10 22:05:29 +00:00
|
|
|
assert delta_keys <= 0
|
2021-03-24 23:32:26 +00:00
|
|
|
|
|
|
|
|
2022-05-08 07:52:08 +00:00
|
|
|
def package_is_built(package_name):
|
|
|
|
return _package_is_built(package_name, pytest.pyodide_dist_dir)
|