Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 28 additions & 47 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,28 @@ defaults:
jobs:
test_with_conda:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
env:
PYJS_CODE_RUNNER_TEST_BROWSERS: ${{ matrix.browser }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4


- name: Get number of CPU cores
uses: SimenB/github-actions-cpu-cores@v1

- name: apt-install
run: |
sudo apt-get update -y && sudo apt-get install -y \
libwoff1 libvpx9 libevent-2.1-7t64 libopus0 libgstreamer-plugins-base1.0-0 \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-bad1.0-0 \
libflite1 \
libavif16 \
libharfbuzz-icu0 \
libsecret-1-0 \
libhyphen0 \
libmanette-0.2-0 \
libgles2 \
gstreamer1.0-libav

- name: Install mamba
uses: mamba-org/setup-micromamba@v2
with:
environment-file: dev-env.yml
environment-name: dev-env
init-shell: >-
bash

- name: Install Playwright
bash

# installs the browser and the system libraries it needs (via apt)
- name: Install Playwright browser
shell: bash -el {0}
run: |
playwright install
playwright install --with-deps ${{ matrix.browser }}

- name: Install pyjs-code-runner
shell: bash -el {0}
Expand All @@ -57,31 +45,19 @@ jobs:
- name: Run Tests
shell: bash -el {0}
run: |
pytest -s -vvvvv
pytest -s -vvvvv


test_with_pip:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
env:
PYJS_CODE_RUNNER_TEST_BROWSERS: ${{ matrix.browser }}
steps:
- uses: actions/checkout@v2

- name: Get number of CPU cores
uses: SimenB/github-actions-cpu-cores@v1

- name: apt-install
run: |
sudo apt-get update -y && sudo apt-get install -y \
libwoff1 libvpx9 libevent-2.1-7t64 libopus0 libgstreamer-plugins-base1.0-0 \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-bad1.0-0 \
libflite1 \
libavif16 \
libharfbuzz-icu0 \
libsecret-1-0 \
libhyphen0 \
libmanette-0.2-0 \
libgles2 \
gstreamer1.0-libav
- uses: actions/checkout@v4

- name: Install mamba
uses: mamba-org/setup-micromamba@v2
Expand All @@ -93,9 +69,14 @@ jobs:
shell: bash -el {0}
run: |
python -m pip install .
playwright install


# installs the browser and the system libraries it needs (via apt)
- name: Install Playwright browser
shell: bash -el {0}
run: |
playwright install --with-deps ${{ matrix.browser }}

- name: Run Tests
shell: bash -el {0}
run: |
pytest -s -vvvvv
pytest -s -vvvvv
3 changes: 2 additions & 1 deletion dev-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies:
- typer
- appdirs
- empack >=3,<4
- microsoft::playwright
- playwright
- playwright-python
- emsdk
- rich
29 changes: 29 additions & 0 deletions pyjs_code_runner/backend/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@ class BackendType(str, Enum):
node = "node"


class BrowserType(str, Enum):
"""Browser engines available via playwright.

`webkit` is playwright's build of WebKit, the engine behind Safari.
"""

chromium = "chromium"
firefox = "firefox"
webkit = "webkit"


async def launch_browser(playwright, browser, headless, slow_mo=None):
"""Launch `browser` (a BrowserType or its name) with playwright."""
browser = BrowserType(browser)
launcher = getattr(playwright, browser.value)
kwargs = dict(headless=headless)
if slow_mo is not None:
kwargs["slow_mo"] = slow_mo
try:
return await launcher.launch(**kwargs)
except Exception as e:
if "Executable doesn't exist" in str(e):
raise RuntimeError(
f"playwright browser `{browser.value}` is not installed, "
f"install it with:\n\n playwright install {browser.value}\n"
) from e
raise


def get_backend_type_family(backend):
if backend == BackendType.node:
return BackendFamilyType.node
Expand Down
21 changes: 14 additions & 7 deletions pyjs_code_runner/backend/browser_main/browser_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import asyncio
from ..backend_base import BackendBase
from ..backend import BrowserType, launch_browser
from ...constants import JS_DIR, HTML_DIR
from .server import server_context, find_free_port
from playwright.async_api import async_playwright
Expand All @@ -13,7 +14,15 @@

class BrowserMainBackend(BackendBase):
def __init__(
self, host_work_dir, work_dir, script, async_main, port, headless, slow_mo
self,
host_work_dir,
work_dir,
script,
async_main,
port,
headless,
slow_mo,
browser=BrowserType.chromium,
):
super().__init__(
host_work_dir=host_work_dir,
Expand All @@ -26,6 +35,7 @@ def __init__(
self.port = port
self.headless = headless
self.slow_mo = slow_mo
self.browser = BrowserType(browser)

def run(self):
browser_main_html = "browser_main.html"
Expand All @@ -43,12 +53,9 @@ def run(self):

async def playwright_run_in_main_thread(self, page_url):
async with async_playwright() as p:
if self.slow_mo is None:
browser = await p.chromium.launch(headless=self.headless)
else:
browser = await p.chromium.launch(
headless=self.headless, slow_mo=self.slow_mo
)
browser = await launch_browser(
p, self.browser, headless=self.headless, slow_mo=self.slow_mo
)
page = await browser.new_page()
await page.goto(page_url)
# n min = n_min * 60 * 1000 ms
Expand Down
108 changes: 70 additions & 38 deletions pyjs_code_runner/backend/browser_worker/browser_worker.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import os
import asyncio
from ..backend_base import BackendBase
from ..backend import BrowserType, launch_browser
from ...constants import JS_DIR, HTML_DIR
from ..browser_main.server import server_context, find_free_port
from playwright.async_api import async_playwright
from playwright.async_api import Error as PlaywrightError
from pathlib import Path
import shutil


def _is_target_closed_error(e):
# TargetClosedError is not part of playwright's public api (and older
# versions raise a plain Error with a "Target closed" message)
msg = str(e)
return (
type(e).__name__ == "TargetClosedError"
or "Target closed" in msg
or "has been closed" in msg
)


class BrowserWorkerBackend(BackendBase):
def __init__(
self, host_work_dir, work_dir, script, async_main, port, headless, slow_mo, preload_shared_libs=True
self,
host_work_dir,
work_dir,
script,
async_main,
port,
headless,
slow_mo,
preload_shared_libs=True,
browser=BrowserType.chromium,
):
super().__init__(
host_work_dir=host_work_dir,
Expand All @@ -23,6 +45,7 @@ def __init__(
self.port = port
self.headless = headless
self.slow_mo = slow_mo
self.browser = BrowserType(browser)
self.preload_shared_libs = preload_shared_libs

def run(self):
Expand All @@ -47,50 +70,26 @@ def run(self):

async def playwright_run_in_worker_thread(self, page_url):
async with async_playwright() as p:
if self.slow_mo is None:
browser = await p.chromium.launch(headless=self.headless)
else:
browser = await p.chromium.launch(
headless=self.headless, slow_mo=self.slow_mo
)
browser = await launch_browser(
p, self.browser, headless=self.headless, slow_mo=self.slow_mo
)
page = await browser.new_page()

# n min = n_min * 60 * 1000 ms
n_min = 4
page.set_default_timeout(n_min * 60 * 1000)

async def handle_worker(worker):
test_output = await worker.evaluate_handle(
f"""async () =>
{{

{self.js_utils()}

var collected_prints = ""
const print = (text) => {{
console.log(text)
collected_prints += text;
collected_prints += "\\n";
}}


var pyjs = await make_pyjs(print, print, {str(self.preload_shared_libs).lower()});

var r = eval_main_script(pyjs, "{self.work_dir}","{self.script}");
if({int(self.async_main)}){{
r = r || await run_async_python_main(pyjs);
}}

msg = {{
return_code : r,
collected_prints : collected_prints
}}
self.postMessage(msg)
return r
}}"""
)
if int(str(test_output)) != 0:
raise RuntimeError(f"tests failed with return code: {test_output}")
try:
await self._run_in_worker(worker)
except PlaywrightError as e:
if not _is_target_closed_error(e):
raise
# the page terminates the worker as soon as the worker has
# posted its result (see browser_worker.html), which may
# happen before playwright receives the evaluate result.
# The return code is read from the page below.
pass

page.on("worker", handle_worker)

Expand All @@ -117,3 +116,36 @@ async def handle_console(msg):
)
return_code = int(str(await test_output.get_property("return_code")))
return return_code

async def _run_in_worker(self, worker):
test_output = await worker.evaluate_handle(
f"""async () =>
{{

{self.js_utils()}

var collected_prints = ""
const print = (text) => {{
console.log(text)
collected_prints += text;
collected_prints += "\\n";
}}


var pyjs = await make_pyjs(print, print, {str(self.preload_shared_libs).lower()});

var r = eval_main_script(pyjs, "{self.work_dir}","{self.script}");
if({int(self.async_main)}){{
r = r || await run_async_python_main(pyjs);
}}

msg = {{
return_code : r,
collected_prints : collected_prints
}}
self.postMessage(msg)
return r
}}"""
)
if int(str(test_output)) != 0:
raise RuntimeError(f"tests failed with return code: {test_output}")
Loading
Loading