Use the new testing machinery for test_examples (#676)

* WIP: start to use the PyScriptTests machinery to test the examples

* factor test_hello_world out of test_examples

* B011 forbids 'assert False' in tests because python -O remove asserts. Thank you, I knew that.

* improve test_simple_clock and remove it from test_examples

* test_altair

* test_bokeh

* rename

* kill the parametrized test_example and write individual tests for each of them

* test_kmeans it's slow, increase the timeout

* improve these xfail

* kill wait_for_load, no longer needed

* write the name of the issue

* add issue number

* add a trick which I discovered to run test interactively

* move the docstring inside the class
This commit is contained in:
Antonio Cuni
2022-08-12 02:18:42 +02:00
committed by GitHub
parent 513dfe0b42
commit 817d0edc69
4 changed files with 213 additions and 234 deletions

View File

@@ -66,6 +66,15 @@ class PyScriptTest:
# this extra print is useful when using pytest -s, else we start printing
# in the middle of the line
print()
#
# if you use pytest --headed you can see the browser page while
# playwright executes the tests. However, the page is closed very
# quickly as soon as the test finishes. If you want to pause the test
# to have time to inspect it manually, uncomment the next two
# lines. The lines after the 'yield' will be executed during the
# teardown, leaving the page open until you exit pdb.
## yield
## import pdb;pdb.set_trace()
def init_page(self, page):
self.page = page

View File

@@ -1,233 +0,0 @@
"""Each example requires the same three tests:
- Test that the initial markup loads properly (currently done by testing the <title>
tag's content)
- Testing that pyodide is loading properly
- Testing that the page contains appropriate content after rendering
The single function iterates through the examples, instantiates one playwright browser
session per example, and runs all three of each example's tests in that same browser
session.
"""
import math
import re
import time
from urllib.parse import urljoin
import pytest
from .support import ROOT
MAX_TEST_TIME = 30 # Number of seconds allowed for checking a testing condition
TEST_TIME_INCREMENT = 0.25 # 1/4 second, the length of each iteration
TEST_ITERATIONS = math.ceil(
MAX_TEST_TIME / TEST_TIME_INCREMENT
) # 120 iters of 1/4 second
# Content that is displayed in the page while pyodide loads
LOADING_MESSAGES = [
"Loading runtime...",
"Runtime created...",
"Initializing components...",
"Initializing scripts...",
]
EXAMPLES = [
"altair",
"bokeh",
"bokeh_interactive",
"d3",
"folium",
"hello_world",
"matplotlib",
"numpy_canvas_fractals",
"panel",
"panel_deckgl",
"panel_kmeans",
"panel_stream",
"repl",
"repl2",
"simple_clock",
"todo",
"todo_pylist",
"toga_freedom",
"webgl_raycaster_index",
]
TEST_PARAMS = {
"altair": {
"file": "examples/altair.html",
"pattern": '<canvas.*?class=\\"marks\\".*?>',
"title": "Altair",
},
"bokeh": {
"file": "examples/bokeh.html",
"pattern": '<div.*class=\\"bk\\".*>',
"title": "Bokeh Example",
},
"bokeh_interactive": {
"file": "examples/bokeh_interactive.html",
"pattern": '<div.*?class=\\"bk\\".*?>',
"title": "Bokeh Example",
},
"d3": {
"file": "examples/d3.html",
"pattern": "<svg.*?>",
"title": "d3: JavaScript & PyScript visualizations side-by-side",
},
"folium": {
"file": "examples/folium.html",
"pattern": "<iframe srcdoc=",
"title": "Folium",
},
"hello_world": {
"file": "examples/hello_world.html",
"pattern": "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+",
"title": "PyScript Hello World",
},
"matplotlib": {
"file": "examples/matplotlib.html",
"pattern": "<img src=['\"]data:image",
"title": "Matplotlib",
},
"numpy_canvas_fractals": {
"file": "examples/numpy_canvas_fractals.html",
"pattern": "<div.*?id=['\"](mandelbrot|julia|newton)['\"].*?>",
"title": "Visualization of Mandelbrot, Julia and "
"Newton sets with NumPy and HTML5 canvas",
},
"panel": {
"file": "examples/panel.html",
"pattern": "<div.*?class=['\"]bk-root['\"].*?>",
"title": "Panel Example",
},
"panel_deckgl": {
"file": "examples/panel_deckgl.html",
"pattern": "<div.*?class=['\"]bk-root['\"].*?>",
"title": "PyScript/Panel DeckGL Demo",
},
"panel_kmeans": {
"file": "examples/panel_kmeans.html",
"pattern": "<div.*?class=['\"]bk-root['\"].*?>",
"title": "Pyscript/Panel KMeans Demo",
},
"panel_stream": {
"file": "examples/panel_stream.html",
"pattern": "<div.*?class=['\"]bk-root['\"].*?>",
"title": "PyScript/Panel Streaming Demo",
},
"repl": {"file": "examples/repl.html", "pattern": "<py-repl.*?>", "title": "REPL"},
"repl2": {
"file": "examples/repl2.html",
"pattern": "<py-repl.*?>",
"title": "Custom REPL Example",
},
"simple_clock": {
"file": "examples/simple_clock.html",
"pattern": "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+",
"title": "Simple Clock Demo",
},
"todo": {
"file": "examples/todo.html",
"pattern": "<input.*?id=['\"]new-task-content['\"].*?>",
"title": "Todo App",
},
"todo_pylist": {
"file": "examples/todo-pylist.html",
"pattern": "<input.*?id=['\"]new-task-content['\"].*?>",
"title": "Todo App",
},
"toga_freedom": {
"file": "examples/toga/freedom.html",
"pattern": "<(main|div).*?id=['\"]toga_\\d+['\"].*?>",
"title": ["Loading...", "Freedom Units"],
},
"webgl_raycaster_index": {
"file": "examples/webgl/raycaster/index.html",
"pattern": "<canvas.*?>",
"title": "Raycaster",
},
}
def wait_for_load(page):
"""
Assert that pyscript loading messages appear.
"""
pyodide_loading = False # Flag to be set to True when condition met
for _ in range(TEST_ITERATIONS):
content = page.text_content("*")
for message in LOADING_MESSAGES:
if message in content:
pyodide_loading = True
if pyodide_loading:
break
time.sleep(TEST_TIME_INCREMENT)
assert pyodide_loading # nosec
def wait_for_render(page, selector, pattern):
"""
Assert that rendering inserts data into the page as expected: search the
DOM from within the timing loop for a string that is not present in the
initial markup but should appear by way of rendering
"""
re_sub_content = re.compile(pattern)
py_rendered = False # Flag to be set to True when condition met
for _ in range(TEST_ITERATIONS):
content = page.inner_html(selector)
if re_sub_content.search(content):
py_rendered = True
break
time.sleep(TEST_TIME_INCREMENT)
assert py_rendered # nosec
@pytest.mark.parametrize("example", EXAMPLES)
def test_examples(example, http_server, page):
# make sure that the http server serves from the right directory
ROOT.join("pyscriptjs").chdir()
base_url = http_server
example_path = urljoin(base_url, TEST_PARAMS[example]["file"])
page.goto(example_path, wait_until="commit")
title = page.title()
# STEP 1: Check page title proper initial loading of the example page
expected_title = TEST_PARAMS[example]["title"]
if isinstance(expected_title, list):
# One example's title changes so expected_title is a list of possible
# titles in that case
assert title in expected_title # nosec
else:
assert title == expected_title # nosec
# STEP 2: Test that pyodide is loading via messages displayed during loading
wait_for_load(page)
# Step 3: Wait for expected pattern to appear on page
wait_for_render(page, "*", TEST_PARAMS[example]["pattern"])
def test_simple_clock(http_server, page):
example_path = urljoin(http_server, TEST_PARAMS["simple_clock"]["file"])
page.goto(example_path, wait_until="commit")
wait_for_load(page)
pattern = r"\d{2}/\d{2}/\d{4}, \d{2}:\d{2}:\d{2}"
for _ in range(TEST_ITERATIONS):
content = page.inner_html("#outputDiv2")
if re.match(pattern, content) and int(content[-1]) in (0, 4, 8):
assert page.inner_html("#outputDiv3") == "It's espresso time!"
break
else:
time.sleep(TEST_TIME_INCREMENT)

View File

@@ -0,0 +1,203 @@
import math
import re
import time
import pytest
from .support import ROOT, PyScriptTest
MAX_TEST_TIME = 30 # Number of seconds allowed for checking a testing condition
TEST_TIME_INCREMENT = 0.25 # 1/4 second, the length of each iteration
TEST_ITERATIONS = math.ceil(
MAX_TEST_TIME / TEST_TIME_INCREMENT
) # 120 iters of 1/4 second
def wait_for_render(page, selector, pattern):
"""
Assert that rendering inserts data into the page as expected: search the
DOM from within the timing loop for a string that is not present in the
initial markup but should appear by way of rendering
"""
re_sub_content = re.compile(pattern)
py_rendered = False # Flag to be set to True when condition met
for _ in range(TEST_ITERATIONS):
content = page.inner_html(selector)
if re_sub_content.search(content):
py_rendered = True
break
time.sleep(TEST_TIME_INCREMENT)
assert py_rendered # nosec
@pytest.mark.usefixtures("chdir")
class TestExamples(PyScriptTest):
"""
Each example requires the same three tests:
- Test that the initial markup loads properly (currently done by
testing the <title> tag's content)
- Testing that pyscript is loading properly
- Testing that the page contains appropriate content after rendering
"""
@pytest.fixture()
def chdir(self):
# make sure that the http server serves from the right directory
ROOT.join("pyscriptjs").chdir()
def test_hello_world(self):
self.goto("examples/hello_world.html")
self.wait_for_pyscript()
assert self.page.title() == "PyScript Hello World"
content = self.page.content()
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
assert re.search(pattern, content)
def test_simple_clock(self):
self.goto("examples/simple_clock.html")
self.wait_for_pyscript()
assert self.page.title() == "Simple Clock Demo"
pattern = r"\d{2}/\d{2}/\d{4}, \d{2}:\d{2}:\d{2}"
# run for 5 seconds to be sure that we see the page with "It's
# espresso time!"
for _ in range(5):
content = self.page.inner_html("#outputDiv2")
if re.match(pattern, content) and int(content[-1]) in (0, 4, 8):
assert self.page.inner_html("#outputDiv3") == "It's espresso time!"
break
else:
time.sleep(1)
else:
assert False, "Espresso time not found :("
def test_altair(self):
# XXX improve this test
self.goto("examples/altair.html")
self.wait_for_pyscript()
assert self.page.title() == "Altair"
wait_for_render(self.page, "*", '<canvas.*?class=\\"marks\\".*?>')
def test_bokeh(self):
# XXX improve this test
self.goto("examples/bokeh.html")
self.wait_for_pyscript()
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*class=\\"bk\\".*>')
def test_bokeh_interactive(self):
# XXX improve this test
self.goto("examples/bokeh_interactive.html")
self.wait_for_pyscript()
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
def test_d3(self):
# XXX improve this test
self.goto("examples/d3.html")
self.wait_for_pyscript()
assert (
self.page.title() == "d3: JavaScript & PyScript visualizations side-by-side"
)
wait_for_render(self.page, "*", "<svg.*?>")
def test_folium(self):
# XXX improve this test
self.goto("examples/folium.html")
self.wait_for_pyscript()
assert self.page.title() == "Folium"
wait_for_render(self.page, "*", "<iframe srcdoc=")
def test_matplotlib(self):
# XXX improve this test
self.goto("examples/matplotlib.html")
self.wait_for_pyscript()
assert self.page.title() == "Matplotlib"
wait_for_render(self.page, "*", "<img src=['\"]data:image")
def test_numpy_canvas_fractals(self):
# XXX improve this test
self.goto("examples/numpy_canvas_fractals.html")
self.wait_for_pyscript()
assert (
self.page.title()
== "Visualization of Mandelbrot, Julia and Newton sets with NumPy and HTML5 canvas"
)
wait_for_render(
self.page, "*", "<div.*?id=['\"](mandelbrot|julia|newton)['\"].*?>"
)
def test_panel(self):
# XXX improve this test
self.goto("examples/panel.html")
self.wait_for_pyscript()
assert self.page.title() == "Panel Example"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
def test_panel_deckgl(self):
# XXX improve this test
self.goto("examples/panel_deckgl.html")
self.wait_for_pyscript()
assert self.page.title() == "PyScript/Panel DeckGL Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
def test_panel_kmeans(self):
# XXX improve this test
self.goto("examples/panel_kmeans.html")
self.wait_for_pyscript(timeout=120 * 1000)
assert self.page.title() == "Pyscript/Panel KMeans Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
@pytest.mark.xfail(reason="JsError: issue #677")
def test_panel_stream(self):
# XXX improve this test
self.goto("examples/panel_stream.html")
self.wait_for_pyscript()
assert self.page.title() == "PyScript/Panel Streaming Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
def test_repl(self):
# XXX improve this test
self.goto("examples/repl.html")
self.wait_for_pyscript()
assert self.page.title() == "REPL"
wait_for_render(self.page, "*", "<py-repl.*?>")
def test_repl2(self):
# XXX improve this test
self.goto("examples/repl2.html")
self.wait_for_pyscript()
assert self.page.title() == "Custom REPL Example"
wait_for_render(self.page, "*", "<py-repl.*?>")
def test_todo(self):
# XXX improve this test
self.goto("examples/todo.html")
self.wait_for_pyscript()
assert self.page.title() == "Todo App"
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
@pytest.mark.xfail(reason="JsError, issue #673")
def test_todo_pylist(self):
# XXX improve this test
self.goto("examples/todo-pylist.html")
self.wait_for_pyscript()
assert self.page.title() == "Todo App"
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
def test_toga_freedom(self):
# XXX improve this test
self.goto("examples/toga/freedom.html")
self.wait_for_pyscript()
assert self.page.title() in ["Loading...", "Freedom Units"]
wait_for_render(self.page, "*", "<(main|div).*?id=['\"]toga_\\d+['\"].*?>")
@pytest.mark.xfail(reason="it never finishes loading, issue #678")
def test_webgl_raycaster_index(self):
# XXX improve this test
self.goto("examples/webgl/raycaster/index.html")
self.wait_for_pyscript()
assert self.page.title() == "Raycaster"
wait_for_render(self.page, "*", "<canvas.*?>")

View File

@@ -7,4 +7,4 @@ max-complexity = 10
max-line-length = 100
show-source = True
statistics = True
extend-ignore = E731
extend-ignore = E731,B011,E266