Skip to content
File

Blob: src/workerd/server/tests/python/python-rpc/worker.py

python279 lines
1# Copyright (c) 2023 Cloudflare, Inc.
2# Licensed under the Apache 2.0 license found in the LICENSE file or at:
3# https://opensource.org/licenses/Apache-2.0
4 
5import collections.abc
6import json
7from asyncio import Future, sleep
8from datetime import datetime
9from http import HTTPMethod
10from unittest import TestCase
11 
12import js
13from workers import Blob, Request, Response, WorkerEntrypoint, handler
14 
15from pyodide.ffi import JsException, JsProxy, to_js
16 
17assertRaises = TestCase().assertRaises
18assertRaisesRegex = TestCase().assertRaisesRegex
19 
20testFuture = Future()
21 
22 
23class PythonRpcTester(WorkerEntrypoint):
24 def __init__(self, ctx, env):
25 super().__init__(ctx, env)
26 # Verify that the superclass constructor initialises the env/ctx fields.
27 assert self.env is not None
28 assert self.ctx is not None
29 
30 assert not isinstance(env, JsProxy)
31 self.env = env
32 
33 async def no_args(self):
34 return "hello from python"
35 
36 async def one_arg(self, x):
37 return f"{x}"
38 
39 async def identity(self, x):
40 assert not isinstance(x, JsProxy)
41 return x
42 
43 async def handle_response(self, response):
44 # Verify that we receive a Python object here...
45 assert isinstance(response, Response)
46 return response
47 
48 async def handle_request(self, req):
49 assert isinstance(req, Request)
50 return req
51 
52 async def check_env(self):
53 # Verify that the `env` supplied to the entrypoint class is wrapped.
54 curr_date = datetime.now()
55 py_date = await self.env.PythonRpc.identity(curr_date)
56 assert isinstance(py_date, datetime)
57 # JavaScript Date objects only have millisecond precision
58 assert abs((py_date - curr_date).microseconds) < 1e6
59 
60 return True
61 
62 async def sleep_then_set_result(self):
63 await sleep(0.1)
64 testFuture.set_result(100)
65 
66 async def test_wait_until_coroutine_lifetime(self):
67 self.ctx.waitUntil(self.sleep_then_set_result())
68 
69 
70class CustomType:
71 def __init__(self, x):
72 self.x = 42
73 
74 
75def assert_equal(a, b, avoid_type_check):
76 # Convert to JSON so that the contents of the values can be easily compared.
77 received = json.dumps(
78 json.loads(js.JSON.stringify(a) if isinstance(a, JsProxy) else json.dumps(a))
79 )
80 expected = json.dumps(
81 json.loads(js.JSON.stringify(b) if isinstance(b, JsProxy) else json.dumps(b))
82 )
83 if received != expected:
84 raise ValueError(
85 f"Assert failed, args contents are not equal. received='{received}' expected='{expected}'"
86 )
87 
88 if type(a) is not type(b) and not avoid_type_check:
89 raise ValueError(
90 f"Assert failed, types don't match. received={type(a)} expected={type(b)}"
91 )
92 
93 
94@handler
95async def test(ctrl, env, ctx):
96 # https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types
97 #
98 # Workers RPC doesn't support all of the above, but does support all the JS Types and
99 # some of the Web API types.
100 #
101 # ReadableStream and WritableStream are also supported.
102 #
103 # We verify that we can send and receive as much of these as possible, including between
104 # Python<->Python, JS<->Python and vice versa. We also ensure that the types we receive are
105 # native Python types, rather than `JsProxy`s.
106 
107 # Simple tests.
108 assert await env.PythonRpc.no_args() == "hello from python"
109 assert await env.JsRpc.noArgs() == "hello from js"
110 assert await env.PythonRpc.one_arg(42) == "42"
111 assert await env.JsRpc.oneArg(42) == "42"
112 arr = await env.PythonRpc.identity([1, 2, 3])
113 assert isinstance(arr, collections.abc.Sequence) and not isinstance(arr, str)
114 
115 # Verify that text bindings can be accessed.
116 assert env.FOO == "text binding"
117 
118 # Python Types
119 for val in ["test", [1, 2, 3], {"key": 42}, 42, 1.2345, False, True, None]:
120 received = await env.PythonRpc.identity(val)
121 assert not isinstance(received, JsProxy), (
122 "Expected the returned value from RPC to be a Python type."
123 )
124 assert_equal(received, val, False)
125 received = await env.JsRpc.identity(val)
126 assert not isinstance(received, JsProxy), (
127 "Expected the returned value from RPC to be a Python type."
128 )
129 assert_equal(received, val, False)
130 
131 curr_date = datetime.now()
132 py_date = await env.PythonRpc.identity(curr_date)
133 assert isinstance(py_date, datetime)
134 # JavaScript Date objects only have millisecond precision
135 assert abs((py_date - curr_date).microseconds) < 1e6
136 
137 py_date_list = await env.PythonRpc.identity([datetime.now(), datetime.now()])
138 for d in py_date_list:
139 assert isinstance(d, datetime)
140 
141 py_set = await env.PythonRpc.identity({1, 2, 3})
142 assert isinstance(py_set, set)
143 assert 2 in py_set
144 assert 42 not in py_set
145 
146 py_binary = await env.PythonRpc.identity(b"binary")
147 assert isinstance(py_binary, memoryview)
148 py_binary = await env.PythonRpc.identity(memoryview(b"abcefg"))
149 assert isinstance(py_binary, memoryview)
150 
151 # JS types
152 for val in [
153 to_js([1, 2, 3, 4]),
154 js.Number.new("1234"),
155 ]:
156 received = await env.PythonRpc.identity(val)
157 assert not isinstance(received, JsProxy), (
158 "Expected the returned value from RPC to be a Python type."
159 )
160 assert_equal(received, val.to_py(), True)
161 received = await env.JsRpc.identity(val)
162 assert not isinstance(received, JsProxy), (
163 "Expected the returned value from RPC to be a Python type."
164 )
165 assert_equal(received, val.to_py(), True)
166 
167 for val in [
168 js.ArrayBuffer.new(8),
169 js.DataView.new(js.ArrayBuffer.new(16)),
170 js.Int16Array.of("10", "24"),
171 ]:
172 js_binary = await env.PythonRpc.identity(val)
173 assert isinstance(js_binary, memoryview)
174 js_binary = await env.JsRpc.identity(val)
175 assert isinstance(js_binary, memoryview)
176 
177 js_map = await env.PythonRpc.identity(
178 js.Map.new(
179 [
180 [1, "one"],
181 [2, "two"],
182 [3, "three"],
183 ]
184 ),
185 )
186 assert isinstance(js_map, dict)
187 assert js_map[1] == "one"
188 
189 js_set = await env.PythonRpc.identity(
190 js.Set.new([1, 2, 3, 4]),
191 )
192 assert isinstance(js_set, set)
193 assert 1 in js_set
194 assert 42 not in js_set
195 
196 js_undefined = await env.PythonRpc.identity(js.undefined)
197 assert js_undefined is None
198 
199 js_date = await env.PythonRpc.identity(js.Date.new())
200 assert isinstance(js_date, datetime)
201 
202 js_date_list = await env.PythonRpc.identity([js.Date.new(), js.Date.new()])
203 for d in js_date_list:
204 assert isinstance(d, datetime)
205 
206 js_exception = await env.PythonRpc.identity(js.Error.new("message"))
207 assert isinstance(js_exception, Exception)
208 
209 js_obj = await env.PythonRpc.identity(
210 to_js({"foo": 42}, dict_converter=js.Object.fromEntries)
211 )
212 assert isinstance(js_obj, dict)
213 assert js_obj["foo"] == 42
214 
215 # Web/API Types
216 # - Response
217 py_response = await env.PythonRpc.handle_response(Response("this is a response"))
218 assert isinstance(py_response, Response)
219 assert await py_response.text() == "this is a response"
220 js_response = await env.JsRpc.handleResponse(Response("this is a response"))
221 assert await js_response.text() == "this is a response"
222 assert isinstance(js_response, Response)
223 
224 # - Request
225 py_response = await env.PythonRpc.handle_request(
226 Request("https://test.com", method=HTTPMethod.POST)
227 )
228 assert isinstance(py_response, Request)
229 assert py_response.method == "POST"
230 js_response = await env.JsRpc.handleRequest(
231 Request("https://test.com", method=HTTPMethod.POST)
232 )
233 assert js_response.method == "POST"
234 assert isinstance(js_response, Request)
235 
236 # - Verify that a JS type can be sent.
237 py_response2 = await env.PythonRpc.handle_response(js.Response.new("a JS response"))
238 assert await py_response2.text() == "a JS response"
239 
240 # Verify that sending unsupported types fails.
241 data_clone_regex = "^DataCloneError"
242 with assertRaisesRegex(JsException, data_clone_regex):
243 await env.PythonRpc.one_arg(CustomType(42))
244 with assertRaisesRegex(JsException, data_clone_regex):
245 await env.PythonRpc.one_arg(Blob("print(42)", content_type="text/python"))
246 with assertRaisesRegex(JsException, data_clone_regex):
247 await env.PythonRpc.identity(complex(1.23))
248 with assertRaises(TypeError):
249 await env.PythonRpc.identity((1, 2, 3))
250 with assertRaisesRegex(JsException, data_clone_regex):
251 await env.PythonRpc.identity(range(0, 30, 5))
252 with assertRaises(TypeError):
253 await env.PythonRpc.identity(bytearray.fromhex("2Ef0 F1f2 "))
254 with assertRaises(TypeError):
255 await env.PythonRpc.identity([(1, 2, 3)])
256 # TODO: Support RegExp.
257 with assertRaises(TypeError):
258 await env.PythonRpc.identity(js.RegExp.new("ab+c", "i"))
259 with assertRaises(TypeError):
260 await env.PythonRpc.identity(lambda x: x + x)
261 with assertRaises(TypeError):
262 
263 def my_func():
264 pass
265 
266 await env.PythonRpc.identity(my_func)
267 with assertRaises(TypeError):
268 await env.PythonRpc.identity({"test": (1, 2, 3)})
269 
270 # Verify that the `env` in the DO is correctly wrapped.
271 assert await env.PythonRpc.check_env()
272 
273 # Check that the coroutine returned by sleep_then_set_result() lasts long enough.
274 # sleep_then_set_result() resolves testFuture after sleeping for 100ms.
275 await env.PythonRpc.test_wait_until_coroutine_lifetime()
276 assert not testFuture.done()
277 await sleep(0.2)
278 assert testFuture.result() == 100