From: Ben Darnell Date: Sun, 19 May 2013 17:11:53 +0000 (-0400) Subject: Merge remote-tracking branch 'birknilson/static_version_override' into merge X-Git-Tag: v3.1.0~61 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=aadfa6685288eb7bd8756d6e94c5d9ee9bcf4b2d;p=thirdparty%2Ftornado.git Merge remote-tracking branch 'birknilson/static_version_override' into merge Conflicts: tornado/test/web_test.py --- aadfa6685288eb7bd8756d6e94c5d9ee9bcf4b2d diff --cc tornado/test/web_test.py index a4a089833,c5e6ce2d8..c732e922b --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@@ -712,62 -502,50 +712,60 @@@ class ErrorResponseTest(WebTestCase) url("/write_error", WriteErrorHandler), url("/get_error_html", GetErrorHtmlHandler), url("/failed_write_error", FailedWriteErrorHandler), - ]) + ] def test_default(self): - response = self.fetch("/default") - self.assertEqual(response.code, 500) - self.assertTrue(b("500: Internal Server Error") in response.body) + with ExpectLog(app_log, "Uncaught exception"): + response = self.fetch("/default") + self.assertEqual(response.code, 500) + self.assertTrue(b"500: Internal Server Error" in response.body) - response = self.fetch("/default?status=503") - self.assertEqual(response.code, 503) - self.assertTrue(b("503: Service Unavailable") in response.body) + response = self.fetch("/default?status=503") + self.assertEqual(response.code, 503) + self.assertTrue(b"503: Service Unavailable" in response.body) def test_write_error(self): - response = self.fetch("/write_error") - self.assertEqual(response.code, 500) - self.assertEqual(b("Exception: ZeroDivisionError"), response.body) + with ExpectLog(app_log, "Uncaught exception"): + response = self.fetch("/write_error") + self.assertEqual(response.code, 500) + self.assertEqual(b"Exception: ZeroDivisionError", response.body) - response = self.fetch("/write_error?status=503") - self.assertEqual(response.code, 503) - self.assertEqual(b("Status: 503"), response.body) + response = self.fetch("/write_error?status=503") + self.assertEqual(response.code, 503) + self.assertEqual(b"Status: 503", response.body) def test_get_error_html(self): - response = self.fetch("/get_error_html") - self.assertEqual(response.code, 500) - self.assertEqual(b("Exception: ZeroDivisionError"), response.body) + with ExpectLog(app_log, "Uncaught exception"): + response = self.fetch("/get_error_html") + self.assertEqual(response.code, 500) + self.assertEqual(b"Exception: ZeroDivisionError", response.body) - response = self.fetch("/get_error_html?status=503") - self.assertEqual(response.code, 503) - self.assertEqual(b("Status: 503"), response.body) + response = self.fetch("/get_error_html?status=503") + self.assertEqual(response.code, 503) + self.assertEqual(b"Status: 503", response.body) def test_failed_write_error(self): - response = self.fetch("/failed_write_error") - self.assertEqual(response.code, 500) - self.assertEqual(b(""), response.body) + with ExpectLog(app_log, "Uncaught exception"): + response = self.fetch("/failed_write_error") + self.assertEqual(response.code, 500) + self.assertEqual(b"", response.body) -class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase): - def get_app(self): + +@wsgi_safe +class StaticFileTest(WebTestCase): + # The expected MD5 hash of robots.txt, used in tests that call + # StaticFileHandler.get_version + robots_txt_hash = b"f71d20196d4caf35b6a670db8c70b03d" + + def get_handlers(self): class StaticUrlHandler(RequestHandler): def get(self, path): - self.write(self.static_url(path)) + with_v = int(self.get_argument('include_version', 1)) + self.write(self.static_url(path, include_version=with_v)) - class AbsoluteStaticUrlHandler(RequestHandler): + class AbsoluteStaticUrlHandler(StaticUrlHandler): include_host = True - def get(self, path): - self.write(self.static_url(path)) - class OverrideStaticUrlHandler(RequestHandler): def get(self, path): do_include = bool(self.get_argument("include_host")) @@@ -811,12 -585,18 +809,21 @@@ def test_absolute_static_url(self): response = self.fetch("/abs_static_url/robots.txt") - self.assertEqual(response.body, - utf8(self.get_url("/") + "static/robots.txt?v=f71d2")) + self.assertEqual(response.body, ( + utf8(self.get_url("/")) + + b"static/robots.txt?v=" + + self.robots_txt_hash + )) + def test_relative_version_exclusion(self): + response = self.fetch("/static_url/robots.txt?include_version=0") - self.assertEqual(response.body, b("/static/robots.txt")) ++ self.assertEqual(response.body, b"/static/robots.txt") + + def test_absolute_version_exclusion(self): + response = self.fetch("/abs_static_url/robots.txt?include_version=0") + self.assertEqual(response.body, + utf8(self.get_url("/") + "static/robots.txt")) + def test_include_host_override(self): self._trigger_include_host_check(False) self._trigger_include_host_check(True) @@@ -826,149 -606,24 +833,149 @@@ response = self.fetch(path % int(include_host)) self.assertEqual(response.body, utf8(str(True))) -class CustomStaticFileTest(AsyncHTTPTestCase, LogTrapTestCase): - def get_app(self): - class MyStaticFileHandler(StaticFileHandler): - def get(self, path): - path = self.parse_url_path(path) - assert path == "foo.txt" - self.write("bar") + def test_static_304_if_modified_since(self): + response1 = self.fetch("/static/robots.txt") + response2 = self.fetch("/static/robots.txt", headers={ + 'If-Modified-Since': response1.headers['Last-Modified']}) + self.assertEqual(response2.code, 304) + self.assertTrue('Content-Length' not in response2.headers) + self.assertTrue('Last-Modified' not in response2.headers) + + def test_static_304_if_none_match(self): + response1 = self.fetch("/static/robots.txt") + response2 = self.fetch("/static/robots.txt", headers={ + 'If-None-Match': response1.headers['Etag']}) + self.assertEqual(response2.code, 304) + + def test_static_if_modified_since_pre_epoch(self): + # On windows, the functions that work with time_t do not accept + # negative values, and at least one client (processing.js) seems + # to use if-modified-since 1/1/1960 as a cache-busting technique. + response = self.fetch("/static/robots.txt", headers={ + 'If-Modified-Since': 'Fri, 01 Jan 1960 00:00:00 GMT'}) + self.assertEqual(response.code, 200) + + def test_static_if_modified_since_time_zone(self): + # Instead of the value from Last-Modified, make requests with times + # chosen just before and after the known modification time + # of the file to ensure that the right time zone is being used + # when parsing If-Modified-Since. + stat = os.stat(os.path.join(os.path.dirname(__file__), + 'static/robots.txt')) + + response = self.fetch('/static/robots.txt', headers={ + 'If-Modified-Since': format_timestamp(stat.st_mtime - 1)}) + self.assertEqual(response.code, 200) + response = self.fetch('/static/robots.txt', headers={ + 'If-Modified-Since': format_timestamp(stat.st_mtime + 1)}) + self.assertEqual(response.code, 304) + + def test_static_etag(self): + response = self.fetch('/static/robots.txt') + self.assertEqual(utf8(response.headers.get("Etag")), + b'"' + self.robots_txt_hash + b'"') + + def test_static_with_range(self): + response = self.fetch('/static/robots.txt', headers={ + 'Range': 'bytes=0-9'}) + self.assertEqual(response.code, 206) + self.assertEqual(response.body, b"User-agent") + self.assertEqual(utf8(response.headers.get("Etag")), + b'"' + self.robots_txt_hash + b'"') + self.assertEqual(response.headers.get("Content-Length"), "10") + self.assertEqual(response.headers.get("Content-Range"), + "0-9/26") + + def test_static_with_range_end_edge(self): + response = self.fetch('/static/robots.txt', headers={ + 'Range': 'bytes=22-'}) + self.assertEqual(response.body, b": /\n") + self.assertEqual(response.headers.get("Content-Length"), "4") + self.assertEqual(response.headers.get("Content-Range"), + "22-25/26") + + def test_static_with_range_neg_end(self): + response = self.fetch('/static/robots.txt', headers={ + 'Range': 'bytes=-4'}) + self.assertEqual(response.body, b": /\n") + self.assertEqual(response.headers.get("Content-Length"), "4") + self.assertEqual(response.headers.get("Content-Range"), + "22-25/26") + + def test_static_invalid_range(self): + response = self.fetch('/static/robots.txt', headers={ + 'Range': 'asdf'}) + self.assertEqual(response.code, 416) + + def test_static_head(self): + response = self.fetch('/static/robots.txt', method='HEAD') + self.assertEqual(response.code, 200) + # No body was returned, but we did get the right content length. + self.assertEqual(response.body, b'') + self.assertEqual(response.headers['Content-Length'], '26') + self.assertEqual(utf8(response.headers['Etag']), + b'"' + self.robots_txt_hash + b'"') + + def test_static_head_range(self): + response = self.fetch('/static/robots.txt', method='HEAD', + headers={'Range': 'bytes=1-4'}) + self.assertEqual(response.code, 206) + self.assertEqual(response.body, b'') + self.assertEqual(response.headers['Content-Length'], '4') + self.assertEqual(utf8(response.headers['Etag']), + b'"' + self.robots_txt_hash + b'"') + + def test_static_range_if_none_match(self): + response = self.fetch('/static/robots.txt', headers={ + 'Range': 'bytes=1-4', + 'If-None-Match': b'"' + self.robots_txt_hash + b'"'}) + self.assertEqual(response.code, 304) + self.assertEqual(response.body, b'') + self.assertTrue('Content-Length' not in response.headers) + self.assertEqual(utf8(response.headers['Etag']), + b'"' + self.robots_txt_hash + b'"') + + def test_static_404(self): + response = self.fetch('/static/blarg') + self.assertEqual(response.code, 404) + + +@wsgi_safe +class StaticDefaultFilenameTest(WebTestCase): + def get_app_kwargs(self): + return dict(static_path=os.path.join(os.path.dirname(__file__), + 'static'), + static_handler_args=dict(default_filename='index.html')) + + def get_handlers(self): + return [] + + def test_static_default_filename(self): + response = self.fetch('/static/dir/', follow_redirects=False) + self.assertEqual(response.code, 200) + self.assertEqual(b'this is the index\n', response.body) + + def test_static_default_redirect(self): + response = self.fetch('/static/dir', follow_redirects=False) + self.assertEqual(response.code, 301) + self.assertTrue(response.headers['Location'].endswith('/static/dir/')) + + +@wsgi_safe +class CustomStaticFileTest(WebTestCase): + def get_handlers(self): + class MyStaticFileHandler(StaticFileHandler): @classmethod - def make_static_url(cls, settings, path): - version = cls.get_version(settings, path) + def make_static_url(cls, settings, path, include_version=True): + version_hash = cls.get_version(settings, path) extension_index = path.rindex('.') before_version = path[:extension_index] after_version = path[(extension_index + 1):] - return '/static/%s.%s.%s' % (before_version, version, - return '/static/%s.%s.%s' % (before_version, 42, after_version) ++ return '/static/%s.%s.%s' % (before_version, version_hash, + after_version) - @classmethod - def parse_url_path(cls, url_path): + def parse_url_path(self, url_path): extension_index = url_path.rindex('.') version_index = url_path.rindex('.', 0, extension_index) return '%s%s' % (url_path[:version_index], diff --cc tornado/web.py index 998380753,e0ebdcb46..765316912 --- a/tornado/web.py +++ b/tornado/web.py @@@ -987,17 -886,18 +987,18 @@@ class RequestHandler(object) return '' - def static_url(self, path, include_host=None): + def static_url(self, path, include_host=None, include_version=True): """Returns a static URL for the given relative static file path. - This method requires you set the 'static_path' setting in your + This method requires you set the ``static_path`` setting in your application (which specifies the root directory of your static files). - We append ?v= to the returned URL, which makes our + We append ``?v=`` to the returned URL, which makes our static file handler set an infinite expiration header on the returned content. The signature is based on the content of the - file. + file. This behavior can be avoided in case the ``include_version`` + is set to False, i.e ?v= is not appended. By default this method returns URLs relative to the current host, but if ``include_host`` is true the URL returned will be @@@ -1969,26 -1560,21 +1971,34 @@@ class StaticFileHandler(RequestHandler) ``settings`` is the `Application.settings` dictionary. ``path`` is the static path being requested. The url returned should be relative to the current host. + + ``include_version`` determines whether the generated URL should + include the query string containing the version hash of the + file corresponding to the given ``path``. """ - static_url_prefix = settings.get('static_url_prefix', '/static/') + url = settings.get('static_url_prefix', '/static/') + path + if not include_version: + return url + version_hash = cls.get_version(settings, path) - if version_hash: - return static_url_prefix + path + "?v=" + version_hash - return static_url_prefix + path + if not version_hash: + return url + + return '%s?v=%s' % (url, version_hash) + def parse_url_path(self, url_path): + """Converts a static URL path into a filesystem path. + + ``url_path`` is the path component of the URL with + ``static_url_prefix`` removed. The return value should be + filesystem path relative to ``static_path``. + + This is the inverse of `make_static_url`. + """ + if os.path.sep != "/": + url_path = url_path.replace("/", os.path.sep) + return url_path + @classmethod def get_version(cls, settings, path): """Generate the version string to be used in static URLs.