forked from emscripten-core/emscripten
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest_sockets.py
368 lines (307 loc) · 16.9 KB
/
test_sockets.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# Copyright 2013 The Emscripten Authors. All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.
import multiprocessing
import os
import socket
import shutil
import sys
import time
from subprocess import Popen
from typing import List
if __name__ == '__main__':
raise Exception('do not run this file directly; do something like: test/runner sockets')
import clang_native
import common
from common import BrowserCore, no_windows, create_file, test_file, read_file
from common import parameterized, requires_native_clang, crossplatform, PYTHON, NON_ZERO
from tools import config
from tools.shared import EMCC, path_from_root, run_process, CLANG_CC
npm_checked = False
def clean_processes(processes):
for p in processes:
if getattr(p, 'exitcode', None) is None and getattr(p, 'returncode', None) is None:
# ask nicely (to try and catch the children)
try:
p.terminate() # SIGTERM
except OSError:
pass
time.sleep(1)
# send a forcible kill immediately afterwards. If the process did not die before, this should clean it.
try:
p.terminate() # SIGKILL
except OSError:
pass
class WebsockifyServerHarness():
def __init__(self, filename, args, listen_port, do_server_check=True):
self.processes = []
self.filename = filename
self.listen_port = listen_port
self.target_port = listen_port - 1
self.args = args or []
self.do_server_check = do_server_check
def __enter__(self):
# compile the server
# NOTE empty filename support is a hack to support
# the current test_enet
if self.filename:
cmd = [CLANG_CC, test_file(self.filename), '-o', 'server', '-DSOCKK=%d' % self.target_port] + clang_native.get_clang_native_args() + self.args
print(cmd)
run_process(cmd, env=clang_native.get_clang_native_env())
process = Popen([os.path.abspath('server')])
self.processes.append(process)
import websockify # type: ignore
# start the websocket proxy
print('running websockify on %d, forward to tcp %d' % (self.listen_port, self.target_port), file=sys.stderr)
# source_is_ipv6=True here signals to websockify that it should prefer ipv6 address when
# resolving host names. This matches what the node `ws` module does and means that `localhost`
# resolves to `::1` on IPv6 systems.
wsp = websockify.WebSocketProxy(verbose=True, source_is_ipv6=True, listen_port=self.listen_port, target_host="127.0.0.1", target_port=self.target_port, run_once=True)
self.websockify = multiprocessing.Process(target=wsp.start_server)
self.websockify.start()
self.processes.append(self.websockify)
# Make sure both the actual server and the websocket proxy are running
for _ in range(10):
try:
if self.do_server_check:
server_sock = socket.create_connection(('localhost', self.target_port), timeout=1)
server_sock.close()
proxy_sock = socket.create_connection(('localhost', self.listen_port), timeout=1)
proxy_sock.close()
break
except IOError:
time.sleep(1)
else:
clean_processes(self.processes)
raise Exception('[Websockify failed to start up in a timely manner]')
print('[Websockify on process %s]' % str(self.processes[-2:]))
return self
def __exit__(self, *args, **kwargs):
# try to kill the websockify proxy gracefully
if self.websockify.is_alive():
self.websockify.terminate()
self.websockify.join()
# clean up any processes we started
clean_processes(self.processes)
class CompiledServerHarness():
def __init__(self, filename, args, listen_port):
self.processes = []
self.filename = filename
self.listen_port = listen_port
self.args = args or []
def __enter__(self):
# assuming this is only used for WebSocket tests at the moment, validate that
# the ws module is installed
global npm_checked
if not npm_checked:
child = run_process(config.NODE_JS + ['-e', 'require("ws");'], check=False)
assert child.returncode == 0, '"ws" node module not found. you may need to run npm install'
npm_checked = True
# compile the server
suffix = '.mjs' if '-sEXPORT_ES6' in self.args else '.js'
proc = run_process([EMCC, '-Werror', test_file(self.filename), '-o', 'server' + suffix, '-DSOCKK=%d' % self.listen_port] + self.args)
print('Socket server build: out:', proc.stdout or '', '/ err:', proc.stderr or '')
process = Popen(config.NODE_JS + ['server' + suffix])
self.processes.append(process)
return self
def __exit__(self, *args, **kwargs):
# clean up any processes we started
clean_processes(self.processes)
# always run these tests last
# make sure to use different ports in each one because it takes a while for the processes to be cleaned up
# Executes a native executable server process
class BackgroundServerProcess():
def __init__(self, args):
self.processes = []
self.args = args
def __enter__(self):
print('Running background server: ' + str(self.args))
process = Popen(self.args)
self.processes.append(process)
return self
def __exit__(self, *args, **kwargs):
clean_processes(self.processes)
def NodeJsWebSocketEchoServerProcess():
return BackgroundServerProcess(config.NODE_JS + [test_file('websocket/nodejs_websocket_echo_server.js')])
def PythonTcpEchoServerProcess(port):
return BackgroundServerProcess([PYTHON, test_file('websocket/tcp_echo_server.py'), port])
class sockets(BrowserCore):
emcc_args: List[str] = []
@classmethod
def setUpClass(cls):
super().setUpClass()
print()
print('Running the socket tests. Make sure the browser allows popups from localhost.')
print()
# Note: in the WebsockifyServerHarness and CompiledServerHarness tests below, explicitly use
# consecutive server listen ports, because server teardown might not occur deterministically
# (python dtor time) and is a bit racy.
# WebsockifyServerHarness uses two port numbers, x and x-1, so increment it by two.
# CompiledServerHarness only uses one. Start with 49160 & 49159 as the first server port
# addresses. If adding new tests, increment the used port addresses below.
@parameterized({
'websockify': [WebsockifyServerHarness, 49160, ['-DTEST_DGRAM=0']],
'tcp': [CompiledServerHarness, 49161, ['-DTEST_DGRAM=0']],
'udp': [CompiledServerHarness, 49162, ['-DTEST_DGRAM=1']],
# The following forces non-NULL addr and addlen parameters for the accept call
'accept_addr': [CompiledServerHarness, 49163, ['-DTEST_DGRAM=0', '-DTEST_ACCEPT_ADDR=1']],
})
def test_sockets_echo(self, harness_class, port, args):
if harness_class == WebsockifyServerHarness and common.EMTEST_LACKS_NATIVE_CLANG:
self.skipTest('requires native clang')
with harness_class(test_file('sockets/test_sockets_echo_server.c'), args, port) as harness:
self.btest_exit('sockets/test_sockets_echo_client.c', emcc_args=['-DSOCKK=%d' % harness.listen_port] + args)
def test_sockets_echo_pthreads(self):
with CompiledServerHarness(test_file('sockets/test_sockets_echo_server.c'), [], 49161) as harness:
self.btest_exit('sockets/test_sockets_echo_client.c', emcc_args=['-pthread', '-sPROXY_TO_PTHREAD', '-DSOCKK=%d' % harness.listen_port])
def test_sdl2_sockets_echo(self):
with CompiledServerHarness('sockets/sdl2_net_server.c', ['-sUSE_SDL=2', '-sUSE_SDL_NET=2'], 49164) as harness:
self.btest_exit('sockets/sdl2_net_client.c', emcc_args=['-sUSE_SDL=2', '-sUSE_SDL_NET=2', '-DSOCKK=%d' % harness.listen_port])
@parameterized({
'websockify': [WebsockifyServerHarness, 49166, ['-DTEST_DGRAM=0']],
'tcp': [CompiledServerHarness, 49167, ['-DTEST_DGRAM=0']],
'udp': [CompiledServerHarness, 49168, ['-DTEST_DGRAM=1']],
# The following forces non-NULL addr and addlen parameters for the accept call
'accept_addr': [CompiledServerHarness, 49169, ['-DTEST_DGRAM=0', '-DTEST_ACCEPT_ADDR=1']],
})
def test_sockets_async_echo(self, harness_class, port, args):
if harness_class == WebsockifyServerHarness and common.EMTEST_LACKS_NATIVE_CLANG:
self.skipTest('requires native clang')
args.append('-DTEST_ASYNC=1')
with harness_class(test_file('sockets/test_sockets_echo_server.c'), args, port) as harness:
self.btest_exit('sockets/test_sockets_echo_client.c', emcc_args=['-DSOCKK=%d' % harness.listen_port] + args)
def test_sockets_async_bad_port(self):
# Deliberately attempt a connection on a port that will fail to test the error callback and
# getsockopt
self.btest_exit('sockets/test_sockets_echo_client.c', emcc_args=['-DSOCKK=49169', '-DTEST_ASYNC=1'])
@parameterized({
'websockify': [WebsockifyServerHarness, 49171, ['-DTEST_DGRAM=0']],
'tcp': [CompiledServerHarness, 49172, ['-DTEST_DGRAM=0']],
'udp': [CompiledServerHarness, 49173, ['-DTEST_DGRAM=1']],
})
def test_sockets_echo_bigdata(self, harness_class, port, args):
if harness_class == WebsockifyServerHarness and common.EMTEST_LACKS_NATIVE_CLANG:
self.skipTest('requires native clang')
sockets_include = '-I' + test_file('sockets')
# generate a large string literal to use as our message
message = ''
for i in range(256 * 256 * 2):
message += str(chr(ord('a') + (i % 26)))
# re-write the client test with this literal (it's too big to pass via command line)
src = read_file(test_file('sockets/test_sockets_echo_client.c'))
create_file('test_sockets_echo_bigdata.c', src.replace('#define MESSAGE "pingtothepong"', '#define MESSAGE "%s"' % message))
with harness_class(test_file('sockets/test_sockets_echo_server.c'), args, port) as harness:
self.btest_exit('test_sockets_echo_bigdata.c', emcc_args=[sockets_include, '-DSOCKK=%d' % harness.listen_port] + args)
@no_windows('This test is Unix-specific.')
def test_sockets_partial(self):
for harness in [
WebsockifyServerHarness(test_file('sockets/test_sockets_partial_server.c'), [], 49180),
CompiledServerHarness(test_file('sockets/test_sockets_partial_server.c'), [], 49181)
]:
with harness:
self.btest_exit('sockets/test_sockets_partial_client.c', assert_returncode=165, emcc_args=['-DSOCKK=%d' % harness.listen_port])
@no_windows('This test is Unix-specific.')
def test_sockets_select_server_down(self):
for harness in [
WebsockifyServerHarness(test_file('sockets/test_sockets_select_server_down_server.c'), [], 49190, do_server_check=False),
CompiledServerHarness(test_file('sockets/test_sockets_select_server_down_server.c'), [], 49191)
]:
with harness:
self.btest_exit('sockets/test_sockets_select_server_down_client.c', emcc_args=['-DSOCKK=%d' % harness.listen_port])
@no_windows('This test is Unix-specific.')
def test_sockets_select_server_closes_connection_rw(self):
for harness in [
WebsockifyServerHarness(test_file('sockets/test_sockets_echo_server.c'), ['-DCLOSE_CLIENT_AFTER_ECHO'], 49200),
CompiledServerHarness(test_file('sockets/test_sockets_echo_server.c'), ['-DCLOSE_CLIENT_AFTER_ECHO'], 49201)
]:
with harness:
self.btest_exit('sockets/test_sockets_select_server_closes_connection_client_rw.c', emcc_args=['-DSOCKK=%d' % harness.listen_port])
@no_windows('This test uses Unix-specific build architecture.')
def test_enet(self):
# this is also a good test of raw usage of emconfigure and emmake
shutil.copytree(test_file('third_party', 'enet'), 'enet')
with common.chdir('enet'):
self.run_process([path_from_root('emconfigure'), './configure', '--disable-shared'])
self.run_process([path_from_root('emmake'), 'make'])
enet = [self.in_dir('enet', '.libs', 'libenet.a'), '-I' + self.in_dir('enet', 'include')]
with CompiledServerHarness(test_file('sockets/test_enet_server.c'), enet, 49210) as harness:
self.btest_exit('sockets/test_enet_client.c', emcc_args=enet + ['-DSOCKK=%d' % harness.listen_port])
@crossplatform
@parameterized({
'native': [WebsockifyServerHarness, 59160, ['-DTEST_DGRAM=0']],
'tcp': [CompiledServerHarness, 59162, ['-DTEST_DGRAM=0', '-sEXPORT_ES6', '--extern-post-js', test_file('modularize_post_js.js')]],
'udp': [CompiledServerHarness, 59164, ['-DTEST_DGRAM=1']],
'pthread': [CompiledServerHarness, 59166, ['-pthread', '-sPROXY_TO_PTHREAD']],
})
def test_nodejs_sockets_echo(self, harness_class, port, args):
if harness_class == WebsockifyServerHarness and common.EMTEST_LACKS_NATIVE_CLANG:
self.skipTest('requires native clang')
# Basic test of node client against both a Websockified and compiled echo server.
with harness_class(test_file('sockets/test_sockets_echo_server.c'), args, port) as harness:
expected = 'do_msg_read: read 14 bytes'
self.do_runf('sockets/test_sockets_echo_client.c', expected, emcc_args=['-DSOCKK=%d' % harness.listen_port] + args)
def test_nodejs_sockets_connect_failure(self):
self.do_runf('sockets/test_sockets_echo_client.c', 'connect failed: Connection refused', emcc_args=['-DSOCKK=666'], assert_returncode=NON_ZERO)
@requires_native_clang
def test_nodejs_sockets_echo_subprotocol(self):
# Test against a Websockified server with compile time configured WebSocket subprotocol. We use a Websockified
# server because as long as the subprotocol list contains binary it will configure itself to accept binary
# This test also checks that the connect url contains the correct subprotocols.
with WebsockifyServerHarness(test_file('sockets/test_sockets_echo_server.c'), [], 59168):
self.run_process([EMCC, '-Werror', test_file('sockets/test_sockets_echo_client.c'), '-o', 'client.js', '-sSOCKET_DEBUG', '-sWEBSOCKET_SUBPROTOCOL="base64, binary"', '-DSOCKK=59168'])
out = self.run_js('client.js')
self.assertContained('do_msg_read: read 14 bytes', out)
self.assertContained(['connect: ws://127.0.0.1:59168, base64,binary', 'connect: ws://127.0.0.1:59168/, base64,binary'], out)
@requires_native_clang
def test_nodejs_sockets_echo_subprotocol_runtime(self):
# Test against a Websockified server with runtime WebSocket configuration. We specify both url and subprotocol.
# In this test we have *deliberately* used the wrong port '-DSOCKK=12345' to configure the echo_client.c, so
# the connection would fail without us specifying a valid WebSocket URL in the configuration.
create_file('websocket_pre.js', '''
var Module = {
websocket: {
url: 'ws://localhost:59168/testA/testB',
subprotocol: 'text, base64, binary',
}
};
''')
with WebsockifyServerHarness(test_file('sockets/test_sockets_echo_server.c'), [], 59168):
self.run_process([EMCC, '-Werror', test_file('sockets/test_sockets_echo_client.c'), '-o', 'client.js', '--pre-js=websocket_pre.js', '-sSOCKET_DEBUG', '-DSOCKK=12345'])
out = self.run_js('client.js')
self.assertContained('do_msg_read: read 14 bytes', out)
self.assertContained('connect: ws://localhost:59168/testA/testB, text,base64,binary', out)
# Test Emscripten WebSockets API to send and receive text and binary messages against an echo server.
# N.B. running this test requires 'npm install ws' in Emscripten root directory
# NOTE: Shared buffer is not allowed for websocket sending.
@parameterized({
'': [[]],
'shared': [['-sSHARED_MEMORY']],
})
def test_websocket_send(self, args):
with NodeJsWebSocketEchoServerProcess():
self.btest_exit('websocket/test_websocket_send.c', emcc_args=['-lwebsocket', '-sNO_EXIT_RUNTIME', '-sWEBSOCKET_DEBUG'] + args)
# Test that native POSIX sockets API can be used by proxying calls to an intermediate WebSockets
# -> POSIX sockets bridge server
def test_posix_proxy_sockets(self):
# Build the websocket bridge server
self.run_process(['cmake', path_from_root('tools/websocket_to_posix_proxy')])
self.run_process(['cmake', '--build', '.'])
if os.name == 'nt': # This is not quite exact, instead of "isWindows()" this should be "If CMake defaults to building with Visual Studio", but there is no good check for that, so assume Windows==VS.
proxy_server = self.in_dir('Debug', 'websocket_to_posix_proxy.exe')
else:
proxy_server = self.in_dir('websocket_to_posix_proxy')
with BackgroundServerProcess([proxy_server, '8080']):
with PythonTcpEchoServerProcess('7777'):
# Build and run the TCP echo client program with Emscripten
self.btest_exit('websocket/tcp_echo_client.c', emcc_args=['-lwebsocket', '-sPROXY_POSIX_SOCKETS', '-pthread', '-sPROXY_TO_PTHREAD'])
# Test that calling send() right after a socket connect() works.
def test_sockets_send_while_connecting(self):
with NodeJsWebSocketEchoServerProcess():
self.btest('sockets/test_sockets_send_while_connecting.c', emcc_args=['-DSOCKET_DEBUG'], expected='0')
class sockets64(sockets):
def setUp(self):
super().setUp()
self.set_setting('MEMORY64')
self.emcc_args.append('-Wno-experimental')
self.require_wasm64()