Skip to content

Commit ee23463

Browse files
committed
Merge remote-tracking branch 'origin/test/migwsgi' into experimental
2 parents db3ae71 + e48b12e commit ee23463

File tree

5 files changed

+369
-6
lines changed

5 files changed

+369
-6
lines changed

local-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ autopep8;python_version >= "3"
77
# NOTE: paramiko-3.0.0 dropped python2 and python3.6 support
88
paramiko;python_version >= "3.7"
99
paramiko<3;python_version < "3.7"
10+
werkzeug

mig/shared/compat.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@
4646
_TYPE_UNICODE = type(u"")
4747

4848

49+
if PY2:
50+
class SimpleNamespace(dict):
51+
"""Bare minimum SimpleNamespace for Python 2."""
52+
53+
def __getattribute__(self, name):
54+
if name == '__dict__':
55+
return dict(**self)
56+
57+
return self[name]
58+
else:
59+
from types import SimpleNamespace
60+
61+
4962
def _is_unicode(val):
5063
"""Return boolean indicating if the value is a unicode string.
5164

mig/wsgi-bin/migwsgi.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def object_type_info(object_type):
5353

5454

5555
def stub(configuration, client_id, import_path, backend, user_arguments_dict,
56-
environ):
56+
environ, _import_module=importlib.import_module):
5757
"""Run backend on behalf of client_id with supplied user_arguments_dict.
5858
I.e. import main from import_path and execute it with supplied arguments.
5959
"""
@@ -91,7 +91,7 @@ def stub(configuration, client_id, import_path, backend, user_arguments_dict,
9191

9292
# _logger.debug("import main from %r" % import_path)
9393
# NOTE: dynamic module loading to find corresponding main function
94-
module_handle = importlib.import_module(import_path)
94+
module_handle = _import_module(import_path)
9595
main = module_handle.main
9696
except Exception as err:
9797
_logger.error("%s could not import %r (%s): %s" %
@@ -187,7 +187,8 @@ def _flush_wsgi_errors():
187187
environ['wsgi.errors'].close()
188188

189189

190-
def application(environ, start_response):
190+
def application(environ, start_response, configuration=None,
191+
_import_module=importlib.import_module, _set_os_environ=True):
191192
"""MiG app called automatically by WSGI.
192193
193194
*environ* is a dictionary populated by the server with CGI-like variables
@@ -238,7 +239,8 @@ def application(environ, start_response):
238239
os_env_value))
239240

240241
# Assign updated environ to LOCAL os.environ for the rest of this session
241-
os.environ = environ
242+
if _set_os_environ:
243+
os.environ(environ)
242244

243245
# NOTE: enable to debug runtime environment to apache error log
244246
# print("DEBUG: python %s" %
@@ -250,7 +252,9 @@ def application(environ, start_response):
250252
if sys.version_info[0] < 3:
251253
sys.stdout = sys.stderr
252254

253-
configuration = get_configuration_object()
255+
if configuration is None:
256+
configuration = get_configuration_object()
257+
254258
_logger = configuration.logger
255259

256260
# NOTE: replace default wsgi errors to apache error log with our own logs
@@ -321,7 +325,8 @@ def application(environ, start_response):
321325
# _logger.debug("wsgi handling script: %s" % script_name)
322326
(output_objs, ret_val) = stub(configuration, client_id,
323327
module_path, backend,
324-
user_arguments_dict, environ)
328+
user_arguments_dict, environ,
329+
_import_module=_import_module)
325330
else:
326331
_logger.warning("wsgi handling refused script:%s" % script_name)
327332
(output_objs, ret_val) = reject_main(client_id,

tests/support/wsgisupp.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# wsgisupp - test support library for WSGI
6+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23+
#
24+
# -- END_HEADER ---
25+
#
26+
27+
"""Test support library for WSGI."""
28+
29+
from collections import namedtuple
30+
import codecs
31+
from io import BytesIO
32+
from werkzeug.datastructures import MultiDict
33+
34+
from tests.support._env import PY2
35+
36+
if PY2:
37+
from urllib import urlencode
38+
from urlparse import urlparse
39+
else:
40+
from urllib.parse import urlencode, urlparse
41+
42+
43+
# named type representing the tuple that is passed to WSGI handlers
44+
_PreparedWsgi = namedtuple('_PreparedWsgi', ['environ', 'start_response'])
45+
46+
47+
class FakeWsgiStartResponse:
48+
"""Glue object that conforms to the same interface as the start_response()
49+
in the WSGI specs but records the calls to it such that they can be
50+
inspected and, for our purposes, asserted against."""
51+
52+
def __init__(self):
53+
self.calls = []
54+
55+
def __call__(self, status, headers, exc=None):
56+
self.calls.append((status, headers, exc))
57+
58+
59+
def create_wsgi_environ(configuration, wsgi_url, method='GET', query=None, headers=None, form=None):
60+
"""Populate the necessary variables that will constitute a valid WSGI
61+
environment given a URL to which we will make a requests under test and
62+
various other options that set up the nature of that request."""
63+
64+
parsed_url = urlparse(wsgi_url)
65+
66+
if query:
67+
method = 'GET'
68+
69+
request_query = urlencode(query)
70+
wsgi_input = ()
71+
elif form:
72+
method = 'POST'
73+
request_query = ''
74+
75+
body = urlencode(MultiDict(form)).encode('ascii')
76+
77+
headers = headers or {}
78+
if not 'Content-Type' in headers:
79+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
80+
81+
headers['Content-Length'] = str(len(body))
82+
wsgi_input = BytesIO(body)
83+
else:
84+
request_query = parsed_url.query
85+
wsgi_input = ()
86+
87+
class _errors:
88+
def close():
89+
pass
90+
91+
environ = {}
92+
environ['wsgi.errors'] = _errors()
93+
environ['wsgi.input'] = wsgi_input
94+
environ['wsgi.url_scheme'] = parsed_url.scheme
95+
environ['wsgi.version'] = (1, 0)
96+
environ['MIG_CONF'] = configuration.config_file
97+
environ['HTTP_HOST'] = parsed_url.netloc
98+
environ['PATH_INFO'] = parsed_url.path
99+
environ['QUERY_STRING'] = request_query
100+
environ['REQUEST_METHOD'] = method
101+
environ['SCRIPT_URI'] = ''.join(
102+
('http://', environ['HTTP_HOST'], environ['PATH_INFO']))
103+
104+
if headers:
105+
for k, v in headers.items():
106+
header_key = k.replace('-', '_').upper()
107+
if header_key.startswith('CONTENT'):
108+
# Content-* headers must not be prefixed in WSGI
109+
pass
110+
else:
111+
header_key = "HTTP_%s" % (header_key),
112+
environ[header_key] = v
113+
114+
return environ
115+
116+
117+
def create_wsgi_start_response():
118+
return FakeWsgiStartResponse()
119+
120+
121+
def prepare_wsgi(configuration, url, **kwargs):
122+
return _PreparedWsgi(
123+
create_wsgi_environ(configuration, url, **kwargs),
124+
create_wsgi_start_response()
125+
)
126+
127+
128+
def _trigger_and_unpack_result(wsgi_result):
129+
chunks = list(wsgi_result)
130+
assert len(chunks) > 0, "invocation returned no output"
131+
complete_value = b''.join(chunks)
132+
decoded_value = codecs.decode(complete_value, 'utf8')
133+
return decoded_value
134+
135+
136+
class WsgiAssertMixin:
137+
"""Custom assertions for verifying server code executed under test."""
138+
139+
def assertWsgiResponse(self, wsgi_result, fake_wsgi, expected_status_code):
140+
assert isinstance(fake_wsgi, _PreparedWsgi)
141+
142+
content = _trigger_and_unpack_result(wsgi_result)
143+
144+
def called_once(fake):
145+
assert hasattr(fake, 'calls')
146+
return len(fake.calls) == 1
147+
148+
fake_start_response = fake_wsgi.start_response
149+
150+
try:
151+
self.assertTrue(called_once(fake_start_response))
152+
except AssertionError:
153+
if len(fake.calls) == 0:
154+
raise AssertionError("WSGI handler did not respond")
155+
else:
156+
raise AssertionError("WSGI handler responded more than once")
157+
158+
wsgi_call = fake_start_response.calls[0]
159+
160+
# check for expected HTTP status code
161+
wsgi_status = wsgi_call[0]
162+
actual_status_code = int(wsgi_status[0:3])
163+
self.assertEqual(actual_status_code, expected_status_code)
164+
165+
headers = dict(wsgi_call[1])
166+
167+
return content, headers

0 commit comments

Comments
 (0)