Merge lp:~michael.nelson/ubuntu-webcatalog/777871-pull-other-apps-from-apt-cache into lp:ubuntu-webcatalog

Proposed by Michael Nelson
Status: Merged
Approved by: Danny Tamez
Approved revision: 36
Merged at revision: 18
Proposed branch: lp:~michael.nelson/ubuntu-webcatalog/777871-pull-other-apps-from-apt-cache
Merge into: lp:ubuntu-webcatalog
Diff against target: 319 lines (+164/-38)
3 files modified
django_project/settings.py (+1/-0)
src/webcatalog/management/commands/import_app_install_data.py (+36/-14)
src/webcatalog/tests/test_commands.py (+127/-24)
To merge this branch: bzr merge lp:~michael.nelson/ubuntu-webcatalog/777871-pull-other-apps-from-apt-cache
Reviewer Review Type Date Requested Status
Danny Tamez (community) Approve
Review via email: mp+64977@code.launchpad.net

Commit message

Enables tests to run in under 3 seconds (rather than 35) by using a mock apt-cache, and uses lucid-compat cache update.

Description of the change

Overview
========
This branch doesn't do what the name implies, it was going to, but got sidetracked ;)

This branch updates the tests so that they run fast again (each of the command tests was doing an apt-cache update taking 3-4 seconds), for me 35secs in total, back so that they run in under 3 seconds. I've done this by adding a mock apt-cache for the tests, but also included a switch so you can run the tests against the real apt-cache if we need to.

It also updates the creation of the on-disk apt caches so that they are only created if they don't already exist, otherwise updated.

Details
=======

After chatting with mvo:

11:54 < noodles> mvo: hi! For the web catalog, we're currently doing: cache.update(sources_list=tmp_sources_list)
11:55 < noodles> to create a cache for whichever distroseries we need... but it seems that doesn't work on lucid:
11:55 < noodles> https://pastebin.canonical.com/48646/
11:55 < noodles> Is there a way to get the same effect on lucid?
11:55 < mvo> noodles: yes
11:56 < mvo> noodles: its best to use "cache = apt.Cache(rootdir=./path/to/temp-rootdir-with-etc-apt-sources.list/)
11:56 < mvo> noodles: you can keep this dir then, i.e. instead of making it a temp one make it permanent
11:57 < mvo> noodles: this will speed up the process of updates as it will use the binary cache in there and also use if-modified-since when doing the cache.update()
11:57 < mvo> noodles: and it will work on lucid :)
11:57 < noodles> Oh, we're already setting the rootdir, was the sources_list unecessary.. OK.
11:57 < noodles> Ah, sweet, I'll update it to do that then. Thanks!

I updated the on-disk cache to be more permanent (created if it doesn't exist, but otherwise just updated).

To test: follow the readme to bootstrap, then `fab test`

To post a comment you must log in.
30. By Michael Nelson

REFACTOR: Moved patch of setting to setup to avoid touching local dev caches.

31. By Michael Nelson

REFACTOR: cleaned up the setUp into a few helper methods.

32. By Michael Nelson

REFACTOR: enable running the tests against the real apt-cache, rather than a mock.

33. By Michael Nelson

Fixed incorrect mock assertion.

34. By Michael Nelson

Reverted changed requirement for dev version of mock.

35. By Michael Nelson

fixed typo in comment for use_mock_apt_cache attribute

36. By Michael Nelson

Extracted our code that creates the apt-cache so it reads better and also makes it easier to test whether the apt-cache-creating code was called.

Revision history for this message
Danny Tamez (zematynnad) wrote :

Much much faster. Works great.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/settings.py'
2--- django_project/settings.py 2011-05-06 15:03:41 +0000
3+++ django_project/settings.py 2011-06-17 14:39:31 +0000
4@@ -147,6 +147,7 @@
5
6 # Strictly for use in our dev environment:
7 SERVE_SITE_MEDIA = True
8+DISK_APT_CACHE_LOCATION = '/tmp/webcat_cache'
9
10 try:
11 from local_settings import *
12
13=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
14--- src/webcatalog/management/commands/import_app_install_data.py 2011-06-07 20:07:04 +0000
15+++ src/webcatalog/management/commands/import_app_install_data.py 2011-06-17 14:39:31 +0000
16@@ -32,6 +32,7 @@
17 import apt
18 from apt.cache import FetchFailedException
19 from apt_inst import DebFile
20+from django.conf import settings
21 from django.core.files.images import ImageFile
22 from django.forms.models import construct_instance
23 from django.core.management.base import LabelCommand
24@@ -52,21 +53,39 @@
25 def __init__(self, distroseries):
26 self.distroseries = distroseries
27
28+ def create_apt_cache(self, series_cache_location):
29+ os.makedirs(os.path.join(series_cache_location, 'etc', 'apt'))
30+ sources_list = os.path.join(
31+ series_cache_location, 'etc', 'apt', 'sources.list')
32+ with open(sources_list, "w+") as sources_list_file:
33+ sources_list_file.write(
34+ 'deb http://archive.ubuntu.com/ubuntu {0} main\n'.format(
35+ self.distroseries))
36+
37+ def get_apt_cache(self):
38+ # Create on-disk cache for distroseries if it
39+ # doesn't yet exist.
40+ series_cache_location = os.path.join(
41+ settings.DISK_APT_CACHE_LOCATION, self.distroseries)
42+ if not os.path.exists(series_cache_location):
43+ self.create_apt_cache(series_cache_location)
44+
45+ return apt.Cache(rootdir=series_cache_location)
46+
47 def __enter__(self):
48 tmp_sources_dir = tempfile.mkdtemp()
49 self.tmp_sources_dir = tmp_sources_dir
50 os.makedirs(os.path.join(tmp_sources_dir, 'etc', 'apt'))
51 tmp_sources_list = os.path.join(
52 tmp_sources_dir, 'etc', 'apt', 'sources.list')
53- sources_list = open(tmp_sources_list, 'w+')
54- sources_list.write(
55- 'deb http://archive.ubuntu.com/ubuntu {0} main\n'.format(
56- self.distroseries))
57- sources_list.close()
58+ with open(tmp_sources_list, "w+") as sources_list:
59+ sources_list.write(
60+ 'deb http://archive.ubuntu.com/ubuntu {0} main\n'.format(
61+ self.distroseries))
62
63- cache = apt.Cache(rootdir=tmp_sources_dir, memonly=True)
64+ cache = self.get_apt_cache()
65 try:
66- cache.update(sources_list=tmp_sources_list)
67+ cache.update()
68 except FetchFailedException:
69 # Make sure __exit__ still gets called
70 pass
71@@ -94,6 +113,8 @@
72 with Cache(distroseries_name) as cache:
73 self.verbosity = int(options['verbosity'])
74 data_dir = tempfile.mkdtemp()
75+
76+ # First create the apps based on the current cache.
77 distroseries, created = DistroSeries.objects.get_or_create(
78 code_name=distroseries_name)
79 if created and self.verbosity > 0:
80@@ -160,24 +181,25 @@
81
82 if form.is_valid():
83 # Check if the app already exists in our DB.
84- existing_apps = Application.objects.filter(
85- package_name=form.cleaned_data['package_name'],
86- distroseries=distroseries)
87- if existing_apps.count() > 0:
88+ try:
89+ app = Application.objects.get(
90+ package_name=form.cleaned_data['package_name'],
91+ distroseries=distroseries)
92 # Force the form to update the existing application.
93 # Simply setting the forms.instance doesn't work as the
94 # form constructs its instance during is_valid, and
95 # doesn't reconstruct it to save.
96- app = construct_instance(form, existing_apps[0])
97- else:
98+ app = construct_instance(form, app)
99+ except Application.DoesNotExist:
100 app = form.save(commit=False)
101 app.distroseries = distroseries
102+
103 # we don't have the description - just the desktop data, so:
104 # use the package_name to lookup the app in the cache and get
105 # the descrption from there
106 try:
107 cached_app = cache[app.package_name]
108- app.description = cached_app.description
109+ app.description = cached_app.candidate.description
110 except KeyError:
111 pass
112 app.save()
113
114=== modified file 'src/webcatalog/tests/test_commands.py'
115--- src/webcatalog/tests/test_commands.py 2011-06-07 20:07:04 +0000
116+++ src/webcatalog/tests/test_commands.py 2011-06-17 14:39:31 +0000
117@@ -21,20 +21,28 @@
118 absolute_import,
119 with_statement,
120 )
121-from apt.cache import Cache
122+import apt
123 import os
124 import shutil
125 import tempfile
126
127+from django.conf import settings
128 from django.core.management import call_command
129+from django.test import TestCase
130 from mock import (
131 patch,
132 MagicMock,
133 Mock,
134 )
135
136-from webcatalog.models import Application
137-from webcatalog.management.commands.import_app_install_data import Command
138+from webcatalog.models import (
139+ Application,
140+ DistroSeries,
141+ )
142+from webcatalog.management.commands.import_app_install_data import (
143+ Cache as CacheContextManager,
144+ Command,
145+ )
146 from webcatalog.tests.factory import TestCaseWithFactory
147
148 __metaclass__ = type
149@@ -45,6 +53,76 @@
150
151 class ImportAppInstallTestCase(TestCaseWithFactory):
152
153+ # If you want to run these tests against the real apt cache (which
154+ # is slower, but the real deal) you can update the following
155+ # attribute. Note: 3 tests will fail which make assumptions about
156+ # the uri for apt-install-data or the value of descriptions for
157+ # firefox and scribus in the cache.
158+ use_mock_apt_cache = True
159+
160+ def make_mock_apt_package(self, description, uri=''):
161+ """Helper to DRY up creating a mock apt package."""
162+ mock_package = Mock(spec=apt.package.Package)
163+ mock_package.candidate.description = description
164+ mock_package.candidate.uri = uri
165+ return mock_package
166+
167+ def make_mock_apt_cache(self):
168+ """Helper creating a mock apt_cache object.
169+
170+ And keeping the setUp readable.
171+ """
172+ mock_cache = Mock()
173+ mock_apt_firefox = self.make_mock_apt_package(
174+ description = "Firefox description")
175+ mock_apt_scribus = self.make_mock_apt_package(
176+ description = "Scribus description")
177+ mock_other_app = self.make_mock_apt_package(
178+ description = "Otherapp description")
179+ mock_app_install_data = self.make_mock_apt_package(
180+ description = "App install desc.",
181+ uri = 'http://example.com/app-install-1.01.deb')
182+ cache_dict = {
183+ 'firefox': mock_apt_firefox,
184+ 'scribus': mock_apt_scribus,
185+ 'otherapp': mock_other_app,
186+ 'app-install-data': mock_app_install_data,
187+ }
188+
189+ def getitem(self, key):
190+ return cache_dict.get(key, None)
191+
192+ mock_cache.__getitem__ = getitem
193+ return mock_cache
194+
195+ def setUp(self):
196+ """Create a mock apt-cache so we don't wait for the update."""
197+ super(ImportAppInstallTestCase, self).setUp()
198+
199+ # We use a temporary on-disk cache location for our
200+ # tests.
201+ self.tmp_apt_cache = tempfile.mkdtemp()
202+ self.patch_cache_setting = patch.object(settings,
203+ 'DISK_APT_CACHE_LOCATION', self.tmp_apt_cache)
204+ self.patch_cache_setting.start()
205+
206+ if self.use_mock_apt_cache:
207+ # The apt.Cache class is patched to return our own mock which
208+ # will be subscriptable with a pre-set dict with mock ock apt
209+ # applications for firefox and scribus.
210+ self.patch_cache = patch('apt.Cache')
211+ self.mock_cache_class = self.patch_cache.start()
212+ self.mock_cache = self.make_mock_apt_cache()
213+ self.mock_cache_class.return_value = self.mock_cache
214+
215+
216+ def tearDown(self):
217+ self.patch_cache_setting.stop()
218+ shutil.rmtree(self.tmp_apt_cache)
219+ if self.use_mock_apt_cache:
220+ self.patch_cache.stop()
221+ super(ImportAppInstallTestCase, self).tearDown()
222+
223 def test_app_data_downloaded_by_default(self):
224 get_data_fn = ('webcatalog.management.commands.'
225 'import_app_install_data.Command.'
226@@ -58,7 +136,8 @@
227 args = mock_get_data_fn.call_args[0]
228 self.assertEqual('onion', args[0])
229 self.assertTrue(args[1].startswith('/tmp/tmp'))
230- self.assertTrue(isinstance(args[2], Cache))
231+ if self.use_mock_apt_cache:
232+ self.assertEqual(self.mock_cache, args[2])
233
234 def test_get_latest_app_data_for_series(self):
235 # Requested and downloaded from the server.
236@@ -166,20 +245,9 @@
237 scribus.icon.size)
238
239 def test_get_app_data_uri_for_series(self):
240- # Note: you can test this with the real cache by commenting out
241- # the mocks - it just takes 4-5 seconds or so.
242- apt_cache = (
243- 'webcatalog.management.commands.import_app_install_data.'
244- 'apt.Cache')
245- with patch(apt_cache) as mock_apt_cache_cls:
246- mock_cache_item = Mock()
247- mock_cache_item.candidate.uri = 'http://example.com/my.deb'
248- mock_cache = MagicMock()
249- mock_cache.__getitem__.return_value = mock_cache_item
250- mock_apt_cache_cls.return_value = mock_cache
251- uri = Command().get_app_data_uri_for_series('natty')
252+ uri = Command().get_app_data_uri_for_series('natty')
253
254- self.assertEqual('http://example.com/my.deb', uri)
255+ self.assertEqual('http://example.com/app-install-1.01.deb', uri)
256
257 def test_description_added_to_app(self):
258 self.assertEqual(0, Application.objects.count())
259@@ -193,12 +261,10 @@
260 self.assertEqual(2, Application.objects.count())
261 firefox = Application.objects.get(package_name='firefox')
262 scribus = Application.objects.get(package_name='scribus')
263- firefox_description = 'Firefox delivers safe, easy web browsing.'
264- scribus_description = 'Scribus is an open source desktop page layout'
265- self.assertTrue(firefox.description.find(firefox_description) > -1)
266- self.assertTrue(scribus.description.find(scribus_description) > -1)
267+ self.assertEqual("Firefox description", firefox.description)
268+ self.assertEqual("Scribus description", scribus.description)
269
270- def test_descdription_updated_in_app(self):
271+ def test_description_updated_in_app(self):
272 app = self.factory.make_application(package_name='scribus',
273 distroseries=self.factory.make_distroseries(code_name='natty'))
274
275@@ -212,5 +278,42 @@
276 self.assertEqual(
277 'Graphic Page Layout and Publication (Stable)',
278 app_reloaded.comment)
279- scribus_description = 'Scribus is an open source desktop page layout'
280- self.assertTrue(app_reloaded.description.find(scribus_description) > -1)
281+ self.assertEqual("Scribus description", app_reloaded.description)
282+
283+ def test_cache_context_manager_creates_sources_list(self):
284+ # If the on-disk cache doesn't yet exist, it is created.
285+ call_command(
286+ 'import_app_install_data', 'natty',
287+ local_app_install_deb=self.factory.get_test_path(
288+ 'app-install-data-test_all.deb'),
289+ verbosity=0)
290+
291+ natty_cache = os.path.join(self.tmp_apt_cache, 'natty')
292+ self.assertTrue(os.path.exists(os.path.join(
293+ natty_cache, 'etc', 'apt', 'sources.list')))
294+ if self.use_mock_apt_cache:
295+ self.mock_cache_class.assert_called_with(rootdir=natty_cache)
296+ self.assertEqual(1, self.mock_cache.update.call_count)
297+
298+ def test_cache_context_manager_doesnt_overwrite_existing(self):
299+ # If an on-disk cache for the distroseries already exists, a
300+ # new one is *not* created.
301+ series_cache = os.path.join(self.tmp_apt_cache, 'testwebcatseries')
302+ if os.path.exists(series_cache):
303+ shutil.rmtree(series_cache)
304+ os.makedirs(series_cache)
305+
306+ create_apt_cache_fn = (
307+ 'webcatalog.management.commands.import_app_install_data.'
308+ 'Cache.create_apt_cache')
309+ with patch(create_apt_cache_fn) as mock_create_apt_cache:
310+ call_command(
311+ 'import_app_install_data', 'testwebcatseries',
312+ local_app_install_deb=self.factory.get_test_path(
313+ 'app-install-data-test_all.deb'),
314+ verbosity=0)
315+
316+ self.assertEqual(0, mock_create_apt_cache.call_count)
317+ if self.use_mock_apt_cache:
318+ self.mock_cache_class.assert_called_with(rootdir=series_cache)
319+ self.assertEqual(1, self.mock_cache.update.call_count)

Subscribers

People subscribed via source and target branches