Merge ~bryce/ubuntu/+source/convoy:convoy-fix-x-focal into ubuntu/+source/convoy:debian/sid

Proposed by Bryce Harrington
Status: Rejected
Rejected by: Bryce Harrington
Proposed branch: ~bryce/ubuntu/+source/convoy:convoy-fix-x-focal
Merge into: ubuntu/+source/convoy:debian/sid
Diff against target: 1469 lines (+553/-297)
15 files modified
Makefile (+30/-10)
convoy/combo.py (+87/-32)
convoy/meta.py (+24/-25)
convoy/tests/__init__.py (+76/-0)
convoy/tests/test_combo.py (+165/-101)
convoy/tests/test_meta.py (+101/-79)
debian/changelog (+22/-0)
debian/control (+14/-11)
debian/python-convoy.install (+1/-0)
debian/python3-convoy.install (+1/-0)
debian/rules (+13/-2)
debian/tests/control (+1/-1)
debian/tests/testsuite (+1/-1)
setup.py (+12/-35)
tox.ini (+5/-0)
Reviewer Review Type Date Requested Status
Christian Ehrhardt  (community) Needs Fixing
Canonical Server Core Reviewers Pending
Review via email: mp+377346@code.launchpad.net

Description of the change

Drops py2 support for convoy, in order to resolve proposed migration block.

python3-django-maas still depends on the py3 binary package, but nothing depends on the py2 binary package.

The autopkgtest for this package ran only against python2 and required python-mocker, which has no python3 equivalent. However, running the tests manually I found it worked fine with python3 and didn't complain about python-mocker.

The package's compat level is old (as are a few other aspects of the package, like use of bzr, etc.) due to that it's not been actively maintained in quite some time. I opted to keep changes to a minimum though, and only changed what was required to get it building without py2. If you think more extensive updating is warranted let me know.

A PPA with the package is here:
    https://launchpad.net/~bryce/+archive/ubuntu/convoy-fix-x

To post a comment you must log in.
Revision history for this message
Bryce Harrington (bryce) wrote :

Btw, launchpad seems to be including the changes from 0.2.1+bzr39-1 for some reason. Here is a more minimal debdiff:

http://paste.ubuntu.com/p/r2Qff7zzwb/

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

For the diff I think the merge target would need to be focal-devel instead of debian/sid.
But it can be reviewed as-is.

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Ack that there is no reverse depends on python-convoy
Ack to the general changes
Nit Picks:
- changelog doesn't mention e.g. d/rules changes for --buildsystem=pybuild
- maybe split the long paragraph in sub-bullets prefixed by the files the chagne takes place

All of the above is style and you could even say "nah I like mine better".
+1 to the actual functional changes.

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

I ran the updated packages .dsc through autopkgtest against the PPA
The result seem to need some test/polishing

$ sudo ~/work/autopkgtest/autopkgtest/runner/autopkgtest --no-built-binaries --apt-upgrade --setup-commands="add-apt-repository ppa:bryce/convoy-fix-x; apt update; apt -y upgrade" --shell-fail convoy_0.2.1+bzr39-1ubuntu1~focal2.dsc -- qemu --qemu-options='-cpu host' --ram-size=2048 --cpus 2 ~/work/autopkgtest-focal-amd64.img
[...]
autopkgtest [07:46:57]: test testsuite: [-----------------------
+ python3 -m unittest discover convoy/tests
E.....................
======================================================================
ERROR: test_combo (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_combo
Traceback (most recent call last):
  File "/usr/lib/python3.7/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "/usr/lib/python3.7/unittest/loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "/tmp/autopkgtest.UK17Pb/build.Oap/src/convoy/tests/test_combo.py", line 22, in <module>
    from webtest import TestApp
ModuleNotFoundError: No module named 'webtest'

----------------------------------------------------------------------
Ran 22 tests in 0.032s

FAILED (errors=1)

I'd not want to upload as-is - did you run the tests on your side and they were ok?

review: Needs Fixing
Revision history for this message
Bryce Harrington (bryce) wrote :

Unmerged commits

0e08aeb... by Bryce Harrington

  * Drop python2 package in favor of python3.
    - Change autopkgtest to use python3 dependencies. Drop dependence on
      python-mocker since there's no py3 version of it; the testsuite
      doesn't appear to need it when run under python3.

6fbd26b... by Bryce Harrington

changelog

b614438... by Matthias Klose

Import patches-unapplied version 0.2.1+bzr39-1build1 to ubuntu/focal-proposed

Imported using git-ubuntu import.

Changelog parent: da3a596fb204f35ce1fe367b1ebdaf6ce4e81a23

New changelog entries:
  * No-change rebuild to generate dependencies on python2.

da3a596... by Andres Rodriguez

Import patches-unapplied version 0.2.1+bzr39-1 to ubuntu/xenial-proposed

Imported using git-ubuntu import.

Changelog parent: b9b26230def71a3b0ad1c83a2a3c624cbec0edc2

New changelog entries:
  * New upstream snapshot.
  * Build with python3.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index e9a4e00..b322f5b 100644
3--- a/Makefile
4+++ b/Makefile
5@@ -1,13 +1,33 @@
6-PY=$(shell which python)
7+PYTHON := python
8
9-.PHONY: test
10-test:
11- $(PY) setup.py test
12+# ---
13
14-.PHONY: build
15-build:
16- $(PY) setup.py sdist
17+dist: bin/python setup.py README
18+ bin/python setup.py egg_info sdist
19
20-.PHONY: upload
21-upload:
22- $(PY) setup.py sdist upload
23+upload: bin/python setup.py README
24+ bin/python setup.py egg_info sdist upload
25+
26+test: bin/tox
27+ @bin/tox
28+
29+clean:
30+ $(RM) -r bin build dist include lib local TAGS tags
31+ find . -name '*.py[co]' -print0 | xargs -r0 $(RM)
32+ find . -name '__pycache__' -print0 | xargs -r0 $(RM) -r
33+ find . -name '*.egg' -print0 | xargs -r0 $(RM) -r
34+ find . -name '*.egg-info' -print0 | xargs -r0 $(RM) -r
35+ find . -name '*~' -print0 | xargs -r0 $(RM)
36+ $(RM) -r .eggs .tox _trial_temp
37+
38+# ---
39+
40+bin/tox: bin/pip
41+ bin/pip install --quiet --ignore-installed tox
42+
43+bin/python bin/pip:
44+ virtualenv --python=$(PYTHON) --quiet $(CURDIR)
45+
46+# ---
47+
48+.PHONY: dist upload test clean
49diff --git a/convoy/combo.py b/convoy/combo.py
50index da27dd3..664f4d9 100644
51--- a/convoy/combo.py
52+++ b/convoy/combo.py
53@@ -1,5 +1,5 @@
54 # Convoy is a WSGI app for loading multiple files in the same request.
55-# Copyright (C) 2010-2012 Canonical, Ltd.
56+# Copyright (C) 2011-2015 Canonical, Ltd.
57 #
58 # This program is free software: you can redistribute it and/or modify
59 # it under the terms of the GNU Affero General Public License as
60@@ -15,15 +15,25 @@
61 # along with this program. If not, see <http://www.gnu.org/licenses/>.
62
63
64-import cgi
65-import os
66+import logging
67+import os.path
68 import re
69-import urlparse
70+import sys
71+
72+try:
73+ from urllib.parse import parse_qsl
74+except ImportError:
75+ from cgi import parse_qsl
76+
77+try:
78+ import urllib.parse as urlparse
79+except ImportError:
80+ import urlparse
81
82
83 CHUNK_SIZE = 2 << 12
84-URL_RE = re.compile("url\([ \"\']*([^ \"\']+)[ \"\']*\)")
85-URL_PARSE = re.compile("/([^/]*).*?$")
86+URL_RE = re.compile(b"""url[(][ "']*([^ "')]+)[ "']*[)]""")
87+URL_PARSE = re.compile(b"/([^/]*).*?$")
88
89
90 def relative_path(from_file, to_file):
91@@ -50,10 +60,27 @@ def parse_qs(query):
92
93 Returns the list of arguments in the original order.
94 """
95- params = cgi.parse_qsl(query, keep_blank_values=True)
96+ params = parse_qsl(query, keep_blank_values=True)
97 return tuple([param for param, value in params])
98
99
100+class InvalidFileError(Exception):
101+ """Exception raised for bogus filenames."""
102+
103+
104+def validate_files(fnames, root):
105+ """Validate that the given filenames are sane.
106+
107+ Filenames must be within the root directory and actual files.
108+
109+ @raises InvalidFileError for any bogus files.
110+ """
111+ for fname in fnames:
112+ full = os.path.abspath(os.path.join(root, fname))
113+ if not (full.startswith(root) and os.path.isfile(full)):
114+ raise InvalidFileError(fname)
115+
116+
117 def combine_files(fnames, root, resource_prefix="", rewrite_urls=True):
118 """Combine many files into one.
119
120@@ -61,59 +88,77 @@ def combine_files(fnames, root, resource_prefix="", rewrite_urls=True):
121 files. The relative path to root will be included as a comment
122 between each file.
123 """
124+ # Used when encoding local file-system paths.
125+ fsenc = sys.getfilesystemencoding()
126+
127+ if not isinstance(root, bytes):
128+ # This is a local file-system path.
129+ root = root.encode(fsenc)
130+ if not isinstance(resource_prefix, bytes):
131+ # This is a URL component, so assume only ASCII is okay.
132+ resource_prefix = resource_prefix.encode("ascii")
133+
134+ resource_prefix = resource_prefix.rstrip(b"/")
135
136- resource_prefix = resource_prefix.rstrip("/")
137 for fname in fnames:
138+ if not isinstance(fname, bytes):
139+ # This is a local file-system path.
140+ fname = fname.encode(fsenc)
141+
142 file_ext = os.path.splitext(fname)[-1]
143 basename = os.path.basename(fname)
144 full = os.path.abspath(os.path.join(root, fname))
145- yield "/* " + fname + " */\n"
146- if not full.startswith(root) or not os.path.exists(full):
147- yield "/* [missing] */\n"
148- else:
149- with open(full, "r") as f:
150- if file_ext == ".css" and rewrite_urls:
151+ if (full.startswith(root) and os.path.isfile(full)):
152+ yield b"/* " + fname + b" */\n"
153+ with open(full, "rb") as f:
154+ if file_ext == b".css" and rewrite_urls:
155 file_content = f.read()
156 src_dir = os.path.dirname(full)
157 relative_parts = relative_path(
158 os.path.join(root, basename), src_dir).split(
159- os.path.sep)
160+ os.path.sep.encode(fsenc))
161
162 def fix_relative_url(match):
163 url = match.group(1)
164 # Don't modify absolute URLs or 'data:' urls.
165- if (url.startswith("http") or
166- url.startswith("/") or
167- url.startswith("data:")):
168+ if (url.startswith(b"http") or
169+ url.startswith(b"/") or
170+ url.startswith(b"data:")):
171 return match.group(0)
172- parts = relative_parts + url.split("/")
173+ parts = relative_parts + url.split(b"/")
174 result = []
175 for part in parts:
176- if part == ".." and result and result[-1] != "..":
177+ if part == b".." and result[-1:] != [b".."]:
178 result.pop(-1)
179 continue
180 result.append(part)
181- return "url(%s)" % "/".join(
182- filter(None, [resource_prefix] + result))
183+ return b"url(x)".replace(b"x", b"/".join(
184+ part for part in [resource_prefix] + result
185+ if part))
186+
187 file_content = URL_RE.sub(fix_relative_url, file_content)
188 yield file_content
189- yield "\n"
190+ yield b"\n"
191 else:
192 while True:
193 chunk = f.read(CHUNK_SIZE)
194 if not chunk:
195- yield "\n"
196+ yield b"\n"
197 break
198 yield chunk
199
200
201-def combo_app(root, resource_prefix="", rewrite_urls=True):
202+def combo_app(
203+ root, resource_prefix="", rewrite_urls=True, additional_headers=None):
204 """A simple YUI Combo Service WSGI app.
205
206 Serves any files under C{root}, setting an appropriate
207 C{Content-Type} header.
208+ Additional headers can be provided as a list of tuples to allow
209+ for generic extensions, but their correctness won't be verified.
210 """
211 root = os.path.abspath(root)
212+ log = logging.getLogger(__name__)
213
214 def app(environ, start_response, root=root):
215 # Path hint uses the rest of the url to map to files on disk based off
216@@ -127,8 +172,9 @@ def combo_app(root, resource_prefix="", rewrite_urls=True):
217 elif fnames[0].endswith(".css"):
218 content_type = "text/css"
219 else:
220+ log.info('No files in querystring.')
221 start_response("404 Not Found", [("Content-Type", content_type)])
222- return ("Not Found",)
223+ return (b"Not Found",)
224
225 # Take any prefix in the url route into consideration for the root to
226 # find files.
227@@ -136,12 +182,21 @@ def combo_app(root, resource_prefix="", rewrite_urls=True):
228 # Enforce that the updated root is not outside the original root.
229 absroot = os.path.abspath(updated_root)
230 if not absroot.startswith(os.path.abspath(root)):
231+ log.info('Updated root is outside of original root.')
232 start_response("400 Bad Request", [("Content-Type", content_type)])
233- return ("Bad Request",)
234+ return (b"Bad Request",)
235+ headers = [("Content-Type", content_type),
236+ ("X-Content-Type-Options", "nosniff")]
237+ if additional_headers is not None:
238+ headers.extend(additional_headers)
239+ try:
240+ validate_files(fnames, updated_root)
241+ except InvalidFileError as if_error:
242+ log.info('No such file: %s' % if_error.args[0])
243+ start_response("400 Bad Request", headers)
244+ return (b"Bad Request",)
245 else:
246- start_response("200 OK", [("Content-Type", content_type),
247- ("X-Content-Type-Options", "nosniff")])
248-
249- return combine_files(fnames, updated_root, resource_prefix,
250- rewrite_urls=rewrite_urls)
251+ start_response("200 OK", headers)
252+ return combine_files(fnames, updated_root, resource_prefix,
253+ rewrite_urls=rewrite_urls)
254 return app
255diff --git a/convoy/meta.py b/convoy/meta.py
256index ed9f3ed..d38c2c7 100644
257--- a/convoy/meta.py
258+++ b/convoy/meta.py
259@@ -1,5 +1,5 @@
260 # Convoy is a WSGI app for loading multiple files in the same request.
261-# Copyright (C) 2010-2012 Canonical, Ltd.
262+# Copyright (C) 2011-2015 Canonical, Ltd.
263 #
264 # This program is free software: you can redistribute it and/or modify
265 # it under the terms of the GNU Affero General Public License as
266@@ -35,6 +35,7 @@ DETAILS_REPLACE = r'"\1":'
267
268 LITERAL_RE = re.compile("([\[ ]+)\"([\w\.\+-]+)\"([^:])")
269 NAME_RE = re.compile("[\.\+-]")
270+COMMENT_RE = re.compile("\/\/.*")
271
272
273 def extract_metadata(src):
274@@ -44,6 +45,7 @@ def extract_metadata(src):
275 name, details, ignore = entry.groups()
276 details = details.replace('\'', '"')
277 details = DETAILS_FIND.sub(DETAILS_REPLACE, details)
278+ details = COMMENT_RE.sub('', details)
279 details = json.loads(details)
280 details["name"] = name
281 metadata.append(details)
282@@ -100,7 +102,8 @@ class Builder:
283
284 for fname in fnames:
285 self.log("Extracting metadata from '%s'" % fname)
286- data = open(fname, "r").read()
287+ with open(fname, "r") as fd:
288+ data = fd.read()
289 meta = extract_metadata(data)
290 prefix = ""
291 if self.prefix and not prefix.endswith("/"):
292@@ -167,13 +170,12 @@ class Builder:
293 return match.group(1) + literals_map[literal] + match.group(3)
294 return match.group(0)
295
296-
297 linebreak = ",\n "
298 variables_decl = "var SKIN_SAM_PREFIX = 'skin-sam-'" + linebreak
299 if self.prefix:
300- variables_decl += "PREFIX = '%s'%s" % (self.prefix, linebreak)
301+ variables_decl += "PREFIX = '%s'%s" % (self.prefix, linebreak)
302 extra_variables = []
303- for literal, variable in sorted(literals_map.iteritems()):
304+ for literal, variable in sorted(literals_map.items()):
305 extra_variable = "%s = %s" % (
306 variable, ('"%s"' % literal).replace(
307 '"skin-sam-', 'SKIN_SAM_PREFIX + "'))
308@@ -202,11 +204,11 @@ class Builder:
309 "after_list"])
310
311 modules_decl = []
312- for module_name, module_info in sorted(modules.iteritems()):
313+ for module_name, module_info in sorted(modules.items()):
314 module_decl = [
315 "modules[%s] = module_info = {}" %
316 NAME_RE.sub("_", module_name).upper()]
317- for key, value in sorted(module_info.iteritems()):
318+ for key, value in sorted(module_info.items()):
319 if value is True or value is False:
320 value = str(value).upper()
321 elif value in ("css", "js"):
322@@ -217,8 +219,8 @@ class Builder:
323 # It's easy to think that doing 'CORE_CSS + %(values)s'
324 # instead of using concat would work, but it doesn't;
325 # you'll end up with a string instead of a list.
326- module_decl.append("after_list = CORE_CSS");
327- module_decl.append("after_list.concat(%s)" % value);
328+ module_decl.append("after_list = CORE_CSS")
329+ module_decl.append("after_list.concat(%s)" % value)
330 value = "after_list"
331 if key == "path":
332 value = value.replace(
333@@ -229,8 +231,7 @@ class Builder:
334
335 modules_decl = ";\n\n ".join(modules_decl)
336
337- module_config = open(out, "w")
338- try:
339+ with open(out, "w") as module_config:
340 module_config.write("""var %s = (function(){
341 %s;
342
343@@ -238,8 +239,6 @@ class Builder:
344
345 return modules;
346 })();""" % (var_name, variables_decl, modules_decl))
347- finally:
348- module_config.close()
349
350 def generate_skin_modules(self, entry, metadata, root):
351 # Generate a skin module definition, since YUI assumes that
352@@ -369,15 +368,15 @@ def get_options():
353
354
355 def main():
356- options, args = get_options()
357- if options.src_dir is None:
358- options.src_dir = os.getcwd()
359- Builder(
360- name=options.name,
361- src_dir=os.path.abspath(options.src_dir),
362- output=options.output,
363- prefix=options.prefix,
364- exclude_regex=options.exclude_regex,
365- ext=options.ext,
366- include_skin=not options.no_skin,
367- ).do_build()
368+ options, args = get_options()
369+ if options.src_dir is None:
370+ options.src_dir = os.getcwd()
371+ Builder(
372+ name=options.name,
373+ src_dir=os.path.abspath(options.src_dir),
374+ output=options.output,
375+ prefix=options.prefix,
376+ exclude_regex=options.exclude_regex,
377+ ext=options.ext,
378+ include_skin=not options.no_skin,
379+ ).do_build()
380diff --git a/convoy/tests/__init__.py b/convoy/tests/__init__.py
381index e69de29..aef8cae 100644
382--- a/convoy/tests/__init__.py
383+++ b/convoy/tests/__init__.py
384@@ -0,0 +1,76 @@
385+# Convoy is a WSGI app for loading multiple files in the same request.
386+# Copyright (C) 2011-2015 Canonical, Ltd.
387+#
388+# This program is free software: you can redistribute it and/or modify
389+# it under the terms of the GNU Affero General Public License as
390+# published by the Free Software Foundation, either version 3 of the
391+# License, or (at your option) any later version.
392+#
393+# This program is distributed in the hope that it will be useful,
394+# but WITHOUT ANY WARRANTY; without even the implied warranty of
395+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
396+# GNU Affero General Public License for more details.
397+#
398+# You should have received a copy of the GNU Affero General Public License
399+# along with this program. If not, see <http://www.gnu.org/licenses/>.
400+
401+import os
402+import os.path
403+import shutil
404+import tempfile
405+import unittest
406+
407+__all__ = [
408+ "ConvoyTestCase",
409+]
410+
411+
412+class ConvoyTestCase(unittest.TestCase):
413+
414+ def makeDir(self, path=None):
415+ """Create a temporary directory.
416+
417+ This directory will be removed when the test completes.
418+
419+ :param path: An optional directory path to create. If not specified a
420+ new directory path will be chosen by `tempfile.mkdtemp`.
421+ :return: The path of the newly created directory.
422+ """
423+ if path is None:
424+ path = tempfile.mkdtemp()
425+ self.addCleanup(shutil.rmtree, path)
426+ return path
427+ else:
428+ os.makedirs(path)
429+ return path
430+
431+ def makeFile(self, content=None, basename=None, dirname=None, path=None):
432+ """Create a temporary file.
433+
434+ This file will be removed when the test completes.
435+
436+ :param content: Optional contents for the new file.
437+ :param basename: Optional base name for the new file.
438+ :param dirname: Optional directory in which to put the new file.
439+ :param path: Optional full path to the new file. Mutually exclusive
440+ with `basename` and `dirname`.
441+ :return: The path of the newly created file.
442+ """
443+ if path is not None:
444+ self.assertIsNone(basename)
445+ self.assertIsNone(dirname)
446+ self.addCleanup(os.unlink, path)
447+ elif basename is not None:
448+ if dirname is None:
449+ dirname = self.makeDir()
450+ path = os.path.join(dirname, basename)
451+ else:
452+ fileno, path = tempfile.mkstemp(dir=dirname)
453+ self.addCleanup(os.unlink, path)
454+ os.close(fileno)
455+
456+ if content is not None:
457+ with open(path, "w") as fd:
458+ fd.write(content)
459+
460+ return path
461diff --git a/convoy/tests/test_combo.py b/convoy/tests/test_combo.py
462index bb9d60d..c291510 100644
463--- a/convoy/tests/test_combo.py
464+++ b/convoy/tests/test_combo.py
465@@ -1,5 +1,5 @@
466 # Convoy is a WSGI app for loading multiple files in the same request.
467-# Copyright (C) 2010-2012 Canonical, Ltd.
468+# Copyright (C) 2011-2015 Canonical, Ltd.
469 #
470 # This program is free software: you can redistribute it and/or modify
471 # it under the terms of the GNU Affero General Public License as
472@@ -18,17 +18,18 @@
473 import os
474 import difflib
475 import textwrap
476-from unittest import defaultTestLoader
477
478-import mocker
479-from paste.fixture import TestApp
480+from webtest import TestApp
481
482 from convoy.combo import combo_app
483 from convoy.combo import combine_files
484 from convoy.combo import parse_url
485+from convoy.combo import validate_files
486+from convoy.combo import InvalidFileError
487+from convoy.tests import ConvoyTestCase
488
489
490-class ComboTestBase(object):
491+class ComboTestBase(ConvoyTestCase):
492
493 def makeSampleFile(self, root, fname, content):
494 content = textwrap.dedent(content).strip()
495@@ -38,19 +39,23 @@ class ComboTestBase(object):
496 os.makedirs(parent)
497 return self.makeFile(content=content, path=full)
498
499- def assertTextEquals(self, expected, got):
500+ def assertTextEquals(self, expected, observed):
501+ if isinstance(expected, bytes):
502+ expected = expected.decode("utf8")
503+ if isinstance(observed, bytes):
504+ observed = observed.decode("utf8")
505 expected = textwrap.dedent(expected).strip()
506- got = textwrap.dedent(got).strip()
507+ observed = textwrap.dedent(observed).strip()
508 diff = difflib.unified_diff(expected.splitlines(),
509- got.splitlines(), lineterm="")
510- self.assertEquals(expected, got, "\n" + "\n".join(diff))
511+ observed.splitlines(), lineterm="")
512+ self.assertEqual(expected, observed, "\n" + "\n".join(diff))
513
514
515-class ComboTest(ComboTestBase, mocker.MockerTestCase):
516+class ComboTest(ComboTestBase):
517
518 def test_parse_url_keeps_order(self):
519 """Parsing a combo loader URL returns an ordered list of filenames."""
520- self.assertEquals(
521+ self.assertEqual(
522 parse_url(("http://yui.yahooapis.com/combo?"
523 "3.0.0/build/yui/yui-min.js&"
524 "3.0.0/build/oop/oop-min.js&"
525@@ -82,12 +87,23 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
526 "** oop-min **",
527 "/* event-custom/event-custom-min.js */",
528 "** event-custom-min **"))
529- self.assertEquals(
530- "".join(combine_files(["yui/yui-min.js",
531- "oop/oop-min.js",
532- "event-custom/event-custom-min.js"],
533- root=test_dir)).strip(),
534- expected)
535+ self.assertEqual(
536+ b"".join(combine_files(
537+ ["yui/yui-min.js",
538+ "oop/oop-min.js",
539+ "event-custom/event-custom-min.js"],
540+ root=test_dir)).strip(),
541+ expected.encode("ascii"))
542+
543+ def test_combine_files_yields_only_byte_strings(self):
544+ """
545+ Combined files are always byte strings.
546+ """
547+ test_dir = self.makeDir()
548+ self.makeSampleFile(test_dir, "one.bin", "\x00\x01")
549+ self.makeSampleFile(test_dir, "two.txt", "foobar")
550+ for part in combine_files(["one.txt", "two.txt"], root=test_dir):
551+ self.assertIsInstance(part, bytes)
552
553 def test_combine_css_makes_relative_path(self):
554 """
555@@ -129,9 +145,10 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
556 }
557 """
558 self.assertTextEquals(
559- "".join(combine_files(["widget/assets/skins/sam/widget.css",
560- "editor/assets/skins/sam/editor.css"],
561- root=test_dir)).strip(),
562+ b"".join(combine_files(
563+ ["widget/assets/skins/sam/widget.css",
564+ "editor/assets/skins/sam/editor.css"],
565+ root=test_dir)).strip(),
566 expected)
567
568 def test_combine_css_leaves_absolute_urls_untouched(self):
569@@ -174,9 +191,10 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
570 }
571 """
572 self.assertTextEquals(
573- "".join(combine_files(["widget/assets/skins/sam/widget.css",
574- "editor/assets/skins/sam/editor.css"],
575- root=test_dir)).strip(),
576+ b"".join(combine_files(
577+ ["widget/assets/skins/sam/widget.css",
578+ "editor/assets/skins/sam/editor.css"],
579+ root=test_dir)).strip(),
580 expected)
581
582 def test_combine_css_leaves_data_uris_untouched(self):
583@@ -219,35 +237,10 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
584 }
585 """
586 self.assertTextEquals(
587- "".join(combine_files(["widget/assets/skins/sam/widget.css",
588- "editor/assets/skins/sam/editor.css"],
589- root=test_dir)).strip(),
590- expected)
591-
592- def test_missing_file_is_ignored(self):
593- """If a missing file is requested we should still combine others."""
594- test_dir = self.makeDir()
595-
596- self.makeSampleFile(
597- test_dir,
598- os.path.join("yui", "yui-min.js"),
599- "** yui-min **"),
600- self.makeSampleFile(
601- test_dir,
602- os.path.join("event-custom", "event-custom-min.js"),
603- "** event-custom-min **"),
604-
605- expected = "\n".join(("/* yui/yui-min.js */",
606- "** yui-min **",
607- "/* oop/oop-min.js */",
608- "/* [missing] */",
609- "/* event-custom/event-custom-min.js */",
610- "** event-custom-min **"))
611- self.assertEquals(
612- "".join(combine_files(["yui/yui-min.js",
613- "oop/oop-min.js",
614- "event-custom/event-custom-min.js"],
615- root=test_dir)).strip(),
616+ b"".join(combine_files(
617+ ["widget/assets/skins/sam/widget.css",
618+ "editor/assets/skins/sam/editor.css"],
619+ root=test_dir)).strip(),
620 expected)
621
622 def test_no_parent_hack(self):
623@@ -265,10 +258,9 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
624 hack = "../../oop/oop-min.js"
625 self.assertTrue(os.path.exists(os.path.join(root, hack)))
626
627- expected = "\n".join(("/* ../../oop/oop-min.js */",
628- "/* [missing] */"))
629- self.assertEquals(
630- "".join(combine_files([hack], root=root)).strip(),
631+ expected = b""
632+ self.assertEqual(
633+ b"".join(combine_files([hack], root=root)).strip(),
634 expected)
635
636 def test_no_absolute_path_hack(self):
637@@ -281,9 +273,9 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
638 hack = "/etc/passwd"
639 self.assertTrue(os.path.exists("/etc/passwd"))
640
641- expected = "/* /etc/passwd */\n/* [missing] */"
642- self.assertEquals(
643- "".join(combine_files([hack], root=test_dir)).strip(),
644+ expected = b""
645+ self.assertEqual(
646+ b"".join(combine_files([hack], root=test_dir)).strip(),
647 expected)
648
649 def test_no_traversing_out_of_root(self):
650@@ -295,17 +287,17 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
651
652 hack = ".."
653
654- expected = "/* .. */\n/* [missing] */"
655- self.assertEquals(
656- "".join(combine_files([hack], root=test_dir)).strip(),
657+ expected = b""
658+ self.assertEqual(
659+ b"".join(combine_files([hack], root=test_dir)).strip(),
660 expected)
661
662 # from /tmp/somedir we want to try to walk up to / and into
663 # etc/password
664 hack = "../../etc/password"
665- expected = "/* ../../etc/password */\n/* [missing] */"
666- self.assertEquals(
667- "".join(combine_files([hack], root=test_dir)).strip(),
668+ expected = b""
669+ self.assertEqual(
670+ b"".join(combine_files([hack], root=test_dir)).strip(),
671 expected)
672
673 def test_combine_css_adds_custom_prefix(self):
674@@ -348,38 +340,89 @@ class ComboTest(ComboTestBase, mocker.MockerTestCase):
675 }
676 """
677 self.assertTextEquals(
678- "".join(combine_files(["widget/assets/skins/sam/widget.css",
679- "editor/assets/skins/sam/editor.css"],
680- root=test_dir,
681- resource_prefix="/static/")).strip(),
682+ b"".join(combine_files(
683+ ["widget/assets/skins/sam/widget.css",
684+ "editor/assets/skins/sam/editor.css"],
685+ root=test_dir, resource_prefix="/static/")).strip(),
686+ expected)
687+
688+ def test_combine_css_adds_custom_prefix_minified(self):
689+ """
690+ The prefix is added to all url() declaration in CSS files, when the
691+ content of the .css file is minified.
692+ """
693+ test_dir = self.makeDir()
694+
695+ self.makeSampleFile(
696+ test_dir,
697+ os.path.join("path", "to", "widget.css"),
698+ ".foo{background:url(foo.png);}"
699+ ".bar{background:url(bar.png);}")
700+ expected = (
701+ "/* path/to/widget.css */\n"
702+ ".foo{background:url(/static/path/to/foo.png);}"
703+ ".bar{background:url(/static/path/to/bar.png);}")
704+ self.assertTextEquals(
705+ b"".join(combine_files(
706+ ["path/to/widget.css"], root=test_dir,
707+ resource_prefix="/static/")),
708 expected)
709
710 def test_rewrite_url_normalizes_parent_references(self):
711 """URL references in CSS files get normalized for parent dirs."""
712 test_dir = self.makeDir()
713- files = [
714- self.makeSampleFile(
715- test_dir,
716- os.path.join("yui", "base", "base.css"),
717- ".foo{background-image:url(../../img.png)}"),
718- ]
719+ self.makeSampleFile(
720+ test_dir, os.path.join("yui", "base", "base.css"),
721+ ".foo{background-image:url(../../img.png)}"),
722
723 expected = """
724 /* yui/base/base.css */
725 .foo{background-image:url(img.png)}
726 """
727 self.assertTextEquals(
728- "".join(combine_files(["yui/base/base.css"],
729- root=test_dir)).strip(),
730+ b"".join(combine_files(
731+ ["yui/base/base.css"],
732+ root=test_dir)).strip(),
733 expected)
734
735
736-class WSGIComboTest(ComboTestBase, mocker.MockerTestCase):
737+class ValidateFilesTest(ComboTestBase):
738+
739+ def test_missing_file_is_ignored(self):
740+ """If a missing file is requested we should still combine others."""
741+ test_dir = self.makeDir()
742+
743+ self.makeSampleFile(
744+ test_dir,
745+ os.path.join("yui", "yui-min.js"),
746+ "** yui-min **"),
747+ self.makeSampleFile(
748+ test_dir,
749+ os.path.join("event", "event-min.js"),
750+ "** event-min **"),
751+
752+ self.assertRaises(
753+ InvalidFileError, validate_files,
754+ ["yui/yui-min.js", "oop/oop-min.js", "event/event-min.js"],
755+ root=test_dir)
756+
757+
758+class WSGIComboTest(ComboTestBase):
759
760 def setUp(self):
761 self.root = self.makeDir()
762 self.app = TestApp(combo_app(self.root))
763
764+ def assertHeaders(self, observed, expected):
765+ """Ensure that the `expected` headers are present in `observed`.
766+
767+ :param observed: A mapping of HTTP response headers.
768+ :param expected: A list of HTTP response headers (as 2-tuples).
769+ """
770+ for name, value in expected:
771+ self.assertIn(name, observed)
772+ self.assertEqual(value, observed[name])
773+
774 def test_combo_app_sets_content_type_for_js(self):
775 """The WSGI App should set a proper Content-Type for Javascript."""
776 self.makeSampleFile(
777@@ -406,9 +449,11 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase):
778 ["yui/yui-min.js",
779 "oop/oop-min.js",
780 "event-custom/event-custom-min.js"]), status=200)
781- self.assertEquals(res.headers, [("Content-Type", "text/javascript"),
782- ("X-Content-Type-Options", "nosniff")])
783- self.assertEquals(res.body.strip(), expected)
784+ self.assertHeaders(res.headers, [
785+ ("Content-Type", "text/javascript"),
786+ ("X-Content-Type-Options", "nosniff"),
787+ ])
788+ self.assertEqual(res.body.strip(), expected.encode("ascii"))
789
790 def test_combo_app_sets_content_type_for_css(self):
791 """The WSGI App should set a proper Content-Type for CSS."""
792@@ -421,27 +466,39 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase):
793
794 res = self.app.get("/?" + "&".join(
795 ["widget/skin/sam/widget.css"]), status=200)
796- self.assertEquals(res.headers, [("Content-Type", "text/css"),
797- ("X-Content-Type-Options", "nosniff")])
798- self.assertEquals(res.body.strip(), expected)
799+ self.assertHeaders(res.headers, [
800+ ("Content-Type", "text/css"),
801+ ("X-Content-Type-Options", "nosniff"),
802+ ])
803+ self.assertEqual(res.body.strip(), expected.encode("ascii"))
804
805 def test_no_filename_gives_404(self):
806 """If no filename is included, a 404 should be returned."""
807 res = self.app.get("/", status=404)
808- self.assertEquals(res.headers, [("Content-Type", "text/plain")])
809- self.assertEquals(res.body, "Not Found")
810+ self.assertHeaders(res.headers, [("Content-Type", "text/plain")])
811+ self.assertEqual(res.body, b"Not Found")
812
813 def test_bogus_filenames_are_plain_text_and_not_sniffed(self):
814 """
815 Content-Type and X-Content-Type-Options headers set for
816 non-existent files.
817 """
818- res = self.app.get("/?" + "&".join(
819- ["foo/bar/baz",
820- "<html><script>alert(document.domain)</script></html>"]),
821- status=200)
822- self.assertEquals(res.headers, [("Content-Type", "text/plain"),
823- ("X-Content-Type-Options", "nosniff")])
824+ res = self.app.get(
825+ "/?" + "&".join([
826+ "foo/bar/baz",
827+ "<html><script>alert(document.domain)</script></html>",
828+ ]),
829+ status=400)
830+ self.assertHeaders(res.headers, [
831+ ("Content-Type", "text/plain"),
832+ ("X-Content-Type-Options", "nosniff"),
833+ ])
834+
835+ def test_js_comment_escape_hack(self):
836+ """Attacks to break out of the JS comment will get a 400 error."""
837+ res = self.app.get("/?%s" % "*/alert%28%27owned%27%29/*",
838+ status=400)
839+ self.assertEqual(res.body.strip(), b"Bad Request")
840
841 def test_combo_respects_path_hints(self):
842 """If I add path info into the combo url, convoy should use it."""
843@@ -476,13 +533,11 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase):
844 expected2 = "\n".join(("/* yui/yui-min.js */",
845 "/* yui-min-2 */"))
846
847- res = app.get("/%s/?%s" % (first_base,
848- "yui/yui-min.js"), status=200)
849- self.assertEquals(res.body.strip(), expected)
850+ res = app.get("/%s/?%s" % (first_base, "yui/yui-min.js"), status=200)
851+ self.assertEqual(res.body.strip(), expected.encode("ascii"))
852
853- res = app.get("/%s/?%s" % (second_base, "&".join(
854- ["yui/yui-min.js"])), status=200)
855- self.assertEquals(res.body.strip(), expected2)
856+ res = app.get("/%s/?%s" % (second_base, "yui/yui-min.js"), status=200)
857+ self.assertEqual(res.body.strip(), expected2.encode("ascii"))
858
859 def test_path_hint_cant_break_root(self):
860 """You should not be able to get outside of the root via path hint."""
861@@ -492,6 +547,15 @@ class WSGIComboTest(ComboTestBase, mocker.MockerTestCase):
862 # and ask for passwd, it's inside the new adjusted root
863 app.get("/../etc?yui-min&passwd", status=400)
864
865-
866-def test_suite():
867- return defaultTestLoader.loadTestsFromName(__name__)
868+ def test_cache_headers_set(self):
869+ app = TestApp(combo_app(
870+ self.root, additional_headers=[
871+ ('Cache-Control', 'max-age=3600, public'),
872+ ]))
873+ res = app.get("/?" + "&".join(
874+ ["widget/skin/sam/widget.css"]), status=400)
875+ self.assertHeaders(res.headers, [
876+ ("Content-Type", "text/css"),
877+ ("X-Content-Type-Options", "nosniff"),
878+ ('Cache-Control', 'max-age=3600, public'),
879+ ])
880diff --git a/convoy/tests/test_meta.py b/convoy/tests/test_meta.py
881index 813af6a..1d9de2d 100644
882--- a/convoy/tests/test_meta.py
883+++ b/convoy/tests/test_meta.py
884@@ -1,5 +1,5 @@
885 # Convoy is a WSGI app for loading multiple files in the same request.
886-# Copyright (C) 2010-2012 Canonical, Ltd.
887+# Copyright (C) 2011-2015 Canonical, Ltd.
888 #
889 # This program is free software: you can redistribute it and/or modify
890 # it under the terms of the GNU Affero General Public License as
891@@ -16,11 +16,9 @@
892
893
894 import os
895-from unittest import defaultTestLoader, TestCase
896-
897-import mocker
898
899 from convoy.meta import Builder, extract_metadata
900+from convoy.tests import ConvoyTestCase
901
902
903 class TestBuilder(Builder):
904@@ -30,12 +28,32 @@ class TestBuilder(Builder):
905 pass
906
907
908-class ExtractMetadataTest(TestCase):
909+class ExtractMetadataTest(ConvoyTestCase):
910+
911+ def test_extract_with_comments(self):
912+ """
913+ Extracting the metadata of a file containing comments
914+ in its requires block should still successfully
915+ extract its requirements.
916+ """
917+ metadata = extract_metadata("""\
918+ YUI.add('lazr.base', function(Y){
919+ Y.log('Hello World');
920+ }, '0.1', {
921+ "requires": [
922+ // this is a comment
923+ "node", "base" // so is this
924+ ]
925+ });
926+ """)
927+ self.assertEqual(len(metadata), 1)
928+ self.assertEqual(metadata[0]["name"], "lazr.base")
929+ self.assertEqual(metadata[0]["requires"], ["node", "base"])
930
931 def test_extract_single_module(self):
932 """
933 Extracting the metadata of a file containing a single module
934- should successfully extract it's requirements.
935+ should successfully extract its requirements.
936 """
937 metadata = extract_metadata("""\
938 YUI.add('lazr.base', function(Y){
939@@ -43,9 +61,9 @@ class ExtractMetadataTest(TestCase):
940 }, '0.1', {"requires": ["node", "base"]});
941 """)
942
943- self.assertEquals(len(metadata), 1)
944- self.assertEquals(metadata[0]["name"], "lazr.base")
945- self.assertEquals(metadata[0]["requires"], ["node", "base"])
946+ self.assertEqual(len(metadata), 1)
947+ self.assertEqual(metadata[0]["name"], "lazr.base")
948+ self.assertEqual(metadata[0]["requires"], ["node", "base"])
949
950 def test_extract_multiple_modules(self):
951 """
952@@ -61,13 +79,13 @@ class ExtractMetadataTest(TestCase):
953 }, '0.1', {"requires": ["node", "anim", "event"]});
954 """)
955
956- self.assertEquals(len(metadata), 2)
957+ self.assertEqual(len(metadata), 2)
958
959- self.assertEquals(metadata[0]["name"], "lazr.base")
960- self.assertEquals(metadata[0]["requires"], ["node", "base"])
961+ self.assertEqual(metadata[0]["name"], "lazr.base")
962+ self.assertEqual(metadata[0]["requires"], ["node", "base"])
963
964- self.assertEquals(metadata[1]["name"], "lazr.anim")
965- self.assertEquals(metadata[1]["requires"], ["node", "anim", "event"])
966+ self.assertEqual(metadata[1]["name"], "lazr.anim")
967+ self.assertEqual(metadata[1]["requires"], ["node", "anim", "event"])
968
969 def test_extract_multi_line(self):
970 """
971@@ -81,9 +99,9 @@ class ExtractMetadataTest(TestCase):
972 "event"]});
973 """)
974
975- self.assertEquals(len(metadata), 1)
976- self.assertEquals(metadata[0]["name"], "lazr.anim")
977- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
978+ self.assertEqual(len(metadata), 1)
979+ self.assertEqual(metadata[0]["name"], "lazr.anim")
980+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
981
982 def test_extract_odd_spacing(self):
983 """
984@@ -96,9 +114,9 @@ class ExtractMetadataTest(TestCase):
985 }, '0.1', {"requires": [ "node" ,"anim" , "event" ]});
986 """)
987
988- self.assertEquals(len(metadata), 1)
989- self.assertEquals(metadata[0]["name"], "lazr.anim")
990- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
991+ self.assertEqual(len(metadata), 1)
992+ self.assertEqual(metadata[0]["name"], "lazr.anim")
993+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
994
995 def test_extract_with_use(self):
996 """
997@@ -108,13 +126,14 @@ class ExtractMetadataTest(TestCase):
998 metadata = extract_metadata("""\
999 YUI.add('lazr.anim', function(Y){
1000 Y.log('Hello World');
1001- }, '0.1', {"use": ["dom"], "requires": [ "node" ,"anim" , "event" ]});
1002+ }, '0.1', {"use": ["dom"], "requires": [
1003+ "node" ,"anim" , "event" ]});
1004 """)
1005
1006- self.assertEquals(len(metadata), 1)
1007- self.assertEquals(metadata[0]["name"], "lazr.anim")
1008- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1009- self.assertEquals(metadata[0]["use"], ["dom"])
1010+ self.assertEqual(len(metadata), 1)
1011+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1012+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1013+ self.assertEqual(metadata[0]["use"], ["dom"])
1014
1015 def test_extract_with_requires_before_use(self):
1016 """
1017@@ -123,13 +142,14 @@ class ExtractMetadataTest(TestCase):
1018 metadata = extract_metadata("""\
1019 YUI.add('lazr.anim', function(Y){
1020 Y.log('Hello World');
1021- }, '0.1', {"requires": [ "node" ,"anim" , "event" ], "use": ["dom"]});
1022+ }, '0.1', {"requires": [ "node" ,"anim" , "event" ],
1023+ "use": ["dom"]});
1024 """)
1025
1026- self.assertEquals(len(metadata), 1)
1027- self.assertEquals(metadata[0]["name"], "lazr.anim")
1028- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1029- self.assertEquals(metadata[0]["use"], ["dom"])
1030+ self.assertEqual(len(metadata), 1)
1031+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1032+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1033+ self.assertEqual(metadata[0]["use"], ["dom"])
1034
1035 def test_extract_requires_in_new_line(self):
1036 """
1037@@ -148,10 +168,10 @@ class ExtractMetadataTest(TestCase):
1038 ]});
1039 """)
1040
1041- self.assertEquals(len(metadata), 1)
1042- self.assertEquals(metadata[0]["name"], "lazr.anim")
1043- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1044- self.assertEquals(metadata[0]["use"], ["dom"])
1045+ self.assertEqual(len(metadata), 1)
1046+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1047+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1048+ self.assertEqual(metadata[0]["use"], ["dom"])
1049
1050 def test_extract_metadata_not_metadata(self):
1051 """
1052@@ -159,11 +179,13 @@ class ExtractMetadataTest(TestCase):
1053 """
1054 metadata = extract_metadata("""
1055 YUI.add('test', function(Y) {
1056- this._dds[h[i]] = new YAHOO.util.DragDrop(this._handles[h[i]], this.get('id') + '-handle-' + h, { useShim: this.get('useShim') });
1057+ this._dds[h[i]] = new YAHOO.util.DragDrop(
1058+ this._handles[h[i]], this.get('id') + '-handle-' + h, {
1059+ useShim: this.get('useShim') });
1060 }, '1.0', {requires: ['dom']});
1061 """)
1062
1063- self.assertEquals(metadata[0]["requires"], ["dom"])
1064+ self.assertEqual(metadata[0]["requires"], ["dom"])
1065
1066 def test_extract_metadata_single_quotes(self):
1067 """
1068@@ -182,11 +204,10 @@ class ExtractMetadataTest(TestCase):
1069 ]});
1070 """)
1071
1072- self.assertEquals(len(metadata), 1)
1073- self.assertEquals(metadata[0]["name"], "lazr.anim")
1074- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1075- self.assertEquals(metadata[0]["use"], ["dom"])
1076-
1077+ self.assertEqual(len(metadata), 1)
1078+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1079+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1080+ self.assertEqual(metadata[0]["use"], ["dom"])
1081
1082 def test_extract_has_no_quotes(self):
1083 """
1084@@ -205,10 +226,10 @@ class ExtractMetadataTest(TestCase):
1085 ]});
1086 """)
1087
1088- self.assertEquals(len(metadata), 1)
1089- self.assertEquals(metadata[0]["name"], "lazr.anim")
1090- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1091- self.assertEquals(metadata[0]["use"], ["dom"])
1092+ self.assertEqual(len(metadata), 1)
1093+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1094+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1095+ self.assertEqual(metadata[0]["use"], ["dom"])
1096
1097 def test_extract_has_single_quotes(self):
1098 """
1099@@ -227,10 +248,10 @@ class ExtractMetadataTest(TestCase):
1100 ]});
1101 """)
1102
1103- self.assertEquals(len(metadata), 1)
1104- self.assertEquals(metadata[0]["name"], "lazr.anim")
1105- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1106- self.assertEquals(metadata[0]["use"], ["dom"])
1107+ self.assertEqual(len(metadata), 1)
1108+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1109+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1110+ self.assertEqual(metadata[0]["use"], ["dom"])
1111
1112 def test_extract_has_use(self):
1113 """
1114@@ -250,10 +271,10 @@ class ExtractMetadataTest(TestCase):
1115 ]});
1116 """)
1117
1118- self.assertEquals(len(metadata), 1)
1119- self.assertEquals(metadata[0]["name"], "lazr.anim")
1120- self.assertEquals(metadata[0]["requires"], ["node", "anim", "event"])
1121- self.assertEquals(metadata[0]["use"], ["cause", "dom"])
1122+ self.assertEqual(len(metadata), 1)
1123+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1124+ self.assertEqual(metadata[0]["requires"], ["node", "anim", "event"])
1125+ self.assertEqual(metadata[0]["use"], ["cause", "dom"])
1126
1127 def test_extract_with_use_and_no_requires(self):
1128 """
1129@@ -265,9 +286,9 @@ class ExtractMetadataTest(TestCase):
1130 }, '0.1', {"use": ["dom"]});
1131 """)
1132
1133- self.assertEquals(len(metadata), 1)
1134- self.assertEquals(metadata[0]["name"], "lazr.anim")
1135- self.assertEquals(metadata[0]["use"], ["dom"])
1136+ self.assertEqual(len(metadata), 1)
1137+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1138+ self.assertEqual(metadata[0]["use"], ["dom"])
1139
1140 def test_extract_with_all_options(self):
1141 """
1142@@ -286,15 +307,20 @@ class ExtractMetadataTest(TestCase):
1143 "after": ["lazr.base"]});
1144 """)
1145
1146- self.assertEquals(len(metadata), 1)
1147- self.assertEquals(metadata[0]["name"], "lazr.anim")
1148- self.assertEquals(metadata[0]["use"], ["dom"])
1149- self.assertEquals(metadata[0]["omit"], ["nono"])
1150- self.assertEquals(metadata[0]["optional"], ["meh"])
1151- self.assertEquals(metadata[0]["supersedes"], ["old-anim"])
1152- self.assertEquals(metadata[0]["after"], ["lazr.base"])
1153+ self.assertEqual(len(metadata), 1)
1154+ self.assertEqual(metadata[0]["name"], "lazr.anim")
1155+ self.assertEqual(metadata[0]["use"], ["dom"])
1156+ self.assertEqual(metadata[0]["omit"], ["nono"])
1157+ self.assertEqual(metadata[0]["optional"], ["meh"])
1158+ self.assertEqual(metadata[0]["supersedes"], ["old-anim"])
1159+ self.assertEqual(metadata[0]["after"], ["lazr.base"])
1160+
1161
1162-class GenerateMetadataTest(mocker.MockerTestCase):
1163+class GenerateMetadataTest(ConvoyTestCase):
1164+
1165+ def readFile(self, path):
1166+ with open(path, "r") as fd:
1167+ return fd.read()
1168
1169 def test_generate_metadata_simple(self):
1170 """
1171@@ -314,10 +340,10 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1172 output=output, exclude_regex="")
1173 builder.do_build()
1174
1175- got = open(output, "r").read()
1176+ got = self.readFile(output)
1177 prefix = got[:18]
1178 modules = "\n\n".join(got.split("\n\n")[1:-1])
1179- self.assertEquals(prefix, "var LAZR_CONFIG = ")
1180+ self.assertEqual(prefix, "var LAZR_CONFIG = ")
1181 self.assertTrue('[PATH] = "anim/anim-min.js"' in modules)
1182 self.assertFalse('[SKINNABLE] = FALSE' in modules)
1183 self.assertTrue('[TYPE] = JS' in modules)
1184@@ -350,10 +376,10 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1185 prefix="lazr")
1186 builder.do_build()
1187
1188- got = open(output, "r").read()
1189+ got = self.readFile(output)
1190 prefix = got[:18]
1191 modules = "\n\n".join(got.split("\n\n")[1:-1])
1192- self.assertEquals(prefix, "var LAZR_CONFIG = ")
1193+ self.assertEqual(prefix, "var LAZR_CONFIG = ")
1194 self.assertTrue(
1195 '[PATH] = PREFIX + '
1196 '"/anim/assets/skins/sam/anim-skin.css"' in modules)
1197@@ -392,10 +418,10 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1198 prefix=None)
1199 builder.do_build()
1200
1201- got = open(output, "r").read()
1202+ got = self.readFile(output)
1203 prefix = got[:18]
1204 modules = "\n\n".join(got.split("\n\n")[1:-1])
1205- self.assertEquals(prefix, "var LAZR_CONFIG = ")
1206+ self.assertEqual(prefix, "var LAZR_CONFIG = ")
1207 self.assertFalse(' PREFIX =' in got, got)
1208 self.assertTrue(
1209 '[PATH] = "anim/assets/skins/sam/anim-skin.css"' in modules)
1210@@ -429,10 +455,10 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1211 prefix=None)
1212 builder.do_build()
1213
1214- got = open(output, "r").read()
1215+ got = self.readFile(output)
1216 prefix = got[:18]
1217 modules = "\n\n".join(got.split("\n\n")[1:-1])
1218- self.assertEquals(prefix, "var LAZR_CONFIG = ")
1219+ self.assertEqual(prefix, "var LAZR_CONFIG = ")
1220 self.assertFalse(' PREFIX =' in got, got)
1221 self.assertTrue(
1222 '[PATH] = "anim-min.js"' in modules)
1223@@ -474,11 +500,11 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1224 prefix="lazr")
1225 builder.do_build()
1226
1227- got = open(output, "r").read()
1228+ got = self.readFile(output)
1229 prefix = got[:18]
1230 blocks = got.split("\n\n")
1231 modules = "\n\n".join(blocks[1:-1])
1232- self.assertEquals(prefix, "var LAZR_CONFIG = ")
1233+ self.assertEqual(prefix, "var LAZR_CONFIG = ")
1234
1235 self.assertTrue(
1236 '[PATH] = PREFIX + "/anim/assets/purty-anim-core.css"' in modules)
1237@@ -527,7 +553,7 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1238 prefix="lazr")
1239 builder.do_build()
1240
1241- got = open(output, "r").read()
1242+ got = self.readFile(output)
1243 self.assertIn('AFTER = "after",', got)
1244 self.assertIn('EXT = "ext",', got)
1245 self.assertIn('OPTIONAL = "optional",', got)
1246@@ -536,7 +562,3 @@ class GenerateMetadataTest(mocker.MockerTestCase):
1247 self.assertIn('SKINNABLE = "skinnable",', got)
1248 self.assertIn('SUPERSEDES = "supersedes",', got)
1249 self.assertIn('USE = "use",', got)
1250-
1251-
1252-def test_suite():
1253- return defaultTestLoader.loadTestsFromName(__name__)
1254diff --git a/debian/changelog b/debian/changelog
1255index 4f051e9..96c8e67 100644
1256--- a/debian/changelog
1257+++ b/debian/changelog
1258@@ -1,3 +1,25 @@
1259+convoy (0.2.1+bzr39-1ubuntu1) focal; urgency=medium
1260+
1261+ * Drop python2 package in favor of python3.
1262+ - Change autopkgtest to use python3 dependencies. Drop dependence on
1263+ python-mocker since there's no py3 version of it; the testsuite
1264+ doesn't appear to need it when run under python3.
1265+
1266+ -- Bryce Harrington <bryce@canonical.com> Wed, 08 Jan 2020 15:42:58 -0800
1267+
1268+convoy (0.2.1+bzr39-1build1) focal; urgency=medium
1269+
1270+ * No-change rebuild to generate dependencies on python2.
1271+
1272+ -- Matthias Klose <doko@ubuntu.com> Tue, 17 Dec 2019 12:31:52 +0000
1273+
1274+convoy (0.2.1+bzr39-1) xenial; urgency=medium
1275+
1276+ * New upstream snapshot.
1277+ * Build with python3.
1278+
1279+ -- Andres Rodriguez <andreserl@ubuntu.com> Thu, 12 Nov 2015 16:26:56 +0000
1280+
1281 convoy (0.2.1+bzr25-3) unstable; urgency=low
1282
1283 * Add missing autopkgtest dependencies on python-mocker and python-
1284diff --git a/debian/control b/debian/control
1285index fb59b84..cb70840 100644
1286--- a/debian/control
1287+++ b/debian/control
1288@@ -1,21 +1,24 @@
1289 Source: convoy
1290 Section: python
1291 Priority: extra
1292-Maintainer: Jelmer Vernooij <jelmer@debian.org>
1293-Build-Depends: debhelper (>= 7.0.50~),
1294- python-all (>= 2.6.6-3~),
1295- python-mocker,
1296- python-nose,
1297- python-paste,
1298- python-setuptools
1299-Standards-Version: 3.9.5
1300+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1301+XSBC-Original-Maintainer: Jelmer Vernooij <jelmer@debian.org>
1302+Build-Depends: debhelper (>= 9~),
1303+ dh-python,
1304+ python3-all,
1305+ python3-nose,
1306+ python3-paste,
1307+ python3-setuptools,
1308+ python3-webtest
1309+Standards-Version: 3.9.6
1310 Homepage: https://launchpad.net/convoy
1311+X-Python3-Version: >= 3.4
1312 XS-Testsuite: autopkgtest
1313
1314-Package: python-convoy
1315+Package: python3-convoy
1316 Architecture: all
1317-Depends: ${misc:Depends}, ${python:Depends}
1318-Description: WSGI app for loading multiple files in the same request
1319+Depends: ${misc:Depends}, ${python3:Depends}
1320+Description: WSGI app for loading multiple files in the same request (Python 3)
1321 Provides a WSGI application that can be hooked up to a
1322 generic WSGI container to create a combo loader server, for
1323 loading Javascript and CSS files combined into a single
1324diff --git a/debian/python-convoy.install b/debian/python-convoy.install
1325new file mode 100644
1326index 0000000..6a53a3b
1327--- /dev/null
1328+++ b/debian/python-convoy.install
1329@@ -0,0 +1 @@
1330+usr/lib/python2*/*-packages/*
1331diff --git a/debian/python3-convoy.install b/debian/python3-convoy.install
1332new file mode 100644
1333index 0000000..87e0b0a
1334--- /dev/null
1335+++ b/debian/python3-convoy.install
1336@@ -0,0 +1 @@
1337+usr/lib/python3/*-packages/*
1338diff --git a/debian/rules b/debian/rules
1339index 236638b..2d4f261 100755
1340--- a/debian/rules
1341+++ b/debian/rules
1342@@ -1,11 +1,22 @@
1343 #!/usr/bin/make -f
1344+export PYBUILD_NAME=convoy
1345+
1346+PYVERS := $(shell py3versions -r)
1347
1348 %:
1349- dh $@ --buildsystem=python_distutils --with python2
1350+ dh $@ --with python3 --buildsystem=pybuild
1351+
1352+override_dh_auto_install:
1353+ dh_auto_install
1354+ set -ex; for python in $(PYVERS); do \
1355+ $$python setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb; \
1356+ done
1357
1358 override_dh_auto_test:
1359 ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
1360- python$* setup.py test
1361+ set -ex; for python in $(PYVERS); do \
1362+ $$python setup.py test; \
1363+ done
1364 endif
1365
1366
1367diff --git a/debian/tests/control b/debian/tests/control
1368index ddb7fae..e2c7467 100644
1369--- a/debian/tests/control
1370+++ b/debian/tests/control
1371@@ -1,3 +1,3 @@
1372 Tests: testsuite
1373-Depends: python-convoy, python-mocker, python-paste
1374+Depends: python3-convoy, python3-paste
1375 Restrictions: allow-stderr
1376diff --git a/debian/tests/testsuite b/debian/tests/testsuite
1377index b1846e0..de41ca5 100644
1378--- a/debian/tests/testsuite
1379+++ b/debian/tests/testsuite
1380@@ -1,2 +1,2 @@
1381 #!/bin/sh -ex
1382-python -m unittest discover convoy/tests
1383+python3 -m unittest discover convoy/tests
1384diff --git a/setup.py b/setup.py
1385index c6c4be3..aec07a0 100644
1386--- a/setup.py
1387+++ b/setup.py
1388@@ -1,5 +1,5 @@
1389 # Convoy is a WSGI app for loading multiple files in the same request.
1390-# Copyright (C) 2010-2012 Canonical, Ltd.
1391+# Copyright (C) 2011-2015 Canonical, Ltd.
1392 #
1393 # This program is free software: you can redistribute it and/or modify
1394 # it under the terms of the GNU Affero General Public License as
1395@@ -14,43 +14,20 @@
1396 # You should have received a copy of the GNU Affero General Public License
1397 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1398
1399+from setuptools import setup
1400
1401-import os
1402-from distutils.core import setup
1403-
1404-# If setuptools is present, use it to find_packages(), and also
1405-# declare our dependency on epsilon.
1406-extra_setup_args = {}
1407-try:
1408- from setuptools import find_packages
1409- extra_setup_args = {
1410- 'install_requires': ['Paste'],
1411- 'tests_require': ['nose', 'mocker']
1412- }
1413-except ImportError:
1414- def find_packages(exclude=None):
1415- """
1416- Compatibility wrapper.
1417-
1418- Taken from storm setup.py.
1419- """
1420- packages = []
1421- for directory, subdirectories, files in os.walk("convoy"):
1422- if '__init__.py' in files:
1423- packages.append(directory.replace(os.sep, '.'))
1424- return packages
1425
1426 setup(
1427 name="convoy",
1428- version="0.2.4",
1429- description="A combo WSGI application for use with YUI",
1430+ version="0.4.4",
1431+ description="A combo-loader for Javascript and CSS.",
1432 author="Canonical Javascripters",
1433+ author_email="-",
1434 url="https://launchpad.net/convoy",
1435 license="AGPL",
1436- packages=find_packages(exclude=('convoy.tests',)),
1437- include_package_data=True,
1438- zip_safe=False,
1439- test_suite="nose.collector",
1440+ packages=['convoy'],
1441+ test_suite="convoy.tests",
1442+ tests_require=['nose', 'webtest'],
1443 classifiers=[
1444 "Development Status :: 5 - Production/Stable",
1445 "Environment :: Web Environment",
1446@@ -58,8 +35,8 @@ setup(
1447 "Intended Audience :: Information Technology",
1448 "License :: OSI Approved :: GNU Affero General Public License v3",
1449 "Programming Language :: Python",
1450+ "Programming Language :: Python :: 2",
1451+ "Programming Language :: Python :: 3",
1452 "Topic :: Internet :: WWW/HTTP",
1453- ],
1454- **extra_setup_args
1455- )
1456-
1457+ ],
1458+)
1459diff --git a/tox.ini b/tox.ini
1460new file mode 100644
1461index 0000000..e72377d
1462--- /dev/null
1463+++ b/tox.ini
1464@@ -0,0 +1,5 @@
1465+[tox]
1466+envlist = py27, py34, py35
1467+
1468+[testenv]
1469+commands = {envbindir}/python {toxinidir}/setup.py test

Subscribers

People subscribed via source and target branches