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
51 changes: 51 additions & 0 deletions .github/workflows/javascript-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: JavaScript runtime compatibility

on:
pull_request:
paths:
- '.github/workflows/javascript-runtime.yml'
- 'src/platform/marketdata/javascript_runtime.py'
- 'tests/test_marketdata_javascript_runtime.py'
- 'requirements.txt'

permissions:
contents: read

jobs:
release-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PDF system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 \
libcairo2 libcairo-gobject2 libgdk-pixbuf-2.0-0 \
libffi-dev libfontconfig1 fonts-noto-cjk
- name: Install release dependencies
run: python -m pip install -r requirements.txt
- name: Run backend release tests
run: python -m pytest tests/ -x -q
- name: Run marketdata package tests
run: python -m pytest packages/marketdata/tests -q

native-contexts:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'ubuntu-24.04-arm']
runtime: ['py-mini-racer==0.6.0', 'mini-racer==0.14.1']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install isolated runtime dependencies
run: python -m pip install pytest '${{ matrix.runtime }}'
- name: Test serial warmup and concurrent native contexts
run: python -m pytest --noconftest tests/test_marketdata_javascript_runtime.py -q
19 changes: 18 additions & 1 deletion src/platform/marketdata/javascript_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,24 @@ def warmup_javascript_runtime() -> None:
logger.info("MiniRacer 未安装,跳过行情 JavaScript 引擎预热")
return
# init_mini_racer alone does not exercise lazy first-isolate setup.
with MiniRacer() as context:
# AkShare installs py-mini-racer 0.6.0, whose contexts have neither
# __enter__ nor close(). The maintained mini-racer package provides
# close(); release it explicitly while retaining legacy GC cleanup.
try:
context = MiniRacer()
except RuntimeError as exc:
# The legacy source distribution has no native binary on ARM.
# Optional JS data sources must not prevent the server starting.
if str(exc).startswith("Native library not available at "):
logger.warning("MiniRacer 原生库不可用,跳过行情 JavaScript 引擎预热: %s", exc)
return
raise
try:
context.eval("1 + 1")
finally:
close = getattr(context, "close", None)
if callable(close):
close()
del context
_ready = True
logger.info("行情 JavaScript 引擎预热完成")
53 changes: 47 additions & 6 deletions tests/test_marketdata_javascript_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import subprocess
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
from unittest.mock import MagicMock, Mock

import pytest

Expand All @@ -17,8 +17,39 @@ def test_warmup_initializes_and_closes_one_context(monkeypatch):
javascript_runtime.warmup_javascript_runtime()
javascript_runtime.warmup_javascript_runtime()
factory.assert_called_once_with()
factory.return_value.__enter__.return_value.eval.assert_called_once_with("1 + 1")
factory.return_value.__exit__.assert_called_once()
factory.return_value.eval.assert_called_once_with("1 + 1")
factory.return_value.close.assert_called_once_with()
factory.return_value.__enter__.assert_not_called()


def test_warmup_supports_legacy_context_without_context_manager_or_close(monkeypatch):
context = SimpleNamespace(eval=Mock(return_value=2))
factory = Mock(return_value=context)
monkeypatch.setitem(sys.modules, "py_mini_racer", SimpleNamespace(MiniRacer=factory))
monkeypatch.setattr(javascript_runtime, "_ready", False)
javascript_runtime.warmup_javascript_runtime()
javascript_runtime.warmup_javascript_runtime()
factory.assert_called_once_with()
context.eval.assert_called_once_with("1 + 1")
assert javascript_runtime._ready is True


def test_warmup_closes_context_after_evaluation_failure(monkeypatch):
context = SimpleNamespace(eval=Mock(side_effect=RuntimeError("evaluation failed")), close=Mock())
monkeypatch.setitem(sys.modules, "py_mini_racer", SimpleNamespace(MiniRacer=lambda: context))
monkeypatch.setattr(javascript_runtime, "_ready", False)
with pytest.raises(RuntimeError, match="evaluation failed"):
javascript_runtime.warmup_javascript_runtime()
context.close.assert_called_once_with()
assert javascript_runtime._ready is False


def test_warmup_allows_legacy_distribution_without_native_binary(monkeypatch):
factory = Mock(side_effect=RuntimeError('Native library not available at /tmp/libmini_racer.glibc.so'))
monkeypatch.setitem(sys.modules, 'py_mini_racer', SimpleNamespace(MiniRacer=factory))
monkeypatch.setattr(javascript_runtime, '_ready', False)
javascript_runtime.warmup_javascript_runtime()
assert javascript_runtime._ready is False


def test_warmup_failure_is_not_marked_ready(monkeypatch):
Expand All @@ -41,15 +72,25 @@ def test_fresh_process_warmup_then_concurrent_native_contexts():
pytest.importorskip("py_mini_racer")
script = """
from concurrent.futures import ThreadPoolExecutor
import sys
from py_mini_racer import MiniRacer
from src.platform.marketdata.javascript_runtime import warmup_javascript_runtime
warmup_javascript_runtime()
from src.platform.marketdata import javascript_runtime
javascript_runtime.warmup_javascript_runtime()
if not javascript_runtime._ready:
sys.exit(77)
def evaluate(_):
with MiniRacer() as context:
context = MiniRacer()
try:
return context.eval('1 + 1')
finally:
close = getattr(context, 'close', None)
if callable(close):
close()
with ThreadPoolExecutor(max_workers=8) as pool:
assert list(pool.map(evaluate, range(32))) == [2] * 32
"""
for _ in range(3):
result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=30)
if result.returncode == 77:
pytest.skip('Installed optional MiniRacer distribution has no native binary on this platform')
assert result.returncode == 0, result.stderr[-2000:]
Loading