Merge lp:~james-w/pkgme-devportal/dict-api into lp:pkgme-devportal
- dict-api
- Merge into trunk
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 |
Related bugs: |
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
- 103. By James Westby
-
Merge trunk.
- 104. By James Westby
-
Test the testable new functions.
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_
This one extracts a .deb to a tempdir, so I'll leave this untested.
> get_arch_
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_
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
Preview Diff
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')) |
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 libdep_ mapping deb_control mapping_ from_symbols
get_arch_
extract_
get_file_contents
libdep_
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