Repository: falconry/falcon

Issue:
Test failure on windows
Tests on windows have some failures and errors.

The summary is `47 failed, 2094 passed, 79 skipped, 1 warning, 16 errors in 1885.17s (0:31:25)`

The errors output is here:
https://gist.github.com/CaselIT/16792866e7dd2c545b186207976e060e

Some failues are related to asgi, while other are simple stuff like `/` vs `\` in the path.
Probably a skip on the incompatible tests and the use of `os.path.normpath` would solve most of these

This is mainly an issue to let you know this, I don't think it's really necessary fixing these, since the rests run so slow on windows (31 minutes) that it would not be practical to develop on it.



Discussion:
Hi :wave:,

Thanks for using Falcon. The large amount of time and effort needed to
maintain the project and develop new features is not sustainable without
the generous financial support of community members like you.

Please consider helping us secure the future of the Falcon framework with a
one-time or recurring donation.

Thank you for your support!


Resolving patch:
diff --git a/falcon/response.py b/falcon/response.py
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -1069,5 +1069,6 @@ def __init__(self):
         self.default_media_type = DEFAULT_MEDIA_TYPE
         self.media_handlers = Handlers()
 
-        mimetypes.init()
+        if not mimetypes.inited:
+            mimetypes.init()
         self.static_media_types = mimetypes.types_map
diff --git a/falcon/routing/static.py b/falcon/routing/static.py
--- a/falcon/routing/static.py
+++ b/falcon/routing/static.py
@@ -49,13 +49,16 @@ def __init__(self, prefix, directory, downloadable=False, fallback_filename=None
         if not prefix.startswith('/'):
             raise ValueError("prefix must start with '/'")
 
-        if not os.path.isabs(directory):
+        self._directory = os.path.normpath(directory)
+        if not os.path.isabs(self._directory):
             raise ValueError('directory must be an absolute path')
 
         if fallback_filename is None:
             self._fallback_filename = None
         else:
-            self._fallback_filename = os.path.join(directory, fallback_filename)
+            self._fallback_filename = os.path.normpath(
+                os.path.join(self._directory, fallback_filename)
+            )
             if not os.path.isfile(self._fallback_filename):
                 raise ValueError('fallback_filename is not a file')
 
@@ -66,7 +69,6 @@ def __init__(self, prefix, directory, downloadable=False, fallback_filename=None
             prefix += '/'
 
         self._prefix = prefix
-        self._directory = directory
         self._downloadable = downloadable
 
     def match(self, path):


Test patch:
diff --git a/tests/asgi/_asgi_test_app.py b/tests/asgi/_asgi_test_app.py
--- a/tests/asgi/_asgi_test_app.py
+++ b/tests/asgi/_asgi_test_app.py
@@ -1,11 +1,14 @@
 import asyncio
 from collections import Counter
+import sys
 import time
 
 import falcon
 import falcon.asgi
 import falcon.util
 
+_WIN32 = sys.platform.startswith('win')
+
 
 class Things:
     def __init__(self):
@@ -67,7 +70,8 @@ def callmesafely(a, b, c=None):
         cms = falcon.util.wrap_sync_to_async(callmesafely, threadsafe=False)
         loop = falcon.util.get_loop()
 
-        num_cms_tasks = 1000
+        # NOTE(caselit): on windows it takes more time so create less tasks
+        num_cms_tasks = 100 if _WIN32 else 1000
 
         for i in range(num_cms_tasks):
             # NOTE(kgriffs): create_task() is used here, so that the coroutines
diff --git a/tests/asgi/test_asgi_servers.py b/tests/asgi/test_asgi_servers.py
--- a/tests/asgi/test_asgi_servers.py
+++ b/tests/asgi/test_asgi_servers.py
@@ -2,6 +2,7 @@
 import os
 import platform
 import subprocess
+import sys
 import time
 
 import pytest
@@ -14,6 +15,7 @@
 _MODULE_DIR = os.path.abspath(os.path.dirname(__file__))
 
 _PYPY = platform.python_implementation() == 'PyPy'
+_WIN32 = sys.platform.startswith('win')
 
 _SERVER_HOST = '127.0.0.1'
 _SIZE_1_KB = 1024
@@ -101,6 +103,15 @@ def _run_server_isolated(process_factory, host, port):
 
     yield server
 
+    if _WIN32:
+        import signal
+        print('\n[Sending CTRL_C_EVENT to server process...]')
+        server.send_signal(signal.CTRL_C_EVENT)
+        try:
+            server.wait(timeout=10)
+        except (KeyboardInterrupt, subprocess.TimeoutExpired):
+            pass
+
     print('\n[Sending SIGTERM to server process...]')
     server.terminate()
 
@@ -148,6 +159,8 @@ def _daphne_factory(host, port):
 @pytest.fixture(params=[_uvicorn_factory, _daphne_factory])
 def server_base_url(request):
     process_factory = request.param
+    if _WIN32 and process_factory == _daphne_factory:
+        pytest.skip('daphne does not support windows')
 
     for i in range(3):
         server_port = testing.get_unused_port()
diff --git a/tests/asgi/test_boundedstream_asgi.py b/tests/asgi/test_boundedstream_asgi.py
--- a/tests/asgi/test_boundedstream_asgi.py
+++ b/tests/asgi/test_boundedstream_asgi.py
@@ -13,7 +13,7 @@
     b'catsup',
     b'\xDE\xAD\xBE\xEF' * 512,
     testing.rand_string(1, 2048),
-])
+], ids=['empty', 'null', 'null-ff', 'normal', 'long', 'random'])
 @pytest.mark.parametrize('extra_body', [True, False])
 @pytest.mark.parametrize('set_content_length', [True, False])
 def test_read_all(body, extra_body, set_content_length):
@@ -155,7 +155,7 @@ async def test_iteration():
     b'catsup',
     b'\xDE\xAD\xBE\xEF' * 512,
     testing.rand_string(1, 2048).encode(),
-])
+], ids=['empty', 'null', 'null-ff', 'normal', 'long', 'random'])
 @pytest.mark.parametrize('chunk_size', [1, 2, 10, 64, 100, 1000, 10000])
 def test_read_chunks(body, chunk_size):
     def stream():
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,7 +3,7 @@
 import falcon
 
 
-@pytest.fixture(params=[True, False])
+@pytest.fixture(params=[True, False], ids=['asgi', 'wsgi'])
 def asgi(request):
     is_asgi = request.param
 
diff --git a/tests/test_cmd_print_api.py b/tests/test_cmd_print_api.py
--- a/tests/test_cmd_print_api.py
+++ b/tests/test_cmd_print_api.py
@@ -1,4 +1,5 @@
 import io
+from os.path import normpath
 
 import pytest
 
@@ -46,7 +47,7 @@ def test_traverse_with_verbose(app):
         get_info, options_info = options_info, get_info
 
     assert options_info.startswith('-->OPTIONS')
-    assert 'falcon/responders.py:' in options_info
+    assert '{}:'.format(normpath('falcon/responders.py')) in options_info
 
     assert get_info.startswith('-->GET')
 
@@ -54,10 +55,11 @@ def test_traverse_with_verbose(app):
     # 18 or 25 (in the case of DummyResourceAsync) in the present file.
     # Adjust the test if the said responder is relocated, or just check for
     # any number if this becomes too painful to maintain.
+    path = normpath('tests/test_cmd_print_api.py')
 
     assert (
-        get_info.endswith('tests/test_cmd_print_api.py:13') or
-        get_info.endswith('tests/test_cmd_print_api.py:20')
+        get_info.endswith('{}:14'.format(path)) or
+        get_info.endswith('{}:21'.format(path))
     )
 
 
diff --git a/tests/test_response.py b/tests/test_response.py
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,6 +1,8 @@
+from unittest.mock import MagicMock
+
 import pytest
 
-from falcon import MEDIA_TEXT
+from falcon import MEDIA_TEXT, ResponseOptions
 
 from _util import create_resp  # NOQA
 
@@ -58,3 +60,18 @@ def test_response_removed_stream_len(resp):
 
     with pytest.raises(AttributeError):
         resp.stream_len
+
+
+def test_response_option_mimetype_init(monkeypatch):
+    mock = MagicMock()
+    mock.inited = False
+    monkeypatch.setattr('falcon.response.mimetypes', mock)
+
+    ro = ResponseOptions()
+
+    assert ro.static_media_types is mock.types_map
+    mock.reset_mock()
+    mock.inited = True
+    ro = ResponseOptions()
+    assert ro.static_media_types is mock.types_map
+    mock.init.assert_not_called()
diff --git a/tests/test_static.py b/tests/test_static.py
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -12,11 +12,11 @@
 import _util  # NOQA
 
 
-@pytest.fixture(params=[True, False])
-def client(request):
-    app = _util.create_app(asgi=request.param)
+@pytest.fixture()
+def client(asgi):
+    app = _util.create_app(asgi=asgi)
     client = testing.TestClient(app)
-    client.asgi = request.param
+    client.asgi = asgi
     return client
 
 
@@ -136,6 +136,12 @@ def test_invalid_args_fallback_filename(client, default):
         client.app.add_static_route(prefix, directory, fallback_filename=default)
 
 
+# NOTE(caselit) depending on the system configuration mime types can have alternative names
+_MIME_ALTERNATIVE = {
+    'application/zip': ('application/zip', 'application/x-zip-compressed')
+}
+
+
 @pytest.mark.parametrize('uri_prefix, uri_path, expected_path, mtype', [
     ('/static/', '/css/test.css', '/css/test.css', 'text/css'),
     ('/static', '/css/test.css', '/css/test.css', 'text/css'),
@@ -180,8 +186,8 @@ async def run():
         sr(req, resp)
         body = resp.stream.read()
 
-    assert resp.content_type == mtype
-    assert body.decode() == '/var/www/statics' + expected_path
+    assert resp.content_type in _MIME_ALTERNATIVE.get(mtype, (mtype, ))
+    assert body.decode() == os.path.normpath('/var/www/statics' + expected_path)
 
 
 def test_lifo(client, monkeypatch):
@@ -192,11 +198,11 @@ def test_lifo(client, monkeypatch):
 
     response = client.simulate_request(path='/downloads/thing.zip')
     assert response.status == falcon.HTTP_200
-    assert response.text == '/opt/somesite/downloads/thing.zip'
+    assert response.text == os.path.normpath('/opt/somesite/downloads/thing.zip')
 
     response = client.simulate_request(path='/downloads/archive/thingtoo.zip')
     assert response.status == falcon.HTTP_200
-    assert response.text == '/opt/somesite/x/thingtoo.zip'
+    assert response.text == os.path.normpath('/opt/somesite/x/thingtoo.zip')
 
 
 def test_lifo_negative(client, monkeypatch):
@@ -207,11 +213,11 @@ def test_lifo_negative(client, monkeypatch):
 
     response = client.simulate_request(path='/downloads/thing.zip')
     assert response.status == falcon.HTTP_200
-    assert response.text == '/opt/somesite/downloads/thing.zip'
+    assert response.text == os.path.normpath('/opt/somesite/downloads/thing.zip')
 
     response = client.simulate_request(path='/downloads/archive/thingtoo.zip')
     assert response.status == falcon.HTTP_200
-    assert response.text == '/opt/somesite/downloads/archive/thingtoo.zip'
+    assert response.text == os.path.normpath('/opt/somesite/downloads/archive/thingtoo.zip')
 
 
 def test_downloadable(client, monkeypatch):
@@ -243,6 +249,7 @@ def test_downloadable_not_found(client):
 @pytest.mark.parametrize('uri, default, expected, content_type', [
     ('', 'default', 'default', 'application/octet-stream'),
     ('other', 'default.html', 'default.html', 'text/html'),
+    ('zip', 'default.zip', 'default.zip', 'application/zip'),
     ('index2', 'index', 'index2', 'application/octet-stream'),
     ('absolute', '/foo/bar/index', '/foo/bar/index', 'application/octet-stream'),
     ('docs/notes/test.txt', 'index.html', 'index.html', 'text/html'),
@@ -253,13 +260,13 @@ def test_fallback_filename(asgi, uri, default, expected, content_type, downloada
                            monkeypatch):
 
     def mock_open(path, mode):
-        if default in path:
+        if os.path.normpath(default) in path:
             return io.BytesIO(path.encode())
 
         raise IOError()
 
     monkeypatch.setattr(io, 'open', mock_open)
-    monkeypatch.setattr('os.path.isfile', lambda file: default in file)
+    monkeypatch.setattr('os.path.isfile', lambda file: os.path.normpath(default) in file)
 
     sr = create_sr(
         asgi,
@@ -290,8 +297,8 @@ async def run():
         body = resp.stream.read()
 
     assert sr.match(req.path)
-    assert body.decode() == os.path.join('/var/www/statics', expected)
-    assert resp.content_type == content_type
+    assert body.decode() == os.path.normpath(os.path.join('/var/www/statics', expected))
+    assert resp.content_type in _MIME_ALTERNATIVE.get(content_type, (content_type, ))
 
     if downloadable:
         assert os.path.basename(expected) in resp.downloadable_as
@@ -329,7 +336,7 @@ def test(prefix, directory, expected):
             assert response.status == falcon.HTTP_404
         else:
             assert response.status == falcon.HTTP_200
-            assert response.text == directory + expected
+            assert response.text == os.path.normpath(directory + expected)
 
     test('/static', '/opt/somesite/static/', static_exp)
     test('/assets', '/opt/somesite/assets/', assert_axp)
@@ -357,7 +364,7 @@ def test_filesystem_traversal_fuse(client, monkeypatch):
     def suspicious_normpath(path):
         return 'assets/../../../../' + path
 
-    monkeypatch.setattr('os.path.normpath', suspicious_normpath)
     client.app.add_static_route('/static', '/etc/nginx/includes/static-data')
+    monkeypatch.setattr('os.path.normpath', suspicious_normpath)
     response = client.simulate_request(path='/static/shadow')
     assert response.status == falcon.HTTP_404