Merge lp:~james-w/pkgme-devportal/dict-api into lp:pkgme-devportal

Proposed by James Westby
Status: Merged
Approved by: Jonathan Lange
Approved revision: 104
Merged at revision: 104
Proposed branch: lp:~james-w/pkgme-devportal/dict-api
Merge into: lp:pkgme-devportal
Prerequisite: lp:~james-w/pkgme-devportal/remove-source-package
Diff against target: 429 lines (+145/-89)
4 files modified
devportalbinary/acceptance/tests/__init__.py (+6/-3)
devportalbinary/database.py (+74/-48)
devportalbinary/tests/test_binary.py (+4/-7)
devportalbinary/tests/test_database.py (+61/-31)
To merge this branch: bzr merge lp:~james-w/pkgme-devportal/dict-api
Reviewer Review Type Date Requested Status
Jonathan Lange Approve
Review via email: mp+123160@code.launchpad.net

Commit message

Move to a dict based api for libdep information.

Description of the change

Hi,

The list-based API was very difficult to reason about, so I rewrote it
to be dict-based. I think it's now much more obvious what each of the
APIs is, and the data is easier to read if you are debugging.

Thanks,

James

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Hey,

Thanks for doing this. The dict-based API is much clearer.

The branch adds a bunch of new functions / methods that don't have direct test coverage AFAICT. Can you please add tests for these before this branch lands?

 libdep_mapping_for_deb
 get_arch_libdep_mapping
 extract_deb_control
 get_file_contents
 libdep_mapping_from_symbols

It might also be a good idea to maybe make some new modules and move things around a bit. Another day, I think.

cheers,
jml

review: Needs Fixing
lp:~james-w/pkgme-devportal/dict-api updated
103. By James Westby

Merge trunk.

104. By James Westby

Test the testable new functions.

Revision history for this message
James Westby (james-w) wrote :

On Fri, 07 Sep 2012 08:21:20 -0000, Jonathan Lange <email address hidden> wrote:
> Review: Needs Fixing
>
> Hey,
>
> Thanks for doing this. The dict-based API is much clearer.
>
> The branch adds a bunch of new functions / methods that don't have direct test coverage AFAICT. Can you please add tests for these before this branch lands?
>
> libdep_mapping_for_deb

This one extracts a .deb to a tempdir, so I'll leave this untested.

> get_arch_libdep_mapping

This downloads a .deb and passes it to the previous, so ditto.

> extract_deb_control

This obviously extracts a .deb also.

> get_file_contents

Done. (Which led to me simplifying the API slightly, to take a path,
rather than two path components which are joined, and to use treeshape
for the first time, which went well)

> libdep_mapping_from_symbols

Done.

> It might also be a good idea to maybe make some new modules and move things around a bit. Another day, I think.

Yeah, it needs some breaking up I think, but really that should be
breaking up in to libdep-service :-)

Thanks,

James

Revision history for this message
Jonathan Lange (jml) wrote :

Yay

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'devportalbinary/acceptance/tests/__init__.py'
2--- devportalbinary/acceptance/tests/__init__.py 2012-09-06 19:53:09 +0000
3+++ devportalbinary/acceptance/tests/__init__.py 2012-09-07 15:19:19 +0000
4@@ -60,8 +60,10 @@
5 def test_gtk(self):
6 """Runs successfully for a basic GTK+ application."""
7 dep_db = self.useFixture(DatabaseFixture())
8- dep_db.db.update_package("pthreads", [([("libpthread.so.0", "libpthread0")], "i386")])
9- dep_db.db.update_package("eglibc", [([("libc.so.6", "libc6")], "i386")])
10+ dep_db.db.update_package("pthreads",
11+ {'i386': {"libpthread.so.0": "libpthread0"}})
12+ dep_db.db.update_package("eglibc",
13+ {'i386': {"libc.so.6": "libc6"}})
14 test_data = self.useFixture(TestData("gtk"))
15 self.run_pkgme(test_data)
16 self.assertThat(test_data.path, HasFileTree({'debian/control': {}}))
17@@ -69,7 +71,8 @@
18 def test_bundled_library(self):
19 """Runs successfully for a basic bundled libary."""
20 dep_db = self.useFixture(DatabaseFixture())
21- dep_db.db.update_package("eglibc", [([("libc.so.6", "libc6")], "i386")])
22+ dep_db.db.update_package("eglibc",
23+ {'i386': {"libc.so.6": "libc6"}})
24 test_data = self.useFixture(TestData("bundled_lib"))
25 self.run_pkgme(test_data)
26 self.assertThat(
27
28=== modified file 'devportalbinary/database.py'
29--- devportalbinary/database.py 2012-09-06 19:53:09 +0000
30+++ devportalbinary/database.py 2012-09-07 15:19:19 +0000
31@@ -1,7 +1,7 @@
32 # Copyright 2011 Canonical Ltd. This software is licensed under the
33 # GNU Affero General Public License version 3 (see the file LICENSE).
34
35-from contextlib import closing
36+from contextlib import closing, contextmanager
37 import errno
38 import gzip
39 from itertools import chain
40@@ -37,6 +37,8 @@
41 APPLICATION_NAME = 'pkgme-binary'
42 SERVICE_ROOT = uris.LPNET_SERVICE_ROOT
43
44+SYMBOLS_FILENAME = 'symbols'
45+
46
47 # A list of package names that don't match lib*, but which we want
48 # to scan anyway.
49@@ -94,6 +96,26 @@
50 for series in our_series])
51
52
53+def libdep_mapping_for_deb(self, deb_file):
54+ """Returns the library -> dependency mapping information from a package.
55+
56+ The symbols file will be read to get the information. If it is not present
57+ an empty dict will be returned indicating that there are no libraries.
58+
59+ If there are libraries the dict will map from the library names
60+ to the package dependencies that should be used to get the
61+ library, e.g.
62+
63+ {'libfoo.so.1': 'libfoo1', ...}
64+ """
65+ with extract_deb_control(deb_file) as extracted:
66+ symbols_path = os.path.join(extracted, SYMBOLS_FILENAME)
67+ symbols = get_file_contents(symbols_path)
68+ if symbols is not None:
69+ return libdep_mapping_from_symbols(symbols)
70+ return {}
71+
72+
73 class BinaryPackage(object):
74
75 def __init__(self, bpph):
76@@ -123,46 +145,58 @@
77 arch,
78 )
79
80- def get_symbol_contents(self):
81+ def get_arch_libdep_mapping(self):
82+ """Return the library -> dependency mapping with architecture info.
83+
84+ :return: a dict mapping architecture tags to dicts mapping library
85+ names to dependencies, e.g.
86+ {'amd64': {'libfoo.so.1': 'libfoo1', ...}, ...}
87+ """
88+ arch = self.bpph.distro_arch_series.architecture_tag
89+ mapping = {}
90 directory = tempfile.mkdtemp()
91 try:
92 url = self.get_url()
93 filename = os.path.splitext(urlutils.basename(url))[0]
94- if not is_library_package(filename):
95- return
96- path = download_file(url, directory)
97- symbols_contents = extract_symbols_file(path)
98- return symbols_contents
99+ if is_library_package(filename):
100+ deb_file = download_file(url, directory)
101+ mapping = libdep_mapping_for_deb(deb_file)
102 finally:
103 shutil.rmtree(directory)
104-
105- def iter_symbols(self):
106- contents = self.get_symbol_contents()
107- if contents:
108- yield contents, self.bpph.distro_arch_series.architecture_tag
109-
110-
111-def extract_symbols_file(binary_package_path):
112- """Return the contents of the symbols file for a binary package.
113-
114- :param binary_package_path: The path to a binary package.
115+ return {arch: mapping}
116+
117+
118+@contextmanager
119+def extract_deb_control(binary_package_path):
120+ """Extract a deb control archive, returning the extracted path.
121+
122+ This is a context manager, and the tempdir will be cleaned up when closed.
123 """
124 temp_dir = tempfile.mkdtemp()
125 try:
126 run_subprocess(['dpkg-deb', '-e', binary_package_path, temp_dir])
127- symbol_path = os.path.join(temp_dir, 'symbols')
128- try:
129- return open(symbol_path).read()
130- except (OSError, IOError), e:
131- if e.errno == errno.ENOENT:
132- return
133- raise
134+ yield temp_dir
135 finally:
136 shutil.rmtree(temp_dir)
137
138
139-def iter_so_names(symbol_contents):
140- """Yield (library, dependency) from a symbols file.
141+def get_file_contents(path):
142+ """Read the contents of the file at `path`.
143+
144+ :return: the contents of the file, or None if there was
145+ no such file.
146+ :raises: Any errors opening or reading the file.
147+ """
148+ try:
149+ return open(path).read()
150+ except (OSError, IOError), e:
151+ if e.errno == errno.ENOENT:
152+ return
153+ raise
154+
155+
156+def libdep_mapping_from_symbols(symbol_contents):
157+ """Returns a dict mapping libraries to dependencies based on the symbols
158
159 Ignores versions and a whole bunch of other stuff and is probably the
160 wrong API even.
161@@ -179,6 +213,7 @@
162 # This is OK, as we're only processing symbols files from binary packages
163 # rather than source packages.
164
165+ mapping = {}
166 for line in symbol_contents.splitlines():
167 if not line:
168 # Blank lines are skipped
169@@ -207,7 +242,8 @@
170 #
171 # e.g. (arch=!armel)#include "libdiagnostics0.symbols.backtrace"
172 continue
173- yield library, dependency
174+ mapping[library] = dependency
175+ return mapping
176
177
178 def iter_contents_file(contents):
179@@ -478,39 +514,29 @@
180 unicode(dependency),
181 unicode(arch)))
182
183- def update_package(self, package_name, symbol_list):
184+ def update_package(self, package_name, arch_libdep_mapping):
185 """Update the database with the libdep info from 'package_name'.
186
187 :param package_name: The name of the package where the
188 symbols came from.
189- :param symbol_list: A list of representations of symbols files, where each
190- representation is a list of tuples of library and
191- dependency and an architecture string.
192- e.g. [([(libfoo, foo), ...], 'i386'), ...].
193- Each architecture should only appear once in the list.
194+ :param arch_libdep_mapping: a dict mapping architecture tags to dicts
195+ mapping library names to dependencies, e.g.
196+ {'amd64': {'libfoo.so.1': 'libfoo1', ...}, ...}
197 """
198- for symbols in symbol_list:
199- deps, architecture = symbols
200+ for arch, libdep_mapping in arch_libdep_mapping.items():
201 self._store.execute(
202 "DELETE FROM libdep WHERE source_package_name = ? "
203 "AND architecture = ?",
204- (unicode(package_name), unicode(architecture)))
205- for library, dependency in deps:
206+ (unicode(package_name), unicode(arch)))
207+ for library, dependency in libdep_mapping.items():
208 self.insert_new_library(
209- package_name, library, dependency, architecture)
210+ package_name, library, dependency, arch)
211 self._store.commit()
212
213 def close(self):
214 self._store.close()
215
216
217-def build_symbol_list(symbols):
218- symbol_list = [
219- (list(iter_so_names(symbols)), architecture)
220- for symbols, architecture in symbols]
221- return symbol_list
222-
223-
224 def fetch_symbol_files(scan_mode, package_name, db):
225 """Insert the libdep info for ``package_name`` into ``db``."""
226 if scan_mode != 'binary':
227@@ -519,8 +545,8 @@
228 package = BinaryPackage.find(lp.anonymous, package_name)
229 if not package:
230 return
231- symbol_list = build_symbol_list(package.iter_symbols())
232- db.update_package(package_name, symbol_list)
233+ arch_libdep_mapping = package.get_arch_libdep_mapping()
234+ db.update_package(package_name, arch_libdep_mapping)
235
236
237 def main():
238
239=== modified file 'devportalbinary/tests/test_binary.py'
240--- devportalbinary/tests/test_binary.py 2012-09-06 19:53:09 +0000
241+++ devportalbinary/tests/test_binary.py 2012-09-07 15:19:19 +0000
242@@ -256,7 +256,7 @@
243 def test_guess_dependencies(self):
244 self.useFixture(DatabaseConfig(self.db_fixture))
245 with closing(PackageDatabase(self.db_fixture.conn)) as db:
246- db.update_package('eglibc', [([('libc.so.6', 'libc6')], 'i386')])
247+ db.update_package('eglibc', {'i386': {'libc.so.6': 'libc6'}})
248 deps, arch = guess_dependencies(get_test_data_dir_path('hello'))
249 self.assertEqual(set(['libc6']), deps)
250
251@@ -330,7 +330,7 @@
252 shutil.copy(
253 get_test_data_file_path('hello', 'hello'), backend.path)
254 with closing(PackageDatabase(self.db_fixture.conn)) as db:
255- db.update_package('eglibc', [([('libc.so.6', 'libc6')], 'i386')])
256+ db.update_package('eglibc', {'i386': {'libc.so.6': 'libc6'}})
257 deps, arch = guess_dependencies(backend.path)
258 expected_deps = ', '.join(deps)
259 build_deps = backend.get_build_depends(None)
260@@ -367,10 +367,7 @@
261 def test_get_lib_overrides_for_packages_for_libraries(self):
262 # The configuration file overrides the found library dependencies.
263 with closing(PackageDatabase(self.db_fixture.conn)) as db:
264- db.update_package('foo', [
265- ([('libasound.so.2', 'libfoo')], 'i386'),
266- ([('libasound.so.2', 'libbar')], 'i386'),
267- ])
268+ db.update_package('foo', {'i386': {'libasound.so.2': 'libfoo'}})
269 self.assertEqual(
270 get_packages_for_libraries(["libasound.so.2"], "i386"),
271 set(["libasound2"]))
272@@ -384,7 +381,7 @@
273 self.useFixture(BinaryFileFixture(path))
274 backend = self.make_backend(path)
275 with closing(PackageDatabase(self.db_fixture.conn)) as db:
276- db.update_package('eglibc', [([('libc.so.6', 'libc6')], 'i386')])
277+ db.update_package('eglibc', {'i386': {'libc.so.6': 'libc6'}})
278 conf = load_configuration()
279 self.assertEqual(backend.get_architecture(metadata={}), "i386")
280
281
282=== modified file 'devportalbinary/tests/test_database.py'
283--- devportalbinary/tests/test_database.py 2012-09-06 19:53:09 +0000
284+++ devportalbinary/tests/test_database.py 2012-09-07 15:19:19 +0000
285@@ -5,6 +5,7 @@
286 from fixtures import (
287 TempDir,
288 )
289+from mock import patch
290 from storm.databases.postgres import psycopg2
291 from storm.exceptions import ClosedError
292 from testresources import ResourcedTestCase
293@@ -13,12 +14,16 @@
294 Equals,
295 Matcher,
296 )
297-from mock import patch
298+from treeshape import (
299+ CONTENT,
300+ FileTree,
301+ )
302
303 from devportalbinary.database import (
304 AptFilePackageDatabase,
305 BinaryPackage,
306- build_symbol_list,
307+ get_file_contents,
308+ libdep_mapping_from_symbols,
309 load_configuration,
310 PackageDatabase,
311 )
312@@ -94,16 +99,16 @@
313 def test_update_package(self):
314 db = self.get_package_db()
315 db.update_package(
316- 'foo', [([('libfoo.so.1', 'foo-bin')], 'i386')])
317+ 'foo', {'i386': {'libfoo.so.1': 'foo-bin'}})
318 deps = db.get_dependencies('libfoo.so.1')
319 self.assertEqual(deps, set(['foo-bin']))
320
321 def test_update_existing_package_no_libraries(self):
322 db = self.get_package_db()
323- db.update_package('foo', [([('libfoo.so.1', 'foo-bin')], 'i386')])
324+ db.update_package('foo', {'i386': {'libfoo.so.1': 'foo-bin'}})
325 # Run again, this time with no symbols, representing that a newer
326 # version of the package no longer exports any libraries.
327- db.update_package('foo', [([], 'i386')])
328+ db.update_package('foo', {'i386': {}})
329 deps = db.get_dependencies('libfoo.so.1')
330 self.assertEqual(deps, set())
331
332@@ -111,25 +116,11 @@
333 # If two architectures are updated separately then they
334 # shouldn't interfere
335 db = self.get_package_db()
336- db.update_package(
337- 'foo', [([('libfoo.so.1', 'foo-bin')], 'i386')])
338- db.update_package(
339- 'foo', [([('libfoo.so.1', 'foo-bin-amd64')], 'amd64')])
340+ db.update_package('foo', {'i386': {'libfoo.so.1': 'foo-bin'}})
341+ db.update_package('foo', {'amd64': {'libfoo.so.1': 'foo-bin-amd64'}})
342 deps = db.get_dependencies('libfoo.so.1', arch='i386')
343 self.assertEqual(deps, set(['foo-bin']))
344
345- def test_fetch_symbol_files(self):
346- # It's hard to test the function because it uses launchpadlib,
347- # so we'll test the main point, taking the output of
348- # package.iter_symbols(), passing it through build_symbol_list(),
349- # and then to # update_package() to ensure that the signatures
350- # match
351- symbol_list = build_symbol_list([('libfoo.so.1 foo-bin', 'i386')])
352- db = self.get_package_db()
353- db.update_package('foo', symbol_list)
354- deps = db.get_dependencies('libfoo.so.1')
355- self.assertEqual(set(['foo-bin']), deps)
356-
357 def test_close(self):
358 # Test that we can close the package db.
359 db = PackageDatabase(self.db_fixture.conn)
360@@ -144,16 +135,6 @@
361 db.close()
362
363
364-class TestBuildSymbolList(TestCase):
365-
366- def test_no_symbols(self):
367- self.assertEqual([], build_symbol_list([]))
368-
369- def test_some_symbols(self):
370- symbol_list = build_symbol_list([('a b', 'i386')])
371- self.assertEqual([([('a', 'b')], 'i386')], symbol_list)
372-
373-
374 class TestDatabaseConfiguration(TestCase):
375
376 def use_database_config(self, **db_settings):
377@@ -291,3 +272,52 @@
378 '1',
379 )
380 self.assertEqual(expected_url, BinaryPackage(bpph).get_url())
381+
382+
383+class TestGetFileContents(TestCase):
384+
385+ def test_exists(self):
386+ expected_content = 'boring content\n'
387+ filename = 'foo.txt'
388+ tree = {filename: {CONTENT: expected_content}}
389+ path = self.useFixture(FileTree(tree)).join(filename)
390+ self.assertEqual(expected_content, get_file_contents(path))
391+
392+ def test_not_exists(self):
393+ tree = {}
394+ path = self.useFixture(FileTree(tree)).join('nothere')
395+ self.assertEqual(None, get_file_contents(path))
396+
397+ def test_directory(self):
398+ tree = {}
399+ path = self.useFixture(FileTree(tree)).path
400+ self.assertRaises(IOError, get_file_contents, path)
401+
402+
403+class TestLibdepMappingFromSymbols(TestCase):
404+
405+ def test_empty(self):
406+ self.assertEqual({}, libdep_mapping_from_symbols(''))
407+
408+ def test_blank_line_ignored(self):
409+ self.assertEqual({}, libdep_mapping_from_symbols('\n'))
410+
411+ def test_alternate_template_ignored(self):
412+ self.assertEqual({}, libdep_mapping_from_symbols('| foo\n'))
413+
414+ def test_meta_information_ignored(self):
415+ self.assertEqual({}, libdep_mapping_from_symbols('* foo\n'))
416+
417+ def test_symbols_ignored(self):
418+ self.assertEqual({}, libdep_mapping_from_symbols(' foo\n'))
419+
420+ def test_comments_ignored(self):
421+ self.assertEqual({}, libdep_mapping_from_symbols('# foo\n'))
422+
423+ def test_include_ignored(self):
424+ self.assertEqual({},
425+ libdep_mapping_from_symbols('(arch=!armel)#include foo\n'))
426+
427+ def test_includes_mapping(self):
428+ self.assertEqual({'libfoo.so.1': 'libfoo'},
429+ libdep_mapping_from_symbols('libfoo.so.1 libfoo #MINVER#\n'))

Subscribers

People subscribed via source and target branches