diff --git a/src/workerd/server/tests/python/BUILD.bazel b/src/workerd/server/tests/python/BUILD.bazel index 386ea963098..b92c6f731e3 100644 --- a/src/workerd/server/tests/python/BUILD.bazel +++ b/src/workerd/server/tests/python/BUILD.bazel @@ -3,14 +3,13 @@ load("//src/workerd/server/tests/python:py_wd_test.bzl", "py_wd_test", "python_t python_test_setup() +# Each test takes a while to start up, so if possible add additional tests to many-tests or sdk or +# some other existing test. py_wd_test("hello") -py_wd_test("env-param") +py_wd_test("many-tests") -py_wd_test( - "top-level-env", - skip_python_flags = ["0.26.0a2"], -) +py_wd_test("env-param") py_wd_test("asgi") @@ -22,8 +21,6 @@ py_wd_test("subdirectory") py_wd_test("sdk") -py_wd_test("seek-metadatafs") - gen_rust_import_tests() py_wd_test("undefined-handler") @@ -50,18 +47,12 @@ py_wd_test( skip_python_flags = ["0.26.0a2"], ) -py_wd_test("js-import") - -py_wd_test("importable-env") - py_wd_test("python-rpc") py_wd_test("workflow-entrypoint") py_wd_test("vendor_dir_compat_flag") -py_wd_test("multiprocessing") - py_wd_test("default-class-with-legacy-global-handlers") py_wd_test( diff --git a/src/workerd/server/tests/python/importable-env/worker.py b/src/workerd/server/tests/python/importable-env/worker.py deleted file mode 100644 index f7e7a1dd506..00000000000 --- a/src/workerd/server/tests/python/importable-env/worker.py +++ /dev/null @@ -1,112 +0,0 @@ -from workers import import_from_javascript - -from pyodide import __version__ -from pyodide.ffi import create_proxy, to_js - -# Then test the regular import function -workers_module = import_from_javascript("cloudflare:workers") -env = workers_module.env -assert env.FOO == "BAR", "env.FOO should be 'BAR'" -sockets_module = import_from_javascript("cloudflare:sockets") - -vectorize = None -threw = False -try: - vectorize = import_from_javascript("cloudflare:vectorize") -except ImportError: - threw = True - - -if __version__ == "0.26.0a2": - # Should have thrown throw "ImportError: Failed to import 'cloudflare:vectorize': Only 'cloudflare:workers' and 'cloudflare:sockets' are available until the next python runtime version." - msg = 'import_from_javascript("cloudflare:vectorize") not expected to work in the global scope in 0.26.0a2' - assert threw, msg - assert vectorize is None, msg -else: - assert {"DistanceMetric", "KnownModel", "MetadataRetrievalLevel"}.issubset( - dir(vectorize) - ) - -from js import setTimeout - -from pyodide.ffi import create_once_callable - - -def f(): - pass - - -# Checks for the patch to top level setTimeout() that is needed for top level -# `import_from_javascript``. -# 1. Test that setTimeout with a zero timeout works at top level -setTimeout(create_once_callable(f), 0) - -# 2. Test that setTimeout with nonzero timeout fails with the expected error. -err = None -try: - setTimeout(create_once_callable(f), 1) -except Exception as e: - err = e -finally: - assert err - assert str(err).startswith("Error: Disallowed operation called within global scope") - del err - - -async def test(): - # Test sockets module if available - assert hasattr(sockets_module, "connect"), ( - "sockets_module should have 'connect' attribute" - ) - setTimeout(create_once_callable(f), 1) - - # Test that cache exists and is accessible - assert env.CACHE, "env.CACHE should exist" - - # Assert that non existent modules throw ImportError - try: - import_from_javascript("crypto") - raise RuntimeError('import_from_javascript("crypto") should not work') - except ImportError: - pass - - if __version__ != "0.26.0a2": - # to_js doesn't work correctly in 0.26.0a2 for some reason - # Test accessing the cache - async def memory_cache_read(x): - from js import Date - - return to_js( - { - "value": 123, - "expiration": Date.now() + 10000, - } - ) - - cached = await env.CACHE.read( - "hello", - create_proxy(memory_cache_read), - ) - assert cached == 123, "cached value should be 123" - - # Test withEnv - async def import_with_env(): - workers_module = import_from_javascript("cloudflare:workers") - env = workers_module.env - return env.FOO - - result = await workers_module.withEnv(to_js({"FOO": 1}), import_with_env) - assert result == 1 - - if __version__ == "0.26.0a2": - try: - vectorize = import_from_javascript("cloudflare:vectorize") - except ImportError as e: - assert ( - e.args[0] - == "Failed to import 'cloudflare:vectorize': Only 'cloudflare:workers' and 'cloudflare:sockets' are available until the next python runtime version." - ) - else: - # Assert that imports that depend on JSPI work as expected - vectorize = import_from_javascript("cloudflare:vectorize") - assert hasattr(vectorize, "DistanceMetric") diff --git a/src/workerd/server/tests/python/js-import/js-import.wd-test b/src/workerd/server/tests/python/js-import/js-import.wd-test deleted file mode 100644 index bdddb798b67..00000000000 --- a/src/workerd/server/tests/python/js-import/js-import.wd-test +++ /dev/null @@ -1,18 +0,0 @@ -using Workerd = import "/workerd/workerd.capnp"; - -const unitTests :Workerd.Config = ( - services = [ - ( name = "js-import-test", - worker = ( - modules = [ - (name = "worker.py", pythonModule = embed "worker.py"), - (name = "child", esModule = "export const value = 'Child module loaded successfully';"), - ], - compatibilityFlags = [%PYTHON_FEATURE_FLAGS, "disable_python_no_global_handlers"], - bindings = [ - (name = "TEST_VALUE", text = "TEST_STRING"), - ], - ) - ), - ], -); diff --git a/src/workerd/server/tests/python/js-import/worker.py b/src/workerd/server/tests/python/js-import/worker.py deleted file mode 100644 index fc0c2736c94..00000000000 --- a/src/workerd/server/tests/python/js-import/worker.py +++ /dev/null @@ -1,23 +0,0 @@ -from workers import import_from_javascript - - -async def test(): - # Import the env module from cloudflare:workers - env_module = import_from_javascript("cloudflare:workers") - - # Test that we can access the imported module's exports - assert hasattr(env_module, "env"), "env property should exist" - assert hasattr(env_module, "withEnv"), "withEnv function should exist" - - # Test with actual env values - assert env_module.env.TEST_VALUE == "TEST_STRING", ( - "TEST_VALUE should be correctly set" - ) - - # Try importing an unsafe module which should fail - try: - import_from_javascript("internal:unsafe-eval") - raise AssertionError("Unsafe import should have failed but succeeded") - except ImportError: - # This is expected to fail, so the test passes - pass diff --git a/src/workerd/server/tests/python/importable-env/importable-env.wd-test b/src/workerd/server/tests/python/many-tests/env.wd-test similarity index 75% rename from src/workerd/server/tests/python/importable-env/importable-env.wd-test rename to src/workerd/server/tests/python/many-tests/env.wd-test index 1618b55816f..a4d209f6b82 100644 --- a/src/workerd/server/tests/python/importable-env/importable-env.wd-test +++ b/src/workerd/server/tests/python/many-tests/env.wd-test @@ -2,18 +2,23 @@ using Workerd = import "/workerd/workerd.capnp"; const unitTests :Workerd.Config = ( services = [ - ( name = "importable-env-test", + ( name = "python-hello", worker = ( modules = [ - (name = "worker.py", pythonModule = embed "worker.py"), + (name = "worker.py", pythonModule = embed "worker.py") ], compatibilityFlags = [ %PYTHON_FEATURE_FLAGS, - "disable_python_no_global_handlers", + "python_no_global_handlers", "unwrap_custom_thenables" ], bindings = [ + ( + name = "secret", + text = "thisisasecret" + ), (name = "FOO", text = "BAR"), + (name = "TEST_VALUE", text = "TEST_STRING"), (name = "CACHE", memoryCache = ( id = "abc123", limits = ( diff --git a/src/workerd/server/tests/python/many-tests/worker.py b/src/workerd/server/tests/python/many-tests/worker.py new file mode 100644 index 00000000000..a23fb67f89b --- /dev/null +++ b/src/workerd/server/tests/python/many-tests/worker.py @@ -0,0 +1,233 @@ +# Check that multiprocessing top level import works +import multiprocessing +import os +from pathlib import Path + +from js import Date, Reflect, setTimeout +from workers import WorkerEntrypoint, env, import_from_javascript, patch_env + +from pyodide import __version__ +from pyodide.ffi import create_once_callable, create_proxy, to_js + + +def use(x): + pass + + +def f(): + pass + + +def top_level_test(): + # Test the regular import function + workers_module = import_from_javascript("cloudflare:workers") + env = workers_module.env + assert env.FOO == "BAR", "env.FOO should be 'BAR'" + sockets_module = import_from_javascript("cloudflare:sockets") + + vectorize = None + threw = False + try: + vectorize = import_from_javascript("cloudflare:vectorize") + except ImportError: + threw = True + + if __version__ == "0.26.0a2": + # Should have thrown throw "ImportError: Failed to import 'cloudflare:vectorize': Only 'cloudflare:workers' and 'cloudflare:sockets' are available until the next python runtime version." + msg = 'import_from_javascript("cloudflare:vectorize") not expected to work in the global scope in 0.26.0a2' + assert threw, msg + assert vectorize is None, msg + else: + assert {"DistanceMetric", "KnownModel", "MetadataRetrievalLevel"}.issubset( + dir(vectorize) + ) + + from js import setTimeout + + from pyodide.ffi import create_once_callable + + # Checks for the patch to top level setTimeout() that is needed for top level + # `import_from_javascript``. + # 1. Test that setTimeout with a zero timeout works at top level + setTimeout(create_once_callable(f), 0) + + # 2. Test that setTimeout with nonzero timeout fails with the expected error. + err = None + try: + setTimeout(create_once_callable(f), 1) + except Exception as e: + err = e + finally: + assert err + assert str(err).startswith( + "Error: Disallowed operation called within global scope" + ) + del err + return [workers_module, env, sockets_module, vectorize] + + +# Check serialization of modules +TOP_LEVEL = top_level_test() + + +def test_import_from_javascript(): + # Import the env module from cloudflare:workers + env_module = import_from_javascript("cloudflare:workers") + + # Test that we can access the imported module's exports + assert hasattr(env_module, "env"), "env property should exist" + assert hasattr(env_module, "withEnv"), "withEnv function should exist" + + # Test with actual env values + assert env_module.env.TEST_VALUE == "TEST_STRING", ( + "TEST_VALUE should be correctly set" + ) + + # Test sockets module + sockets_module = import_from_javascript("cloudflare:sockets") + assert hasattr(sockets_module, "connect"), ( + "sockets_module should have 'connect' attribute" + ) + setTimeout(create_once_callable(f), 1) + + +def test_import_vectorize(): + if __version__ == "0.26.0a2": + try: + vectorize = import_from_javascript("cloudflare:vectorize") + except ImportError as e: + assert ( + e.args[0] + == "Failed to import 'cloudflare:vectorize': Only 'cloudflare:workers' and 'cloudflare:sockets' are available until the next python runtime version." + ) + else: + # Assert that imports that depend on JSPI work as expected + vectorize = import_from_javascript("cloudflare:vectorize") + assert hasattr(vectorize, "DistanceMetric") + + +def test_import_failures(): + # Try importing an unsafe module which should fail + try: + import_from_javascript("internal:unsafe-eval") + raise AssertionError("Unsafe import should have failed but succeeded") + except ImportError: + # This is expected to fail, so the test passes + pass + + # Assert that non existent modules throw ImportError + try: + import_from_javascript("crypto") + raise RuntimeError('import_from_javascript("crypto") should not work') + except ImportError: + pass + + +async def test_memory_cache(): + # Test that cache exists and is accessible + assert env.CACHE, "env.CACHE should exist" + if __version__ == "0.26.0a2": + # to_js doesn't work correctly in 0.26.0a2 for some reason + return + + # Test accessing the cache + async def memory_cache_read(x): + return to_js( + { + "value": 123, + "expiration": Date.now() + 10000, + } + ) + + cached = await env.CACHE.read( + "hello", + create_proxy(memory_cache_read), + ) + assert cached == 123, "cached value should be 123" + + +async def test_with_env(): + if __version__ == "0.26.0a2": + # doesn't work correctly in 0.26.0a2 for some reason + return + workers_module = import_from_javascript("cloudflare:workers") + + # Test withEnv + async def import_with_env(): + workers_module = import_from_javascript("cloudflare:workers") + env = workers_module.env + return env.FOO + + result = await workers_module.withEnv(to_js({"FOO": 1}), import_with_env) + assert result == 1 + + +def check_normal(): + assert env.secret == "thisisasecret" + assert not hasattr(env, "NAME") + assert not hasattr(env, "place") + assert not hasattr(env, "extra") + assert set(Reflect.ownKeys(env).to_py()).issuperset({"secret"}) + + +def test_patch_env(): + if __version__ == "0.26.0a2": + # doesn't work correctly in 0.26.0a2 for some reason + return + check_normal() + + with patch_env(NAME=1, place="Somewhere"): + assert env.NAME == 1 + assert env.place == "Somewhere" + assert not hasattr(env, "secret") + + check_normal() + + with patch_env({"NAME": 1, "place": "Somewhere"}): + assert env.NAME == 1 + assert env.place == "Somewhere" + assert not hasattr(env, "secret") + + check_normal() + + with patch_env([("NAME", 1), ("place", "Somewhere")]): + assert env.NAME == 1 + assert env.place == "Somewhere" + assert not hasattr(env, "secret") + + check_normal() + + with patch_env({"NAME": 1, "place": "Somewhere"}, extra=22): + assert env.NAME == 1 + assert env.place == "Somewhere" + assert env.extra == 22 + assert not hasattr(env, "secret") + + check_normal() + + try: + with patch_env(NAME=1, place="Somewhere"): + raise RuntimeError("oops") # noqa: TRY301 + except Exception: + pass + + check_normal() + + +def test_seek_metadata_fs(): + fh = Path(__file__).open() + print(fh.fileno()) + print(f"This file is {os.lseek(fh.fileno(), 0, os.SEEK_END)} characters long") + + +class Default(WorkerEntrypoint): + async def test(self): + test_import_from_javascript() + test_import_vectorize() + test_import_failures() + + await test_memory_cache() + await test_with_env() + test_patch_env() + use(multiprocessing) + test_seek_metadata_fs() diff --git a/src/workerd/server/tests/python/multiprocessing/multiprocessing.wd-test b/src/workerd/server/tests/python/multiprocessing/multiprocessing.wd-test deleted file mode 100644 index 0193948efa3..00000000000 --- a/src/workerd/server/tests/python/multiprocessing/multiprocessing.wd-test +++ /dev/null @@ -1,14 +0,0 @@ -using Workerd = import "/workerd/workerd.capnp"; - -const unitTests :Workerd.Config = ( - services = [ - ( name = "python-hello", - worker = ( - modules = [ - (name = "worker.py", pythonModule = embed "worker.py") - ], - compatibilityFlags = [%PYTHON_FEATURE_FLAGS, "disable_python_no_global_handlers", "disable_python_check_rng_state"], - ) - ), - ], -); diff --git a/src/workerd/server/tests/python/multiprocessing/worker.py b/src/workerd/server/tests/python/multiprocessing/worker.py deleted file mode 100644 index 5799a88baf3..00000000000 --- a/src/workerd/server/tests/python/multiprocessing/worker.py +++ /dev/null @@ -1,9 +0,0 @@ -import multiprocessing - - -def use(x): - pass - - -def test(): - use(multiprocessing) diff --git a/src/workerd/server/tests/python/seek-metadatafs/a.py b/src/workerd/server/tests/python/seek-metadatafs/a.py deleted file mode 100644 index 1f76ff283d0..00000000000 --- a/src/workerd/server/tests/python/seek-metadatafs/a.py +++ /dev/null @@ -1,8 +0,0 @@ -import os -from pathlib import Path - - -def test(): - fh = Path(__file__).open() - print(fh.fileno()) - print(f"This file is {os.lseek(fh.fileno(), 0, os.SEEK_END)} characters long") diff --git a/src/workerd/server/tests/python/seek-metadatafs/seek-metadatafs.wd-test b/src/workerd/server/tests/python/seek-metadatafs/seek-metadatafs.wd-test deleted file mode 100644 index df720907479..00000000000 --- a/src/workerd/server/tests/python/seek-metadatafs/seek-metadatafs.wd-test +++ /dev/null @@ -1,15 +0,0 @@ -using Workerd = import "/workerd/workerd.capnp"; - -const unitTests :Workerd.Config = ( - services = [ - ( name = "main", - worker = ( - modules = [ - (name = "a.py", pythonModule = embed "./a.py"), - ], - compatibilityFlags = [%PYTHON_FEATURE_FLAGS, "disable_python_no_global_handlers"], - ) - ), - ], -); - diff --git a/src/workerd/server/tests/python/top-level-env/env.wd-test b/src/workerd/server/tests/python/top-level-env/env.wd-test deleted file mode 100644 index 5e43de089bd..00000000000 --- a/src/workerd/server/tests/python/top-level-env/env.wd-test +++ /dev/null @@ -1,20 +0,0 @@ -using Workerd = import "/workerd/workerd.capnp"; - -const unitTests :Workerd.Config = ( - services = [ - ( name = "python-hello", - worker = ( - modules = [ - (name = "worker.py", pythonModule = embed "worker.py") - ], - bindings = [ - ( - name = "secret", - text = "thisisasecret" - ), - ], - compatibilityFlags = [%PYTHON_FEATURE_FLAGS], - ) - ), - ], -); diff --git a/src/workerd/server/tests/python/top-level-env/worker.py b/src/workerd/server/tests/python/top-level-env/worker.py deleted file mode 100644 index 82b60d2f63b..00000000000 --- a/src/workerd/server/tests/python/top-level-env/worker.py +++ /dev/null @@ -1,52 +0,0 @@ -from js import Reflect -from workers import WorkerEntrypoint, env, patch_env - - -def check_normal(): - assert env.secret == "thisisasecret" - assert not hasattr(env, "NAME") - assert not hasattr(env, "place") - assert not hasattr(env, "extra") - assert Reflect.ownKeys(env).to_py() == ["secret"] - - -class Default(WorkerEntrypoint): - def test(self): - check_normal() - - with patch_env(NAME=1, place="Somewhere"): - assert env.NAME == 1 - assert env.place == "Somewhere" - assert not hasattr(env, "secret") - - check_normal() - - with patch_env({"NAME": 1, "place": "Somewhere"}): - assert env.NAME == 1 - assert env.place == "Somewhere" - assert not hasattr(env, "secret") - - check_normal() - - with patch_env([("NAME", 1), ("place", "Somewhere")]): - assert env.NAME == 1 - assert env.place == "Somewhere" - assert not hasattr(env, "secret") - - check_normal() - - with patch_env({"NAME": 1, "place": "Somewhere"}, extra=22): - assert env.NAME == 1 - assert env.place == "Somewhere" - assert env.extra == 22 - assert not hasattr(env, "secret") - - check_normal() - - try: - with patch_env(NAME=1, place="Somewhere"): - raise RuntimeError("oops") # noqa: TRY301 - except Exception: - pass - - check_normal()