2010-08-24 15:11:01 +02:00
|
|
|
import unittest
|
|
|
|
import osc.core
|
|
|
|
import shutil
|
|
|
|
import tempfile
|
|
|
|
import os
|
|
|
|
import sys
|
2020-06-02 22:22:17 +02:00
|
|
|
try:
|
|
|
|
# Works up to Python 3.8, needed for Python < 3.3 (inc 2.7)
|
|
|
|
from xml.etree import cElementTree as ET
|
|
|
|
except ImportError:
|
|
|
|
# will import a fast implementation from 3.3 onwards, needed
|
|
|
|
# for 3.9+
|
|
|
|
from xml.etree import ElementTree as ET
|
2010-08-24 15:11:01 +02:00
|
|
|
EXPECTED_REQUESTS = []
|
|
|
|
|
2013-04-09 14:22:45 +02:00
|
|
|
try:
|
2013-04-10 11:34:59 +02:00
|
|
|
#python 2.x
|
2013-04-09 14:22:45 +02:00
|
|
|
from cStringIO import StringIO
|
|
|
|
from urllib2 import HTTPHandler, addinfourl, build_opener
|
2013-04-10 11:34:59 +02:00
|
|
|
from urlparse import urlparse, parse_qs
|
2013-04-09 14:22:45 +02:00
|
|
|
except ImportError:
|
|
|
|
from io import StringIO
|
|
|
|
from urllib.request import HTTPHandler, addinfourl, build_opener
|
2013-04-10 11:34:59 +02:00
|
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
|
2018-11-09 10:33:30 +01:00
|
|
|
from io import BytesIO
|
|
|
|
|
2013-04-10 11:34:59 +02:00
|
|
|
def urlcompare(url, *args):
|
|
|
|
"""compare all components of url except query string - it is converted to
|
|
|
|
dict, therefor different ordering does not makes url's different, as well
|
|
|
|
as quoting of a query string"""
|
|
|
|
|
|
|
|
components = urlparse(url)
|
|
|
|
query_args = parse_qs(components.query)
|
|
|
|
components = components._replace(query=None)
|
|
|
|
|
|
|
|
if not args:
|
|
|
|
return False
|
|
|
|
|
|
|
|
for url in args:
|
|
|
|
components2 = urlparse(url)
|
|
|
|
query_args2 = parse_qs(components2.query)
|
|
|
|
components2 = components2._replace(query=None)
|
|
|
|
|
|
|
|
if components != components2 or \
|
|
|
|
query_args != query_args2:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2013-04-09 14:22:45 +02:00
|
|
|
|
tests: Ignore the ordering of attributes in XML documents
Old xml.etree.cElementTree versions (python2) reorder the attributes
while recent xml.etree.cElementTree versions (python3) keep the
document order.
Example:
python3:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
b'<foo y="foo" x="bar" />'
>>>
python2:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
'<foo x="bar" y="foo" />'
>>>
So far, the testsuite compared two serialized XML documents via a simple
string comparison. For instance via,
self.assertEqual(actual_serialized_xml, expected_serialized_xml) where
the expected_serialized_xml is, for instance, a hardcoded str. Obviously,
this would only work for python2 or python3.
In order to support both python versions, we first parse both XML
documents and then compare the corresponding trees (this is OK because
we do not care about comments etc.).
A related issue is the way how the testsuite compares data that is "send"
to the API. So far, this was a plain bytes comparison. Again, this won't
work in case of XML documents (see above). Moreover, we have currently
no notion to "indicate" that the transmitted data is an XML document.
As a workaround, we keep the plain bytes comparison and in case it fails,
we try an xml comparison (see above) as a last resort. Strictly speaking,
this is "wrong" (there might be cases (in the future) where we want to
ensure that the transmitted XML data is bit identical to a fixture file)
but a reasonable comprise for now.
Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
|
|
|
|
|
|
|
def xml_equal(actual, exp):
|
|
|
|
try:
|
|
|
|
actual_xml = ET.fromstring(actual)
|
|
|
|
exp_xml = ET.fromstring(exp)
|
|
|
|
except ET.ParseError:
|
|
|
|
return False
|
|
|
|
todo = [(actual_xml, exp_xml)]
|
|
|
|
while todo:
|
|
|
|
actual_xml, exp_xml = todo.pop(0)
|
|
|
|
if actual_xml.tag != exp_xml.tag:
|
|
|
|
return False
|
|
|
|
if actual_xml.attrib != exp_xml.attrib:
|
|
|
|
return False
|
|
|
|
if actual_xml.text != exp_xml.text:
|
|
|
|
return False
|
|
|
|
if actual_xml.tail != exp_xml.tail:
|
|
|
|
return False
|
|
|
|
if len(actual_xml) != len(exp_xml):
|
|
|
|
return False
|
|
|
|
todo.extend(list(zip(actual_xml, exp_xml)))
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2010-08-24 15:11:01 +02:00
|
|
|
class RequestWrongOrder(Exception):
|
2010-08-30 18:29:24 +02:00
|
|
|
"""raised if an unexpected request is issued to urllib2"""
|
2010-08-24 15:11:01 +02:00
|
|
|
def __init__(self, url, exp_url, method, exp_method):
|
|
|
|
Exception.__init__(self)
|
|
|
|
self.url = url
|
|
|
|
self.exp_url = exp_url
|
|
|
|
self.method = method
|
|
|
|
self.exp_method = exp_method
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '%s, %s, %s, %s' % (self.url, self.exp_url, self.method, self.exp_method)
|
|
|
|
|
2010-08-30 18:29:24 +02:00
|
|
|
class RequestDataMismatch(Exception):
|
|
|
|
"""raised if POSTed or PUTed data doesn't match with the expected data"""
|
|
|
|
def __init__(self, url, got, exp):
|
|
|
|
self.url = url
|
|
|
|
self.got = got
|
|
|
|
self.exp = exp
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '%s, %s, %s' % (self.url, self.got, self.exp)
|
|
|
|
|
2013-04-09 14:22:45 +02:00
|
|
|
class MyHTTPHandler(HTTPHandler):
|
2010-08-24 15:11:01 +02:00
|
|
|
def __init__(self, exp_requests, fixtures_dir):
|
2013-04-09 14:22:45 +02:00
|
|
|
HTTPHandler.__init__(self)
|
2010-08-24 15:11:01 +02:00
|
|
|
self.__exp_requests = exp_requests
|
|
|
|
self.__fixtures_dir = fixtures_dir
|
|
|
|
|
|
|
|
def http_open(self, req):
|
|
|
|
r = self.__exp_requests.pop(0)
|
2013-04-10 11:34:59 +02:00
|
|
|
if not urlcompare(req.get_full_url(), r[1]) or req.get_method() != r[0]:
|
2010-08-24 15:11:01 +02:00
|
|
|
raise RequestWrongOrder(req.get_full_url(), r[1], req.get_method(), r[0])
|
2010-08-30 18:29:24 +02:00
|
|
|
if req.get_method() in ('GET', 'DELETE'):
|
2010-08-24 15:11:01 +02:00
|
|
|
return self.__mock_GET(r[1], **r[2])
|
2010-08-30 18:29:24 +02:00
|
|
|
elif req.get_method() in ('PUT', 'POST'):
|
|
|
|
return self.__mock_PUT(req, **r[2])
|
2010-08-24 15:11:01 +02:00
|
|
|
|
|
|
|
def __mock_GET(self, fullurl, **kwargs):
|
|
|
|
return self.__get_response(fullurl, **kwargs)
|
|
|
|
|
2010-08-30 18:29:24 +02:00
|
|
|
def __mock_PUT(self, req, **kwargs):
|
|
|
|
exp = kwargs.get('exp', None)
|
2013-04-09 14:22:45 +02:00
|
|
|
if exp is not None and 'expfile' in kwargs:
|
2010-08-30 18:29:24 +02:00
|
|
|
raise RuntimeError('either specify exp or expfile')
|
2013-04-09 14:22:45 +02:00
|
|
|
elif 'expfile' in kwargs:
|
2020-06-03 17:46:07 +02:00
|
|
|
exp = open(os.path.join(self.__fixtures_dir, kwargs['expfile']), 'rb').read()
|
2010-09-03 17:46:01 +02:00
|
|
|
elif exp is None:
|
|
|
|
raise RuntimeError('exp or expfile required')
|
2020-06-03 17:46:07 +02:00
|
|
|
else:
|
|
|
|
# for now, assume exp is a str
|
|
|
|
exp = exp.encode('utf-8')
|
|
|
|
# use req.data instead of req.get_data() for python3 compatiblity
|
|
|
|
data = req.data
|
|
|
|
if hasattr(data, 'read'):
|
|
|
|
data = data.read()
|
|
|
|
if data != exp:
|
tests: Ignore the ordering of attributes in XML documents
Old xml.etree.cElementTree versions (python2) reorder the attributes
while recent xml.etree.cElementTree versions (python3) keep the
document order.
Example:
python3:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
b'<foo y="foo" x="bar" />'
>>>
python2:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
'<foo x="bar" y="foo" />'
>>>
So far, the testsuite compared two serialized XML documents via a simple
string comparison. For instance via,
self.assertEqual(actual_serialized_xml, expected_serialized_xml) where
the expected_serialized_xml is, for instance, a hardcoded str. Obviously,
this would only work for python2 or python3.
In order to support both python versions, we first parse both XML
documents and then compare the corresponding trees (this is OK because
we do not care about comments etc.).
A related issue is the way how the testsuite compares data that is "send"
to the API. So far, this was a plain bytes comparison. Again, this won't
work in case of XML documents (see above). Moreover, we have currently
no notion to "indicate" that the transmitted data is an XML document.
As a workaround, we keep the plain bytes comparison and in case it fails,
we try an xml comparison (see above) as a last resort. Strictly speaking,
this is "wrong" (there might be cases (in the future) where we want to
ensure that the transmitted XML data is bit identical to a fixture file)
but a reasonable comprise for now.
Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
|
|
|
# We do not have a notion to explicitly mark xml content. In case
|
|
|
|
# of xml, we do not care about the exact xml representation (for
|
|
|
|
# now). Hence, if both, data and exp, are xml and are "equal",
|
|
|
|
# everything is fine (for now); otherwise, error out
|
|
|
|
# (of course, this is problematic if we want to ensure that XML
|
|
|
|
# documents are bit identical...)
|
|
|
|
if not xml_equal(data, exp):
|
2016-05-29 16:19:43 +02:00
|
|
|
raise RequestDataMismatch(req.get_full_url(), repr(data), repr(exp))
|
2010-08-30 18:29:24 +02:00
|
|
|
return self.__get_response(req.get_full_url(), **kwargs)
|
|
|
|
|
2010-08-24 15:11:01 +02:00
|
|
|
def __get_response(self, url, **kwargs):
|
|
|
|
f = None
|
2013-04-09 14:22:45 +02:00
|
|
|
if 'exception' in kwargs:
|
2010-08-30 18:29:24 +02:00
|
|
|
raise kwargs['exception']
|
2013-04-09 14:22:45 +02:00
|
|
|
if 'text' not in kwargs and 'file' in kwargs:
|
2018-11-09 10:33:30 +01:00
|
|
|
f = BytesIO(open(os.path.join(self.__fixtures_dir, kwargs['file']), 'rb').read())
|
2013-04-09 14:22:45 +02:00
|
|
|
elif 'text' in kwargs and 'file' not in kwargs:
|
2020-06-03 17:49:51 +02:00
|
|
|
f = BytesIO(kwargs['text'].encode('utf-8'))
|
2010-08-24 15:11:01 +02:00
|
|
|
else:
|
|
|
|
raise RuntimeError('either specify text or file')
|
2013-04-09 14:22:45 +02:00
|
|
|
resp = addinfourl(f, {}, url)
|
2011-03-21 16:52:13 +01:00
|
|
|
resp.code = kwargs.get('code', 200)
|
2010-08-24 15:11:01 +02:00
|
|
|
resp.msg = ''
|
|
|
|
return resp
|
|
|
|
|
2010-08-30 18:29:24 +02:00
|
|
|
def urldecorator(method, fullurl, **kwargs):
|
2010-08-24 15:11:01 +02:00
|
|
|
def decorate(test_method):
|
|
|
|
def wrapped_test_method(*args):
|
2010-08-30 18:29:24 +02:00
|
|
|
addExpectedRequest(method, fullurl, **kwargs)
|
2010-08-24 15:11:01 +02:00
|
|
|
test_method(*args)
|
2010-08-24 19:06:49 +02:00
|
|
|
# "rename" method otherwise we cannot specify a TestCaseClass.testName
|
|
|
|
# cmdline arg when using unittest.main()
|
|
|
|
wrapped_test_method.__name__ = test_method.__name__
|
2010-08-24 15:11:01 +02:00
|
|
|
return wrapped_test_method
|
|
|
|
return decorate
|
|
|
|
|
2010-08-30 18:29:24 +02:00
|
|
|
def GET(fullurl, **kwargs):
|
|
|
|
return urldecorator('GET', fullurl, **kwargs)
|
|
|
|
|
|
|
|
def PUT(fullurl, **kwargs):
|
|
|
|
return urldecorator('PUT', fullurl, **kwargs)
|
|
|
|
|
|
|
|
def POST(fullurl, **kwargs):
|
|
|
|
return urldecorator('POST', fullurl, **kwargs)
|
|
|
|
|
|
|
|
def DELETE(fullurl, **kwargs):
|
|
|
|
return urldecorator('DELETE', fullurl, **kwargs)
|
|
|
|
|
2010-08-24 15:11:01 +02:00
|
|
|
def addExpectedRequest(method, url, **kwargs):
|
|
|
|
global EXPECTED_REQUESTS
|
|
|
|
EXPECTED_REQUESTS.append((method, url, kwargs))
|
|
|
|
|
|
|
|
class OscTestCase(unittest.TestCase):
|
2010-12-30 01:57:41 +01:00
|
|
|
def setUp(self, copytree=True):
|
2013-01-18 22:58:53 +01:00
|
|
|
oscrc = os.path.join(self._get_fixtures_dir(), 'oscrc')
|
|
|
|
osc.core.conf.get_config(override_conffile=oscrc,
|
2013-01-18 14:27:01 +01:00
|
|
|
override_no_keyring=True, override_no_gnome_keyring=True)
|
2013-01-18 22:58:53 +01:00
|
|
|
os.environ['OSC_CONFIG'] = oscrc
|
|
|
|
|
2010-08-24 15:11:01 +02:00
|
|
|
self.tmpdir = tempfile.mkdtemp(prefix='osc_test')
|
2010-12-30 01:57:41 +01:00
|
|
|
if copytree:
|
|
|
|
shutil.copytree(os.path.join(self._get_fixtures_dir(), 'osctest'), os.path.join(self.tmpdir, 'osctest'))
|
2010-08-24 15:11:01 +02:00
|
|
|
global EXPECTED_REQUESTS
|
|
|
|
EXPECTED_REQUESTS = []
|
2013-04-09 14:22:45 +02:00
|
|
|
osc.core.conf._build_opener = lambda u: build_opener(MyHTTPHandler(EXPECTED_REQUESTS, self._get_fixtures_dir()))
|
2010-08-24 15:11:01 +02:00
|
|
|
self.stdout = sys.stdout
|
2013-04-09 14:22:45 +02:00
|
|
|
sys.stdout = StringIO()
|
2010-08-24 15:11:01 +02:00
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
self.assertTrue(len(EXPECTED_REQUESTS) == 0)
|
|
|
|
sys.stdout = self.stdout
|
|
|
|
try:
|
|
|
|
shutil.rmtree(self.tmpdir)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _get_fixtures_dir(self):
|
|
|
|
raise NotImplementedError('subclasses should implement this method')
|
2010-08-24 16:06:10 +02:00
|
|
|
|
|
|
|
def _change_to_pkg(self, name):
|
|
|
|
os.chdir(os.path.join(self.tmpdir, 'osctest', name))
|
2010-08-24 18:06:47 +02:00
|
|
|
|
2010-08-30 13:46:49 +02:00
|
|
|
def _check_list(self, fname, exp):
|
2010-08-24 18:06:47 +02:00
|
|
|
fname = os.path.join('.osc', fname)
|
|
|
|
self.assertTrue(os.path.exists(fname))
|
|
|
|
self.assertEqual(open(fname, 'r').read(), exp)
|
|
|
|
|
2010-08-25 11:20:25 +02:00
|
|
|
def _check_addlist(self, exp):
|
2010-08-30 13:46:49 +02:00
|
|
|
self._check_list('_to_be_added', exp)
|
2010-08-25 11:20:25 +02:00
|
|
|
|
|
|
|
def _check_deletelist(self, exp):
|
2010-08-30 13:46:49 +02:00
|
|
|
self._check_list('_to_be_deleted', exp)
|
2010-08-25 11:20:25 +02:00
|
|
|
|
|
|
|
def _check_conflictlist(self, exp):
|
2010-08-30 13:46:49 +02:00
|
|
|
self._check_list('_in_conflict', exp)
|
2010-08-25 11:20:25 +02:00
|
|
|
|
2010-08-24 18:06:47 +02:00
|
|
|
def _check_status(self, p, fname, exp):
|
|
|
|
self.assertEqual(p.status(fname), exp)
|
2010-08-30 18:29:24 +02:00
|
|
|
|
|
|
|
def _check_digests(self, fname, *skipfiles):
|
|
|
|
fname = os.path.join(self._get_fixtures_dir(), fname)
|
tests: Ignore the ordering of attributes in XML documents
Old xml.etree.cElementTree versions (python2) reorder the attributes
while recent xml.etree.cElementTree versions (python3) keep the
document order.
Example:
python3:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
b'<foo y="foo" x="bar" />'
>>>
python2:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
'<foo x="bar" y="foo" />'
>>>
So far, the testsuite compared two serialized XML documents via a simple
string comparison. For instance via,
self.assertEqual(actual_serialized_xml, expected_serialized_xml) where
the expected_serialized_xml is, for instance, a hardcoded str. Obviously,
this would only work for python2 or python3.
In order to support both python versions, we first parse both XML
documents and then compare the corresponding trees (this is OK because
we do not care about comments etc.).
A related issue is the way how the testsuite compares data that is "send"
to the API. So far, this was a plain bytes comparison. Again, this won't
work in case of XML documents (see above). Moreover, we have currently
no notion to "indicate" that the transmitted data is an XML document.
As a workaround, we keep the plain bytes comparison and in case it fails,
we try an xml comparison (see above) as a last resort. Strictly speaking,
this is "wrong" (there might be cases (in the future) where we want to
ensure that the transmitted XML data is bit identical to a fixture file)
but a reasonable comprise for now.
Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
|
|
|
with open(os.path.join('.osc', '_files'), 'r') as f:
|
|
|
|
files_act = f.read()
|
|
|
|
with open(fname, 'r') as f:
|
|
|
|
files_exp = f.read()
|
|
|
|
self.assertXMLEqual(files_act, files_exp)
|
|
|
|
root = ET.fromstring(files_act)
|
2010-08-30 18:29:24 +02:00
|
|
|
for i in root.findall('entry'):
|
|
|
|
if i.get('name') in skipfiles:
|
|
|
|
continue
|
|
|
|
self.assertTrue(os.path.exists(os.path.join('.osc', i.get('name'))))
|
|
|
|
self.assertEqual(osc.core.dgst(os.path.join('.osc', i.get('name'))), i.get('md5'))
|
2013-01-18 20:38:25 +01:00
|
|
|
|
tests: Ignore the ordering of attributes in XML documents
Old xml.etree.cElementTree versions (python2) reorder the attributes
while recent xml.etree.cElementTree versions (python3) keep the
document order.
Example:
python3:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
b'<foo y="foo" x="bar" />'
>>>
python2:
>>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>'))
'<foo x="bar" y="foo" />'
>>>
So far, the testsuite compared two serialized XML documents via a simple
string comparison. For instance via,
self.assertEqual(actual_serialized_xml, expected_serialized_xml) where
the expected_serialized_xml is, for instance, a hardcoded str. Obviously,
this would only work for python2 or python3.
In order to support both python versions, we first parse both XML
documents and then compare the corresponding trees (this is OK because
we do not care about comments etc.).
A related issue is the way how the testsuite compares data that is "send"
to the API. So far, this was a plain bytes comparison. Again, this won't
work in case of XML documents (see above). Moreover, we have currently
no notion to "indicate" that the transmitted data is an XML document.
As a workaround, we keep the plain bytes comparison and in case it fails,
we try an xml comparison (see above) as a last resort. Strictly speaking,
this is "wrong" (there might be cases (in the future) where we want to
ensure that the transmitted XML data is bit identical to a fixture file)
but a reasonable comprise for now.
Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
|
|
|
def assertXMLEqual(self, act, exp):
|
|
|
|
if xml_equal(act, exp):
|
|
|
|
return
|
|
|
|
# ok, xmls are different, hence, assertEqual is expected to fail
|
|
|
|
# (we just use it in order to get a "nice" error message)
|
|
|
|
self.assertEqual(act, exp)
|
|
|
|
# not reached (unless assertEqual is overridden in an incompatible way)
|
|
|
|
raise RuntimeError('assertEqual assumptions violated')
|
|
|
|
|
2013-01-18 20:38:25 +01:00
|
|
|
def assertEqualMultiline(self, got, exp):
|
|
|
|
if (got + exp).find('\n') == -1:
|
|
|
|
self.assertEqual(got, exp)
|
|
|
|
else:
|
|
|
|
start_delim = "\n" + (" 8< ".join(["-----"] * 8)) + "\n"
|
|
|
|
end_delim = "\n" + (" >8 ".join(["-----"] * 8)) + "\n\n"
|
|
|
|
self.assertEqual(got, exp,
|
|
|
|
"got:" + start_delim + got + end_delim +
|
|
|
|
"expected:" + start_delim + exp + end_delim)
|