Delete old test_client and add a gitignore.
This commit is contained in:
parent
a5a6a8fddb
commit
ee965ff4a7
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.egg-info
|
||||||
|
*.py[co]
|
||||||
|
.DS_Store
|
@ -1,520 +0,0 @@
|
|||||||
# Copyright (c) 2010-2012 OpenStack, LLC.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# TODO: More tests
|
|
||||||
import socket
|
|
||||||
import unittest
|
|
||||||
from urlparse import urlparse
|
|
||||||
|
|
||||||
# TODO: mock http connection class with more control over headers
|
|
||||||
from test.unit.proxy.test_server import fake_http_connect
|
|
||||||
|
|
||||||
from swiftclient import client as c
|
|
||||||
|
|
||||||
|
|
||||||
class TestClientException(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_is_exception(self):
|
|
||||||
self.assertTrue(issubclass(c.ClientException, Exception))
|
|
||||||
|
|
||||||
def test_format(self):
|
|
||||||
exc = c.ClientException('something failed')
|
|
||||||
self.assertTrue('something failed' in str(exc))
|
|
||||||
test_kwargs = (
|
|
||||||
'scheme',
|
|
||||||
'host',
|
|
||||||
'port',
|
|
||||||
'path',
|
|
||||||
'query',
|
|
||||||
'status',
|
|
||||||
'reason',
|
|
||||||
'device',
|
|
||||||
)
|
|
||||||
for value in test_kwargs:
|
|
||||||
kwargs = {
|
|
||||||
'http_%s' % value: value,
|
|
||||||
}
|
|
||||||
exc = c.ClientException('test', **kwargs)
|
|
||||||
self.assertTrue(value in str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
class TestJsonImport(unittest.TestCase):
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
reload(json)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
reload(simplejson)
|
|
||||||
|
|
||||||
def test_any(self):
|
|
||||||
self.assertTrue(hasattr(c, 'json_loads'))
|
|
||||||
|
|
||||||
def test_no_simplejson(self):
|
|
||||||
# break simplejson
|
|
||||||
try:
|
|
||||||
import simplejson
|
|
||||||
except ImportError:
|
|
||||||
# not installed, so we don't have to break it for these tests
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
delattr(simplejson, 'loads')
|
|
||||||
reload(c)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from json import loads
|
|
||||||
except ImportError:
|
|
||||||
# this case is stested in _no_json
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.assertEquals(loads, c.json_loads)
|
|
||||||
|
|
||||||
|
|
||||||
class MockHttpTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
def fake_http_connection(*args, **kwargs):
|
|
||||||
_orig_http_connection = c.http_connection
|
|
||||||
return_read = kwargs.get('return_read')
|
|
||||||
|
|
||||||
def wrapper(url, proxy=None):
|
|
||||||
parsed, _conn = _orig_http_connection(url, proxy=proxy)
|
|
||||||
conn = fake_http_connect(*args, **kwargs)()
|
|
||||||
|
|
||||||
def request(*args, **kwargs):
|
|
||||||
return
|
|
||||||
conn.request = request
|
|
||||||
|
|
||||||
conn.has_been_read = False
|
|
||||||
_orig_read = conn.read
|
|
||||||
|
|
||||||
def read(*args, **kwargs):
|
|
||||||
conn.has_been_read = True
|
|
||||||
return _orig_read(*args, **kwargs)
|
|
||||||
conn.read = return_read or read
|
|
||||||
|
|
||||||
return parsed, conn
|
|
||||||
return wrapper
|
|
||||||
self.fake_http_connection = fake_http_connection
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
reload(c)
|
|
||||||
|
|
||||||
|
|
||||||
class TestHttpHelpers(MockHttpTest):
|
|
||||||
|
|
||||||
def test_quote(self):
|
|
||||||
value = 'standard string'
|
|
||||||
self.assertEquals('standard%20string', c.quote(value))
|
|
||||||
value = u'\u0075nicode string'
|
|
||||||
self.assertEquals('unicode%20string', c.quote(value))
|
|
||||||
|
|
||||||
def test_http_connection(self):
|
|
||||||
url = 'http://www.test.com'
|
|
||||||
_junk, conn = c.http_connection(url)
|
|
||||||
self.assertTrue(isinstance(conn, c.HTTPConnection))
|
|
||||||
url = 'https://www.test.com'
|
|
||||||
_junk, conn = c.http_connection(url)
|
|
||||||
self.assertTrue(isinstance(conn, c.HTTPSConnection))
|
|
||||||
url = 'ftp://www.test.com'
|
|
||||||
self.assertRaises(c.ClientException, c.http_connection, url)
|
|
||||||
|
|
||||||
def test_json_request(self):
|
|
||||||
def read(*args, **kwargs):
|
|
||||||
body = {'a': '1',
|
|
||||||
'b': '2'}
|
|
||||||
return c.json_dumps(body)
|
|
||||||
c.http_connection = self.fake_http_connection(200, return_read=read)
|
|
||||||
url = 'http://www.test.com'
|
|
||||||
_junk, conn = c.json_request('GET', url, body={'username': 'user1',
|
|
||||||
'password': 'secure'})
|
|
||||||
self.assertTrue(type(conn) is dict)
|
|
||||||
|
|
||||||
# TODO: following tests are placeholders, need more tests, better coverage
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetAuth(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
self.assertEquals(url, None)
|
|
||||||
self.assertEquals(token, None)
|
|
||||||
|
|
||||||
def test_auth_v1(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf',
|
|
||||||
auth_version="1.0")
|
|
||||||
self.assertEquals(url, None)
|
|
||||||
self.assertEquals(token, None)
|
|
||||||
|
|
||||||
def test_auth_v2(self):
|
|
||||||
def read(*args, **kwargs):
|
|
||||||
acct_url = 'http://127.0.01/AUTH_FOO'
|
|
||||||
body = {'access': {'serviceCatalog':
|
|
||||||
[{u'endpoints': [{'publicURL': acct_url}],
|
|
||||||
'type': 'object-store'}],
|
|
||||||
'token': {'id': 'XXXXXXX'}}}
|
|
||||||
return c.json_dumps(body)
|
|
||||||
c.http_connection = self.fake_http_connection(200, return_read=read)
|
|
||||||
url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf',
|
|
||||||
tenant_name='asdf', auth_version="2.0")
|
|
||||||
self.assertTrue(url.startswith("http"))
|
|
||||||
self.assertTrue(token)
|
|
||||||
|
|
||||||
def test_auth_v2_no_tenant_name(self):
|
|
||||||
def read(*args, **kwargs):
|
|
||||||
acct_url = 'http://127.0.01/AUTH_FOO'
|
|
||||||
body = {'access': {'serviceCatalog':
|
|
||||||
[{u'endpoints': [{'publicURL': acct_url}],
|
|
||||||
'type': 'object-store'}],
|
|
||||||
'token': {'id': 'XXXXXXX'}}}
|
|
||||||
return c.json_dumps(body)
|
|
||||||
c.http_connection = self.fake_http_connection(200, return_read=read)
|
|
||||||
self.assertRaises(c.ClientException, c.get_auth,
|
|
||||||
'http://www.tests.com', 'asdf', 'asdf',
|
|
||||||
auth_version='2.0')
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetAccount(MockHttpTest):
|
|
||||||
|
|
||||||
def test_no_content(self):
|
|
||||||
c.http_connection = self.fake_http_connection(204)
|
|
||||||
value = c.get_account('http://www.test.com', 'asdf')[1]
|
|
||||||
self.assertEquals(value, [])
|
|
||||||
|
|
||||||
|
|
||||||
class TestHeadAccount(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
value = c.head_account('http://www.tests.com', 'asdf')
|
|
||||||
# TODO: Hmm. This doesn't really test too much as it uses a fake that
|
|
||||||
# always returns the same dict. I guess it "exercises" the code, so
|
|
||||||
# I'll leave it for now.
|
|
||||||
self.assertEquals(type(value), dict)
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
body = 'c' * 65
|
|
||||||
c.http_connection = self.fake_http_connection(500, body=body)
|
|
||||||
self.assertRaises(c.ClientException, c.head_account,
|
|
||||||
'http://www.tests.com', 'asdf')
|
|
||||||
try:
|
|
||||||
value = c.head_account('http://www.tests.com', 'asdf')
|
|
||||||
except c.ClientException as e:
|
|
||||||
new_body = "[first 60 chars of response] " + body[0:60]
|
|
||||||
self.assertEquals(e.__str__()[-89:], new_body)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetContainer(MockHttpTest):
|
|
||||||
|
|
||||||
def test_no_content(self):
|
|
||||||
c.http_connection = self.fake_http_connection(204)
|
|
||||||
value = c.get_container('http://www.test.com', 'asdf', 'asdf')[1]
|
|
||||||
self.assertEquals(value, [])
|
|
||||||
|
|
||||||
|
|
||||||
class TestHeadContainer(MockHttpTest):
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
body = 'c' * 60
|
|
||||||
c.http_connection = self.fake_http_connection(500, body=body)
|
|
||||||
self.assertRaises(c.ClientException, c.head_container,
|
|
||||||
'http://www.test.com', 'asdf', 'asdf',
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
value = c.head_container('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
except c.ClientException as e:
|
|
||||||
self.assertEquals(e.http_response_content, body)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPutContainer(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
value = c.put_container('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
self.assertEquals(value, None)
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
body = 'c' * 60
|
|
||||||
c.http_connection = self.fake_http_connection(500, body=body)
|
|
||||||
self.assertRaises(c.ClientException, c.put_container,
|
|
||||||
'http://www.test.com', 'asdf', 'asdf',
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
value = c.put_container('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
except c.ClientException as e:
|
|
||||||
self.assertEquals(e.http_response_content, body)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteContainer(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
value = c.delete_container('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
self.assertEquals(value, None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetObject(MockHttpTest):
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
c.http_connection = self.fake_http_connection(500)
|
|
||||||
self.assertRaises(c.ClientException, c.get_object,
|
|
||||||
'http://www.test.com', 'asdf', 'asdf', 'asdf')
|
|
||||||
|
|
||||||
|
|
||||||
class TestHeadObject(MockHttpTest):
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
c.http_connection = self.fake_http_connection(500)
|
|
||||||
self.assertRaises(c.ClientException, c.head_object,
|
|
||||||
'http://www.test.com', 'asdf', 'asdf', 'asdf')
|
|
||||||
|
|
||||||
|
|
||||||
class TestPutObject(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
args = ('http://www.test.com', 'asdf', 'asdf', 'asdf', 'asdf')
|
|
||||||
value = c.put_object(*args)
|
|
||||||
self.assertTrue(isinstance(value, basestring))
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
body = 'c' * 60
|
|
||||||
c.http_connection = self.fake_http_connection(500, body=body)
|
|
||||||
args = ('http://www.test.com', 'asdf', 'asdf', 'asdf', 'asdf')
|
|
||||||
self.assertRaises(c.ClientException, c.put_object, *args)
|
|
||||||
try:
|
|
||||||
value = c.put_object(*args)
|
|
||||||
except c.ClientException as e:
|
|
||||||
self.assertEquals(e.http_response_content, body)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPostObject(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
args = ('http://www.test.com', 'asdf', 'asdf', 'asdf', {})
|
|
||||||
value = c.post_object(*args)
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
body = 'c' * 60
|
|
||||||
c.http_connection = self.fake_http_connection(500, body=body)
|
|
||||||
args = ('http://www.test.com', 'asdf', 'asdf', 'asdf', {})
|
|
||||||
self.assertRaises(c.ClientException, c.post_object, *args)
|
|
||||||
try:
|
|
||||||
value = c.post_object(*args)
|
|
||||||
except c.ClientException as e:
|
|
||||||
self.assertEquals(e.http_response_content, body)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteObject(MockHttpTest):
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
value = c.delete_object('http://www.test.com', 'asdf', 'asdf', 'asdf')
|
|
||||||
|
|
||||||
def test_server_error(self):
|
|
||||||
c.http_connection = self.fake_http_connection(500)
|
|
||||||
self.assertRaises(c.ClientException, c.delete_object,
|
|
||||||
'http://www.test.com', 'asdf', 'asdf', 'asdf')
|
|
||||||
|
|
||||||
|
|
||||||
class TestConnection(MockHttpTest):
|
|
||||||
|
|
||||||
def test_instance(self):
|
|
||||||
conn = c.Connection('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
self.assertEquals(conn.retries, 5)
|
|
||||||
|
|
||||||
def test_retry(self):
|
|
||||||
c.http_connection = self.fake_http_connection(500)
|
|
||||||
|
|
||||||
def quick_sleep(*args):
|
|
||||||
pass
|
|
||||||
c.sleep = quick_sleep
|
|
||||||
conn = c.Connection('http://www.test.com', 'asdf', 'asdf')
|
|
||||||
self.assertRaises(c.ClientException, conn.head_account)
|
|
||||||
self.assertEquals(conn.attempts, conn.retries + 1)
|
|
||||||
|
|
||||||
def test_resp_read_on_server_error(self):
|
|
||||||
c.http_connection = self.fake_http_connection(500)
|
|
||||||
conn = c.Connection('http://www.test.com', 'asdf', 'asdf', retries=0)
|
|
||||||
|
|
||||||
def get_auth(*args, **kwargs):
|
|
||||||
return 'http://www.new.com', 'new'
|
|
||||||
conn.get_auth = get_auth
|
|
||||||
self.url, self.token = conn.get_auth()
|
|
||||||
|
|
||||||
method_signatures = (
|
|
||||||
(conn.head_account, []),
|
|
||||||
(conn.get_account, []),
|
|
||||||
(conn.head_container, ('asdf',)),
|
|
||||||
(conn.get_container, ('asdf',)),
|
|
||||||
(conn.put_container, ('asdf',)),
|
|
||||||
(conn.delete_container, ('asdf',)),
|
|
||||||
(conn.head_object, ('asdf', 'asdf')),
|
|
||||||
(conn.get_object, ('asdf', 'asdf')),
|
|
||||||
(conn.put_object, ('asdf', 'asdf', 'asdf')),
|
|
||||||
(conn.post_object, ('asdf', 'asdf', {})),
|
|
||||||
(conn.delete_object, ('asdf', 'asdf')),
|
|
||||||
)
|
|
||||||
|
|
||||||
for method, args in method_signatures:
|
|
||||||
self.assertRaises(c.ClientException, method, *args)
|
|
||||||
try:
|
|
||||||
self.assertTrue(conn.http_conn[1].has_been_read)
|
|
||||||
except AssertionError:
|
|
||||||
msg = '%s did not read resp on server error' % method.__name__
|
|
||||||
self.fail(msg)
|
|
||||||
except Exception, e:
|
|
||||||
raise e.__class__("%s - %s" % (method.__name__, e))
|
|
||||||
|
|
||||||
def test_reauth(self):
|
|
||||||
c.http_connection = self.fake_http_connection(401)
|
|
||||||
|
|
||||||
def get_auth(*args, **kwargs):
|
|
||||||
return 'http://www.new.com', 'new'
|
|
||||||
|
|
||||||
def swap_sleep(*args):
|
|
||||||
self.swap_sleep_called = True
|
|
||||||
c.get_auth = get_auth
|
|
||||||
c.http_connection = self.fake_http_connection(200)
|
|
||||||
c.sleep = swap_sleep
|
|
||||||
self.swap_sleep_called = False
|
|
||||||
|
|
||||||
conn = c.Connection('http://www.test.com', 'asdf', 'asdf',
|
|
||||||
preauthurl='http://www.old.com',
|
|
||||||
preauthtoken='old',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEquals(conn.attempts, 0)
|
|
||||||
self.assertEquals(conn.url, 'http://www.old.com')
|
|
||||||
self.assertEquals(conn.token, 'old')
|
|
||||||
|
|
||||||
value = conn.head_account()
|
|
||||||
|
|
||||||
self.assertTrue(self.swap_sleep_called)
|
|
||||||
self.assertEquals(conn.attempts, 2)
|
|
||||||
self.assertEquals(conn.url, 'http://www.new.com')
|
|
||||||
self.assertEquals(conn.token, 'new')
|
|
||||||
|
|
||||||
def test_reset_stream(self):
|
|
||||||
|
|
||||||
class LocalContents(object):
|
|
||||||
|
|
||||||
def __init__(self, tell_value=0):
|
|
||||||
self.already_read = False
|
|
||||||
self.seeks = []
|
|
||||||
self.tell_value = tell_value
|
|
||||||
|
|
||||||
def tell(self):
|
|
||||||
return self.tell_value
|
|
||||||
|
|
||||||
def seek(self, position):
|
|
||||||
self.seeks.append(position)
|
|
||||||
self.already_read = False
|
|
||||||
|
|
||||||
def read(self, size=-1):
|
|
||||||
if self.already_read:
|
|
||||||
return ''
|
|
||||||
else:
|
|
||||||
self.already_read = True
|
|
||||||
return 'abcdef'
|
|
||||||
|
|
||||||
class LocalConnection(object):
|
|
||||||
|
|
||||||
def __init__(self, parsed_url=None):
|
|
||||||
self.reason = ""
|
|
||||||
if parsed_url:
|
|
||||||
self.host = parsed_url.netloc
|
|
||||||
self.port = parsed_url.netloc
|
|
||||||
|
|
||||||
def putrequest(self, *args, **kwargs):
|
|
||||||
return
|
|
||||||
|
|
||||||
def putheader(self, *args, **kwargs):
|
|
||||||
return
|
|
||||||
|
|
||||||
def endheaders(self, *args, **kwargs):
|
|
||||||
return
|
|
||||||
|
|
||||||
def send(self, *args, **kwargs):
|
|
||||||
raise socket.error('oops')
|
|
||||||
|
|
||||||
def request(self, *args, **kwargs):
|
|
||||||
return
|
|
||||||
|
|
||||||
def getresponse(self, *args, **kwargs):
|
|
||||||
self.status = 200
|
|
||||||
return self
|
|
||||||
|
|
||||||
def getheader(self, *args, **kwargs):
|
|
||||||
return 'header'
|
|
||||||
|
|
||||||
def read(self, *args, **kwargs):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def local_http_connection(url, proxy=None):
|
|
||||||
parsed = urlparse(url)
|
|
||||||
return parsed, LocalConnection()
|
|
||||||
|
|
||||||
orig_conn = c.http_connection
|
|
||||||
try:
|
|
||||||
c.http_connection = local_http_connection
|
|
||||||
conn = c.Connection('http://www.example.com', 'asdf', 'asdf',
|
|
||||||
retries=1, starting_backoff=.0001)
|
|
||||||
|
|
||||||
contents = LocalContents()
|
|
||||||
exc = None
|
|
||||||
try:
|
|
||||||
conn.put_object('c', 'o', contents)
|
|
||||||
except socket.error, err:
|
|
||||||
exc = err
|
|
||||||
self.assertEquals(contents.seeks, [0])
|
|
||||||
self.assertEquals(str(exc), 'oops')
|
|
||||||
|
|
||||||
contents = LocalContents(tell_value=123)
|
|
||||||
exc = None
|
|
||||||
try:
|
|
||||||
conn.put_object('c', 'o', contents)
|
|
||||||
except socket.error, err:
|
|
||||||
exc = err
|
|
||||||
self.assertEquals(contents.seeks, [123])
|
|
||||||
self.assertEquals(str(exc), 'oops')
|
|
||||||
|
|
||||||
contents = LocalContents()
|
|
||||||
contents.tell = None
|
|
||||||
exc = None
|
|
||||||
try:
|
|
||||||
conn.put_object('c', 'o', contents)
|
|
||||||
except c.ClientException, err:
|
|
||||||
exc = err
|
|
||||||
self.assertEquals(contents.seeks, [])
|
|
||||||
self.assertEquals(str(exc), "put_object('c', 'o', ...) failure "
|
|
||||||
"and no ability to reset contents for reupload.")
|
|
||||||
finally:
|
|
||||||
c.http_connection = orig_conn
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
Loading…
Reference in New Issue
Block a user