2008-02-02 07:05:00 -04:00
|
|
|
"""
|
2008-05-24 15:31:28 -03:00
|
|
|
Test suite for SocketServer.py.
|
2008-02-02 07:05:00 -04:00
|
|
|
"""
|
2001-07-10 08:52:38 -03:00
|
|
|
|
2008-02-28 14:03:15 -04:00
|
|
|
import contextlib
|
2008-02-02 07:05:00 -04:00
|
|
|
import imp
|
2008-02-28 01:53:18 -04:00
|
|
|
import os
|
2001-07-10 08:52:38 -03:00
|
|
|
import select
|
2008-02-28 01:53:18 -04:00
|
|
|
import signal
|
|
|
|
import socket
|
2012-04-08 19:47:24 -03:00
|
|
|
import select
|
|
|
|
import errno
|
2008-02-28 01:53:18 -04:00
|
|
|
import tempfile
|
2008-02-02 07:05:00 -04:00
|
|
|
import unittest
|
2008-05-24 15:31:28 -03:00
|
|
|
import SocketServer
|
2008-02-02 07:05:00 -04:00
|
|
|
|
|
|
|
import test.test_support
|
2009-10-27 18:27:24 -03:00
|
|
|
from test.test_support import reap_children, reap_threads, verbose
|
2010-04-27 20:55:59 -03:00
|
|
|
try:
|
|
|
|
import threading
|
|
|
|
except ImportError:
|
|
|
|
threading = None
|
2008-02-02 07:05:00 -04:00
|
|
|
|
|
|
|
test.test_support.requires("network")
|
2001-07-10 08:52:38 -03:00
|
|
|
|
2008-02-02 07:05:00 -04:00
|
|
|
TEST_STR = "hello world\n"
|
- Issue #2550: The approach used by client/server code for obtaining ports
to listen on in network-oriented tests has been refined in an effort to
facilitate running multiple instances of the entire regression test suite
in parallel without issue. test_support.bind_port() has been fixed such
that it will always return a unique port -- which wasn't always the case
with the previous implementation, especially if socket options had been
set that affected address reuse (i.e. SO_REUSEADDR, SO_REUSEPORT). The
new implementation of bind_port() will actually raise an exception if it
is passed an AF_INET/SOCK_STREAM socket with either the SO_REUSEADDR or
SO_REUSEPORT socket option set. Furthermore, if available, bind_port()
will set the SO_EXCLUSIVEADDRUSE option on the socket it's been passed.
This currently only applies to Windows. This option prevents any other
sockets from binding to the host/port we've bound to, thus removing the
possibility of the 'non-deterministic' behaviour, as Microsoft puts it,
that occurs when a second SOCK_STREAM socket binds and accepts to a
host/port that's already been bound by another socket. The optional
preferred port parameter to bind_port() has been removed. Under no
circumstances should tests be hard coding ports!
test_support.find_unused_port() has also been introduced, which will pass
a temporary socket object to bind_port() in order to obtain an unused port.
The temporary socket object is then closed and deleted, and the port is
returned. This method should only be used for obtaining an unused port
in order to pass to an external program (i.e. the -accept [port] argument
to openssl's s_server mode) or as a parameter to a server-oriented class
that doesn't give you direct access to the underlying socket used.
Finally, test_support.HOST has been introduced, which should be used for
the host argument of any relevant socket calls (i.e. bind and connect).
The following tests were updated to following the new conventions:
test_socket, test_smtplib, test_asyncore, test_ssl, test_httplib,
test_poplib, test_ftplib, test_telnetlib, test_socketserver,
test_asynchat and test_socket_ssl.
It is now possible for multiple instances of the regression test suite to
run in parallel without issue.
2008-04-08 20:47:30 -03:00
|
|
|
HOST = test.test_support.HOST
|
2008-02-02 07:05:00 -04:00
|
|
|
|
|
|
|
HAVE_UNIX_SOCKETS = hasattr(socket, "AF_UNIX")
|
2013-11-03 17:15:46 -04:00
|
|
|
requires_unix_sockets = unittest.skipUnless(HAVE_UNIX_SOCKETS,
|
|
|
|
'requires Unix sockets')
|
2008-02-02 07:05:00 -04:00
|
|
|
HAVE_FORKING = hasattr(os, "fork") and os.name != "os2"
|
2013-11-03 17:15:46 -04:00
|
|
|
requires_forking = unittest.skipUnless(HAVE_FORKING, 'requires forking')
|
2008-02-02 07:05:00 -04:00
|
|
|
|
2008-03-05 02:19:56 -04:00
|
|
|
def signal_alarm(n):
|
|
|
|
"""Call signal.alarm when it exists (i.e. not on Windows)."""
|
|
|
|
if hasattr(signal, 'alarm'):
|
|
|
|
signal.alarm(n)
|
2001-07-10 08:52:38 -03:00
|
|
|
|
2012-04-08 20:15:06 -03:00
|
|
|
# Remember real select() to avoid interferences with mocking
|
|
|
|
_real_select = select.select
|
|
|
|
|
2008-02-02 07:05:00 -04:00
|
|
|
def receive(sock, n, timeout=20):
|
2012-04-08 20:15:06 -03:00
|
|
|
r, w, x = _real_select([sock], [], [], timeout)
|
2008-02-02 07:05:00 -04:00
|
|
|
if sock in r:
|
|
|
|
return sock.recv(n)
|
|
|
|
else:
|
|
|
|
raise RuntimeError, "timed out on %r" % (sock,)
|
|
|
|
|
2008-02-03 19:57:24 -04:00
|
|
|
if HAVE_UNIX_SOCKETS:
|
2008-05-24 15:31:28 -03:00
|
|
|
class ForkingUnixStreamServer(SocketServer.ForkingMixIn,
|
|
|
|
SocketServer.UnixStreamServer):
|
2008-02-03 19:57:24 -04:00
|
|
|
pass
|
|
|
|
|
2008-05-24 15:31:28 -03:00
|
|
|
class ForkingUnixDatagramServer(SocketServer.ForkingMixIn,
|
|
|
|
SocketServer.UnixDatagramServer):
|
2008-02-03 19:57:24 -04:00
|
|
|
pass
|
2008-02-02 07:05:00 -04:00
|
|
|
|
|
|
|
|
2008-02-28 14:03:15 -04:00
|
|
|
@contextlib.contextmanager
|
|
|
|
def simple_subprocess(testcase):
|
|
|
|
pid = os.fork()
|
|
|
|
if pid == 0:
|
2012-12-18 15:27:37 -04:00
|
|
|
# Don't raise an exception; it would be caught by the test harness.
|
2008-02-28 14:03:15 -04:00
|
|
|
os._exit(72)
|
|
|
|
yield None
|
|
|
|
pid2, status = os.waitpid(pid, 0)
|
2010-11-21 09:34:58 -04:00
|
|
|
testcase.assertEqual(pid2, pid)
|
|
|
|
testcase.assertEqual(72 << 8, status)
|
2008-02-28 14:03:15 -04:00
|
|
|
|
|
|
|
|
2017-09-27 07:02:57 -03:00
|
|
|
def close_server(server):
|
|
|
|
server.server_close()
|
|
|
|
|
|
|
|
if hasattr(server, 'active_children'):
|
|
|
|
# ForkingMixIn: Manually reap all child processes, since server_close()
|
|
|
|
# calls waitpid() in non-blocking mode using the WNOHANG flag.
|
|
|
|
for pid in server.active_children.copy():
|
|
|
|
try:
|
|
|
|
os.waitpid(pid, 0)
|
|
|
|
except ChildProcessError:
|
|
|
|
pass
|
|
|
|
server.active_children.clear()
|
|
|
|
|
|
|
|
|
2010-05-26 14:25:28 -03:00
|
|
|
@unittest.skipUnless(threading, 'Threading required for this test.')
|
2008-02-02 07:05:00 -04:00
|
|
|
class SocketServerTest(unittest.TestCase):
|
|
|
|
"""Test all socket servers."""
|
|
|
|
|
|
|
|
def setUp(self):
|
2018-06-01 10:23:02 -03:00
|
|
|
self.addCleanup(signal_alarm, 0)
|
2011-07-14 09:53:24 -03:00
|
|
|
signal_alarm(60) # Kill deadlocks after 60 seconds.
|
2008-02-02 07:05:00 -04:00
|
|
|
self.port_seed = 0
|
|
|
|
self.test_files = []
|
|
|
|
|
|
|
|
def tearDown(self):
|
2018-06-01 10:23:02 -03:00
|
|
|
self.doCleanups()
|
2008-02-02 07:05:00 -04:00
|
|
|
reap_children()
|
|
|
|
|
|
|
|
for fn in self.test_files:
|
|
|
|
try:
|
|
|
|
os.remove(fn)
|
|
|
|
except os.error:
|
|
|
|
pass
|
|
|
|
self.test_files[:] = []
|
|
|
|
|
|
|
|
def pickaddr(self, proto):
|
|
|
|
if proto == socket.AF_INET:
|
2008-02-28 01:53:18 -04:00
|
|
|
return (HOST, 0)
|
2008-02-02 07:05:00 -04:00
|
|
|
else:
|
2008-02-28 01:53:18 -04:00
|
|
|
# XXX: We need a way to tell AF_UNIX to pick its own name
|
|
|
|
# like AF_INET provides port==0.
|
|
|
|
dir = None
|
|
|
|
if os.name == 'os2':
|
|
|
|
dir = '\socket'
|
|
|
|
fn = tempfile.mktemp(prefix='unix_socket.', dir=dir)
|
2008-02-02 07:05:00 -04:00
|
|
|
if os.name == 'os2':
|
|
|
|
# AF_UNIX socket names on OS/2 require a specific prefix
|
|
|
|
# which can't include a drive letter and must also use
|
|
|
|
# backslashes as directory separators
|
|
|
|
if fn[1] == ':':
|
|
|
|
fn = fn[2:]
|
|
|
|
if fn[0] in (os.sep, os.altsep):
|
|
|
|
fn = fn[1:]
|
|
|
|
if os.sep == '/':
|
|
|
|
fn = fn.replace(os.sep, os.altsep)
|
|
|
|
else:
|
|
|
|
fn = fn.replace(os.altsep, os.sep)
|
|
|
|
self.test_files.append(fn)
|
|
|
|
return fn
|
|
|
|
|
2008-03-07 02:22:15 -04:00
|
|
|
def make_server(self, addr, svrcls, hdlrbase):
|
|
|
|
class MyServer(svrcls):
|
|
|
|
def handle_error(self, request, client_address):
|
|
|
|
self.close_request(request)
|
2017-09-27 07:02:57 -03:00
|
|
|
close_server(self)
|
2008-03-07 02:22:15 -04:00
|
|
|
raise
|
|
|
|
|
2008-02-28 01:53:18 -04:00
|
|
|
class MyHandler(hdlrbase):
|
|
|
|
def handle(self):
|
|
|
|
line = self.rfile.readline()
|
|
|
|
self.wfile.write(line)
|
|
|
|
|
2008-03-07 02:22:15 -04:00
|
|
|
if verbose: print "creating server"
|
|
|
|
server = MyServer(addr, MyHandler)
|
2010-11-21 09:34:58 -04:00
|
|
|
self.assertEqual(server.server_address, server.socket.getsockname())
|
2008-03-07 02:22:15 -04:00
|
|
|
return server
|
|
|
|
|
2009-10-27 18:27:24 -03:00
|
|
|
@reap_threads
|
2008-03-07 02:22:15 -04:00
|
|
|
def run_server(self, svrcls, hdlrbase, testfunc):
|
|
|
|
server = self.make_server(self.pickaddr(svrcls.address_family),
|
|
|
|
svrcls, hdlrbase)
|
|
|
|
# We had the OS pick a port, so pull the real address out of
|
|
|
|
# the server.
|
|
|
|
addr = server.server_address
|
2008-02-28 01:53:18 -04:00
|
|
|
if verbose:
|
2008-03-07 02:22:15 -04:00
|
|
|
print "server created"
|
2008-02-28 01:53:18 -04:00
|
|
|
print "ADDR =", addr
|
|
|
|
print "CLASS =", svrcls
|
2008-03-07 02:22:15 -04:00
|
|
|
t = threading.Thread(
|
|
|
|
name='%s serving' % svrcls,
|
|
|
|
target=server.serve_forever,
|
|
|
|
# Short poll interval to make the test finish quickly.
|
|
|
|
# Time between requests is short enough that we won't wake
|
|
|
|
# up spuriously too many times.
|
|
|
|
kwargs={'poll_interval':0.01})
|
2008-08-18 15:01:43 -03:00
|
|
|
t.daemon = True # In case this function raises.
|
2008-02-28 01:53:18 -04:00
|
|
|
t.start()
|
|
|
|
if verbose: print "server running"
|
2008-03-07 02:22:15 -04:00
|
|
|
for i in range(3):
|
2008-02-28 01:53:18 -04:00
|
|
|
if verbose: print "test client", i
|
|
|
|
testfunc(svrcls.address_family, addr)
|
|
|
|
if verbose: print "waiting for server"
|
2008-03-07 02:22:15 -04:00
|
|
|
server.shutdown()
|
2008-02-28 01:53:18 -04:00
|
|
|
t.join()
|
2017-09-27 07:02:57 -03:00
|
|
|
close_server(server)
|
2015-07-28 21:48:42 -03:00
|
|
|
self.assertRaises(socket.error, server.socket.fileno)
|
2008-02-28 01:53:18 -04:00
|
|
|
if verbose: print "done"
|
2008-02-02 07:05:00 -04:00
|
|
|
|
|
|
|
def stream_examine(self, proto, addr):
|
|
|
|
s = socket.socket(proto, socket.SOCK_STREAM)
|
|
|
|
s.connect(addr)
|
|
|
|
s.sendall(TEST_STR)
|
|
|
|
buf = data = receive(s, 100)
|
|
|
|
while data and '\n' not in buf:
|
|
|
|
data = receive(s, 100)
|
|
|
|
buf += data
|
2010-11-21 09:34:58 -04:00
|
|
|
self.assertEqual(buf, TEST_STR)
|
2008-02-02 07:05:00 -04:00
|
|
|
s.close()
|
|
|
|
|
|
|
|
def dgram_examine(self, proto, addr):
|
|
|
|
s = socket.socket(proto, socket.SOCK_DGRAM)
|
2016-02-24 00:45:33 -04:00
|
|
|
if HAVE_UNIX_SOCKETS and proto == socket.AF_UNIX:
|
|
|
|
s.bind(self.pickaddr(proto))
|
2008-02-02 07:05:00 -04:00
|
|
|
s.sendto(TEST_STR, addr)
|
|
|
|
buf = data = receive(s, 100)
|
|
|
|
while data and '\n' not in buf:
|
|
|
|
data = receive(s, 100)
|
|
|
|
buf += data
|
2010-11-21 09:34:58 -04:00
|
|
|
self.assertEqual(buf, TEST_STR)
|
2008-02-02 07:05:00 -04:00
|
|
|
s.close()
|
|
|
|
|
2008-02-28 01:53:18 -04:00
|
|
|
def test_TCPServer(self):
|
2008-05-24 15:31:28 -03:00
|
|
|
self.run_server(SocketServer.TCPServer,
|
|
|
|
SocketServer.StreamRequestHandler,
|
2008-02-28 01:53:18 -04:00
|
|
|
self.stream_examine)
|
|
|
|
|
|
|
|
def test_ThreadingTCPServer(self):
|
2008-05-24 15:31:28 -03:00
|
|
|
self.run_server(SocketServer.ThreadingTCPServer,
|
|
|
|
SocketServer.StreamRequestHandler,
|
2008-02-28 01:53:18 -04:00
|
|
|
self.stream_examine)
|
|
|
|
|
2013-11-03 17:15:46 -04:00
|
|
|
@requires_forking
|
|
|
|
def test_ForkingTCPServer(self):
|
|
|
|
with simple_subprocess(self):
|
|
|
|
self.run_server(SocketServer.ForkingTCPServer,
|
2008-05-24 15:31:28 -03:00
|
|
|
SocketServer.StreamRequestHandler,
|
2008-02-28 01:53:18 -04:00
|
|
|
self.stream_examine)
|
|
|
|
|
2013-11-03 17:15:46 -04:00
|
|
|
@requires_unix_sockets
|
|
|
|
def test_UnixStreamServer(self):
|
|
|
|
self.run_server(SocketServer.UnixStreamServer,
|
|
|
|
SocketServer.StreamRequestHandler,
|
|
|
|
self.stream_examine)
|
|
|
|
|
|
|
|
@requires_unix_sockets
|
|
|
|
def test_ThreadingUnixStreamServer(self):
|
|
|
|
self.run_server(SocketServer.ThreadingUnixStreamServer,
|
|
|
|
SocketServer.StreamRequestHandler,
|
|
|
|
self.stream_examine)
|
|
|
|
|
|
|
|
@requires_unix_sockets
|
|
|
|
@requires_forking
|
|
|
|
def test_ForkingUnixStreamServer(self):
|
|
|
|
with simple_subprocess(self):
|
|
|
|
self.run_server(ForkingUnixStreamServer,
|
2008-05-24 15:31:28 -03:00
|
|
|
SocketServer.StreamRequestHandler,
|
2008-02-28 01:53:18 -04:00
|
|
|
self.stream_examine)
|
|
|
|
|
|
|
|
def test_UDPServer(self):
|
2008-05-24 15:31:28 -03:00
|
|
|
self.run_server(SocketServer.UDPServer,
|
|
|
|
SocketServer.DatagramRequestHandler,
|
2008-02-28 01:53:18 -04:00
|
|
|
self.dgram_examine)
|
|
|
|
|
|
|
|
def test_ThreadingUDPServer(self):
|
2008-05-24 15:31:28 -03:00
|
|
|
self.run_server(SocketServer.ThreadingUDPServer,
|
|
|
|
SocketServer.DatagramRequestHandler,
|
2008-02-28 01:53:18 -04:00
|
|
|
self.dgram_examine)
|
|
|
|
|
2013-11-03 17:15:46 -04:00
|
|
|
@requires_forking
|
|
|
|
def test_ForkingUDPServer(self):
|
|
|
|
with simple_subprocess(self):
|
|
|
|
self.run_server(SocketServer.ForkingUDPServer,
|
|
|
|
SocketServer.DatagramRequestHandler,
|
|
|
|
self.dgram_examine)
|
2008-02-02 07:05:00 -04:00
|
|
|
|
2012-04-08 19:47:24 -03:00
|
|
|
@contextlib.contextmanager
|
|
|
|
def mocked_select_module(self):
|
|
|
|
"""Mocks the select.select() call to raise EINTR for first call"""
|
|
|
|
old_select = select.select
|
|
|
|
|
|
|
|
class MockSelect:
|
|
|
|
def __init__(self):
|
|
|
|
self.called = 0
|
|
|
|
|
|
|
|
def __call__(self, *args):
|
|
|
|
self.called += 1
|
|
|
|
if self.called == 1:
|
|
|
|
# raise the exception on first call
|
2012-04-08 20:41:34 -03:00
|
|
|
raise select.error(errno.EINTR, os.strerror(errno.EINTR))
|
2012-04-08 19:47:24 -03:00
|
|
|
else:
|
|
|
|
# Return real select value for consecutive calls
|
|
|
|
return old_select(*args)
|
|
|
|
|
|
|
|
select.select = MockSelect()
|
|
|
|
try:
|
|
|
|
yield select.select
|
|
|
|
finally:
|
|
|
|
select.select = old_select
|
|
|
|
|
|
|
|
def test_InterruptServerSelectCall(self):
|
|
|
|
with self.mocked_select_module() as mock_select:
|
|
|
|
pid = self.run_server(SocketServer.TCPServer,
|
|
|
|
SocketServer.StreamRequestHandler,
|
|
|
|
self.stream_examine)
|
|
|
|
# Make sure select was called again:
|
|
|
|
self.assertGreater(mock_select.called, 1)
|
|
|
|
|
2016-02-24 00:45:33 -04:00
|
|
|
@requires_unix_sockets
|
|
|
|
def test_UnixDatagramServer(self):
|
|
|
|
self.run_server(SocketServer.UnixDatagramServer,
|
|
|
|
SocketServer.DatagramRequestHandler,
|
|
|
|
self.dgram_examine)
|
|
|
|
|
|
|
|
@requires_unix_sockets
|
|
|
|
def test_ThreadingUnixDatagramServer(self):
|
|
|
|
self.run_server(SocketServer.ThreadingUnixDatagramServer,
|
|
|
|
SocketServer.DatagramRequestHandler,
|
|
|
|
self.dgram_examine)
|
|
|
|
|
|
|
|
@requires_unix_sockets
|
|
|
|
@requires_forking
|
|
|
|
def test_ForkingUnixDatagramServer(self):
|
|
|
|
self.run_server(ForkingUnixDatagramServer,
|
|
|
|
SocketServer.DatagramRequestHandler,
|
|
|
|
self.dgram_examine)
|
2008-02-02 07:05:00 -04:00
|
|
|
|
2010-04-25 18:40:32 -03:00
|
|
|
@reap_threads
|
|
|
|
def test_shutdown(self):
|
|
|
|
# Issue #2302: shutdown() should always succeed in making an
|
|
|
|
# other thread leave serve_forever().
|
|
|
|
class MyServer(SocketServer.TCPServer):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class MyHandler(SocketServer.StreamRequestHandler):
|
|
|
|
pass
|
|
|
|
|
|
|
|
threads = []
|
|
|
|
for i in range(20):
|
|
|
|
s = MyServer((HOST, 0), MyHandler)
|
|
|
|
t = threading.Thread(
|
|
|
|
name='MyServer serving',
|
|
|
|
target=s.serve_forever,
|
|
|
|
kwargs={'poll_interval':0.01})
|
|
|
|
t.daemon = True # In case this function raises.
|
|
|
|
threads.append((t, s))
|
|
|
|
for t, s in threads:
|
|
|
|
t.start()
|
|
|
|
s.shutdown()
|
|
|
|
for t, s in threads:
|
|
|
|
t.join()
|
2017-09-27 07:02:57 -03:00
|
|
|
close_server(s)
|
2010-04-25 18:40:32 -03:00
|
|
|
|
2014-10-13 14:39:34 -03:00
|
|
|
def test_tcpserver_bind_leak(self):
|
|
|
|
# Issue #22435: the server socket wouldn't be closed if bind()/listen()
|
|
|
|
# failed.
|
|
|
|
# Create many servers for which bind() will fail, to see if this result
|
|
|
|
# in FD exhaustion.
|
|
|
|
for i in range(1024):
|
|
|
|
with self.assertRaises(OverflowError):
|
|
|
|
SocketServer.TCPServer((HOST, -1),
|
|
|
|
SocketServer.StreamRequestHandler)
|
|
|
|
|
2016-02-18 22:16:42 -04:00
|
|
|
|
|
|
|
class MiscTestCase(unittest.TestCase):
|
|
|
|
|
2016-02-18 06:43:55 -04:00
|
|
|
def test_shutdown_request_called_if_verify_request_false(self):
|
|
|
|
# Issue #26309: BaseServer should call shutdown_request even if
|
|
|
|
# verify_request is False
|
|
|
|
|
|
|
|
class MyServer(SocketServer.TCPServer):
|
|
|
|
def verify_request(self, request, client_address):
|
|
|
|
return False
|
|
|
|
|
2016-02-18 22:16:42 -04:00
|
|
|
shutdown_called = 0
|
2016-02-18 06:43:55 -04:00
|
|
|
def shutdown_request(self, request):
|
2016-02-18 22:16:42 -04:00
|
|
|
self.shutdown_called += 1
|
2016-02-18 06:43:55 -04:00
|
|
|
SocketServer.TCPServer.shutdown_request(self, request)
|
|
|
|
|
2016-02-18 22:16:42 -04:00
|
|
|
server = MyServer((HOST, 0), SocketServer.StreamRequestHandler)
|
|
|
|
s = socket.socket(server.address_family, socket.SOCK_STREAM)
|
|
|
|
s.connect(server.server_address)
|
|
|
|
s.close()
|
|
|
|
server.handle_request()
|
|
|
|
self.assertEqual(server.shutdown_called, 1)
|
2017-09-27 07:02:57 -03:00
|
|
|
close_server(server)
|
2016-02-18 06:43:55 -04:00
|
|
|
|
2001-07-10 08:52:38 -03:00
|
|
|
|
2001-09-17 20:56:20 -03:00
|
|
|
def test_main():
|
|
|
|
if imp.lock_held():
|
2008-02-02 07:05:00 -04:00
|
|
|
# If the import lock is held, the threads will hang
|
2009-03-26 18:10:30 -03:00
|
|
|
raise unittest.SkipTest("can't run when import lock is held")
|
2001-09-17 20:56:20 -03:00
|
|
|
|
2008-02-02 07:05:00 -04:00
|
|
|
test.test_support.run_unittest(SocketServerTest)
|
2001-07-10 08:52:38 -03:00
|
|
|
|
2001-09-17 20:56:20 -03:00
|
|
|
if __name__ == "__main__":
|
|
|
|
test_main()
|