Merge lp:~cjwatson/click/libclick into lp:click

Proposed by Colin Watson
Status: Merged
Merged at revision: 354
Proposed branch: lp:~cjwatson/click/libclick
Merge into: lp:click
Diff against target: 9053 lines (+5629/-2337)
52 files modified
.bzrignore (+14/-0)
Makefile.am (+1/-1)
click/Makefile.am (+2/-0)
click/commands/desktophook.py (+5/-4)
click/commands/hook.py (+16/-8)
click/commands/info.py (+9/-6)
click/commands/install.py (+6/-2)
click/commands/list.py (+13/-10)
click/commands/pkgdir.py (+8/-7)
click/commands/register.py (+13/-5)
click/commands/unregister.py (+10/-5)
click/database.py (+0/-323)
click/hooks.py (+0/-438)
click/install.py (+14/-11)
click/osextras.py (+9/-1)
click/paths.py.in (+0/-2)
click/query.py (+0/-43)
click/tests/Makefile.am (+10/-0)
click/tests/__init__.py (+39/-0)
click/tests/config.py.in (+20/-0)
click/tests/gimock.py (+499/-0)
click/tests/gimock_types.py (+89/-0)
click/tests/helpers.py (+35/-6)
click/tests/preload.h (+110/-0)
click/tests/test_database.py (+283/-219)
click/tests/test_hooks.py (+846/-713)
click/tests/test_install.py (+26/-17)
click/tests/test_osextras.py (+82/-40)
click/tests/test_query.py (+52/-0)
click/tests/test_user.py (+151/-129)
click/user.py (+0/-344)
configure.ac (+44/-0)
debian/changelog (+4/-0)
debian/control (+38/-2)
debian/gir1.2-click-0.4.install (+1/-0)
debian/libclick-0.4-0.install (+1/-0)
debian/libclick-0.4-0.symbols (+87/-0)
debian/libclick-0.4-dev.install (+4/-0)
debian/rules (+9/-1)
lib/Makefile.am (+1/-0)
lib/click/Makefile.am (+98/-0)
lib/click/click-0.4.pc.in (+27/-0)
lib/click/click.sym (+85/-0)
lib/click/database.vala (+602/-0)
lib/click/hooks.vala (+1169/-0)
lib/click/osextras.vala (+213/-0)
lib/click/paths.vala.in (+46/-0)
lib/click/posix-extra.vapi (+48/-0)
lib/click/query.vala (+58/-0)
lib/click/user.vala (+677/-0)
lib/click/valac-wrapper.in (+51/-0)
run-click (+4/-0)
To merge this branch: bzr merge lp:~cjwatson/click/libclick
Reviewer Review Type Date Requested Status
Thomas Voß Pending
Review via email: mp+209105@code.launchpad.net

Description of the change

Hi Thomas. You volunteered your services as a reviewer. I still have a good deal of proofreading and testing to do on this, but I think it's reached the point where it's worth another reviewer having a look in parallel.

This is the first (and most probably largest) chunk of libclick. The core of it is a reasonably literal translation of several of click's Python modules to Vala. This part is fairly straightforward, though I took as much care as I could to keep the public ABI down to only what I'm prepared to keep stable; Vala helped but I couldn't get it to stop exposing some internal methods, so I resorted to libtool -export-symbols.

The really difficult bit was getting the test suite going (and I'd have had essentially the same issues with C). I wanted to satisfy the following constraints:

 * Don't rewrite the test suite at the same time as the code. In fact I'm quite happy with the test suite staying in Python permanently, but I definitely wanted to be able to run the existing test suite over the new code to validate the translation, with only relatively minor per-test changes.
 * Preserve the ability to insert mock functions.
 * Don't require relinking the library to run tests against it. This is a popular strategy for inserting mock functions in C, but I'm not comfortable with this level of interference with the code under test.

After a good deal of experimentation, I managed to get an initial version of this working, by generating LD_PRELOADable objects on the fly, using gobject-introspection to extract function signatures in a way that should hopefully be reasonably maintainable, and using ctypes to generate callbacks to Python methods and injecting pointers to those functions into the preloaded object. Affected tests must go through fork+execve in order that the dynamic loader can deal with LD_PRELOAD. I left an extensive header comment to explain how all this works. It's a bit fragile in various ways, and requires some manual maintenance of function declarations and structure contents (the handling of "struct stat" is particularly gnarly), but it pays off at the per-test level. I expect to start a new project implementing these general techniques reasonably soon, but I think it's worth having this in click for the time being rather than blocking on applying gold-plating.

To post a comment you must log in.
lp:~cjwatson/click/libclick updated
352. By Colin Watson

Keep to <80 columns.

353. By Colin Watson

Fix bug in Vala conversion of find_on_path: only regular files should count.

354. By Colin Watson

Remove unnecessary PosixExtra versions of setgrent and endgrent.

355. By Colin Watson

Fix infinite loop if Vala version of find_package_directory reaches "/".

356. By Colin Watson

lib/click/database.vala: Clear errno before calling getpwnam so that we can detect errors. (We already did this elsewhere.)

357. By Colin Watson

lib/click/user.vala: Might as well use our "exists" helper here too.

358. By Colin Watson

Use has_package_name in User.is_removable for brevity and to be closer to the original Python code.

359. By Colin Watson

Simplify Hooks.open_all slightly.

360. By Colin Watson

Relink library if lib/click/click.sym changes.

Revision history for this message
Colin Watson (cjwatson) wrote :

There are several bugs I'd want to tackle after this, collected here:

  https://bugs.launchpad.net/ubuntu/+source/click/+bugs?field.tag=libclick

lp:~cjwatson/click/libclick updated
361. By Colin Watson

merge trunk

362. By Colin Watson

Hook Exec lines must be run using /bin/sh; Shell.parse_argv isn't enough, as Exec may contain more than just a simple command.

363. By Colin Watson

Add a run-click script to run bin/click from the build tree; useful during development.

364. By Colin Watson

merge trunk

365. By Colin Watson

When creating a User, implicitly create a DB if none was provided. This is convenient for the common case of quick lookups in clients.

366. By Colin Watson

Remove "#include <gee.h>" from our external header file.

367. By Colin Watson

libclick-0.4-dev needs to depend on libglib2.0-dev for <glib.h> and <glib-object.h>.

Revision history for this message
Colin Watson (cjwatson) wrote :

I chatted with Thomas about this today. He doesn't think he's going to want major rework, although would like to carry on reviewing. We agreed that I would go ahead and land this anyway (since libclick is blocking other things, and it would be good to try to squeeze it in before Qt 5.2 breaks landing for however many days), and if he finds issues that I need to bump soname for then so be it.

(Let me know if anything here is inaccurate.)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2014-03-05 16:36:20 +0000
3+++ .bzrignore 2014-03-06 07:04:44 +0000
4@@ -27,6 +27,8 @@
5 dist
6 .tox
7 click/paths.py
8+click/tests/config.py
9+click/tests/preload.gir
10 build-aux/compile
11 build-aux/config.guess
12 build-aux/config.rpath
13@@ -44,6 +46,9 @@
14 debian/click
15 debian/click-doc
16 debian/files
17+debian/gir1.2-click-0.4
18+debian/libclick-0.4-0
19+debian/libclick-0.4-dev
20 debian/packagekit-plugin-click
21 debian/python3-click
22 debian/tmp
23@@ -51,6 +56,15 @@
24 init/systemd/click-user-hooks.service
25 init/upstart/click-system-hooks.conf
26 init/upstart/click-user-hooks.conf
27+lib/click/*.c
28+lib/click/*.gir
29+lib/click/*.typelib
30+lib/click/click.h
31+lib/click/click-0.4.pc
32+lib/click/click-0.4.vapi
33+lib/click/libclick_0_4_la_vala.stamp
34+lib/click/paths.vala
35+lib/click/valac-wrapper
36 m4/*
37 pk-plugin/com.ubuntu.click.policy
38 po/.intltool-merge-cache
39
40=== modified file 'Makefile.am'
41--- Makefile.am 2014-03-05 16:36:20 +0000
42+++ Makefile.am 2014-03-06 07:04:44 +0000
43@@ -1,4 +1,4 @@
44-SUBDIRS = click conf debhelper init po preload schroot
45+SUBDIRS = lib preload click conf debhelper init po schroot
46 if PACKAGEKIT
47 SUBDIRS += pk-plugin
48 endif
49
50=== modified file 'click/Makefile.am'
51--- click/Makefile.am 2013-09-03 12:50:11 +0000
52+++ click/Makefile.am 2014-03-06 07:04:44 +0000
53@@ -1,3 +1,5 @@
54+SUBDIRS = tests
55+
56 noinst_SCRIPTS = paths.py
57 CLEANFILES = $(noinst_SCRIPTS)
58
59
60=== modified file 'click/commands/desktophook.py'
61--- click/commands/desktophook.py 2013-09-30 09:14:02 +0000
62+++ click/commands/desktophook.py 2014-03-06 07:04:44 +0000
63@@ -23,8 +23,9 @@
64 from optparse import OptionParser
65 import os
66
67+from gi.repository import Click
68+
69 from click import osextras
70-from click.query import find_package_directory
71
72
73 COMMENT = \
74@@ -71,7 +72,7 @@
75
76 def read_hooks_for(path, package, app_name):
77 try:
78- directory = find_package_directory(path)
79+ directory = Click.find_package_directory(path)
80 manifest_path = os.path.join(
81 directory, ".click", "info", "%s.manifest" % package)
82 with io.open(manifest_path, encoding="UTF-8") as manifest:
83@@ -111,10 +112,10 @@
84 # TODO: This is a very crude .desktop file mangler; we should instead
85 # implement proper (de)serialisation.
86 def write_desktop_file(target_path, source_path, profile):
87- osextras.ensuredir(os.path.dirname(target_path))
88+ Click.ensuredir(os.path.dirname(target_path))
89 with io.open(source_path, encoding="UTF-8") as source, \
90 io.open(target_path, "w", encoding="UTF-8") as target:
91- source_dir = find_package_directory(source_path)
92+ source_dir = Click.find_package_directory(source_path)
93 written_comment = False
94 seen_path = False
95 for line in source:
96
97=== modified file 'click/commands/hook.py'
98--- click/commands/hook.py 2013-09-20 11:07:55 +0000
99+++ click/commands/hook.py 2014-03-06 07:04:44 +0000
100@@ -20,8 +20,7 @@
101 from optparse import OptionParser
102 from textwrap import dedent
103
104-from click.database import ClickDB
105-from click.hooks import ClickHook, run_system_hooks, run_user_hooks
106+from gi.repository import Click
107
108
109 per_hook_subcommands = {
110@@ -54,16 +53,25 @@
111 if subcommand in per_hook_subcommands:
112 if len(args) < 2:
113 parser.error("need hook name")
114- db = ClickDB(options.root)
115+ db = Click.DB()
116+ db.read()
117+ if options.root is not None:
118+ db.add(options.root)
119 name = args[1]
120- hook = ClickHook.open(db, name)
121+ hook = Click.Hook.open(db, name)
122 getattr(hook, per_hook_subcommands[subcommand])()
123 elif subcommand == "run-system":
124- db = ClickDB(options.root)
125- run_system_hooks(db)
126+ db = Click.DB()
127+ db.read()
128+ if options.root is not None:
129+ db.add(options.root)
130+ Click.run_system_hooks(db)
131 elif subcommand == "run-user":
132- db = ClickDB(options.root)
133- run_user_hooks(db, user=options.user)
134+ db = Click.DB()
135+ db.read()
136+ if options.root is not None:
137+ db.add(options.root)
138+ Click.run_user_hooks(db, user_name=options.user)
139 else:
140 parser.error(
141 "unknown subcommand '%s' (known: install, remove, run-system,"
142
143=== modified file 'click/commands/info.py'
144--- click/commands/info.py 2013-09-30 09:20:16 +0000
145+++ click/commands/info.py 2014-03-06 07:04:44 +0000
146@@ -24,18 +24,21 @@
147 import os
148 import sys
149
150-from click.database import ClickDB
151+from gi.repository import Click
152+
153 from click.install import DebFile
154-from click.user import ClickUser
155
156
157 def get_manifest(options, arg):
158 if "/" not in arg:
159- db = ClickDB(options.root)
160- registry = ClickUser(db, user=options.user)
161- if arg in registry:
162+ db = Click.DB()
163+ db.read()
164+ if options.root is not None:
165+ db.add(options.root)
166+ registry = Click.User.for_user(db, name=options.user)
167+ if registry.has_package_name(arg):
168 manifest_path = os.path.join(
169- registry.path(arg), ".click", "info", "%s.manifest" % arg)
170+ registry.get_path(arg), ".click", "info", "%s.manifest" % arg)
171 with io.open(manifest_path, encoding="UTF-8") as manifest:
172 return json.load(manifest)
173
174
175=== modified file 'click/commands/install.py'
176--- click/commands/install.py 2013-10-22 10:44:43 +0000
177+++ click/commands/install.py 2014-03-06 07:04:44 +0000
178@@ -21,7 +21,8 @@
179 import sys
180 from textwrap import dedent
181
182-from click.database import ClickDB
183+from gi.repository import Click
184+
185 from click.install import ClickInstaller, ClickInstallerError
186
187
188@@ -45,7 +46,10 @@
189 options, args = parser.parse_args(argv)
190 if len(args) < 1:
191 parser.error("need package file name")
192- db = ClickDB(options.root)
193+ db = Click.DB()
194+ db.read()
195+ if options.root is not None:
196+ db.add(options.root)
197 package_path = args[0]
198 installer = ClickInstaller(db, options.force_missing_framework)
199 try:
200
201=== modified file 'click/commands/list.py'
202--- click/commands/list.py 2013-09-30 09:14:02 +0000
203+++ click/commands/list.py 2014-03-06 07:04:44 +0000
204@@ -23,22 +23,25 @@
205 import os
206 import sys
207
208-from click.database import ClickDB
209-from click.user import ClickUser
210+from gi.repository import Click
211
212
213 def list_packages(options):
214- db = ClickDB(options.root)
215+ db = Click.DB()
216+ db.read()
217+ if options.root is not None:
218+ db.add(options.root)
219 if options.all:
220- for package, version, path, writeable in \
221- db.packages(all_versions=True):
222- yield package, version, path, writeable
223+ for inst in db.get_packages(all_versions=True):
224+ yield (
225+ inst.props.package, inst.props.version, inst.props.path,
226+ inst.props.writeable)
227 else:
228- registry = ClickUser(db, user=options.user)
229- for package, version in sorted(registry.items()):
230+ registry = Click.User.for_user(db, name=options.user)
231+ for package in sorted(registry.get_package_names()):
232 yield (
233- package, version, registry.path(package),
234- registry.removable(package))
235+ package, registry.get_version(package),
236+ registry.get_path(package), registry.is_removable(package))
237
238
239 def run(argv):
240
241=== modified file 'click/commands/pkgdir.py'
242--- click/commands/pkgdir.py 2013-09-16 12:44:40 +0000
243+++ click/commands/pkgdir.py 2014-03-06 07:04:44 +0000
244@@ -21,9 +21,7 @@
245 from optparse import OptionParser
246 import sys
247
248-from click.database import ClickDB
249-from click.query import find_package_directory
250-from click.user import ClickUser
251+from gi.repository import Click
252
253
254 def run(argv):
255@@ -39,12 +37,15 @@
256 parser.error("need package name")
257 try:
258 if "/" in args[0]:
259- print(find_package_directory(args[0]))
260+ print(Click.find_package_directory(args[0]))
261 else:
262- db = ClickDB(options.root)
263+ db = Click.DB()
264+ db.read()
265+ if options.root is not None:
266+ db.add(options.root)
267 package_name = args[0]
268- registry = ClickUser(db, user=options.user)
269- print(registry.path(package_name))
270+ registry = Click.User(db, name=options.user)
271+ print(registry.get_path(package_name))
272 except Exception as e:
273 print(e, file=sys.stderr)
274 return 1
275
276=== modified file 'click/commands/register.py'
277--- click/commands/register.py 2013-09-30 09:14:02 +0000
278+++ click/commands/register.py 2014-03-06 07:04:44 +0000
279@@ -19,8 +19,7 @@
280
281 from optparse import OptionParser
282
283-from click.database import ClickDB
284-from click.user import ClickUser
285+from gi.repository import Click, GLib
286
287
288 def run(argv):
289@@ -38,11 +37,20 @@
290 parser.error("need package name")
291 if len(args) < 2:
292 parser.error("need version")
293- db = ClickDB(options.root)
294+ db = Click.DB()
295+ db.read()
296+ if options.root is not None:
297+ db.add(options.root)
298 package = args[0]
299 version = args[1]
300- registry = ClickUser(db, user=options.user, all_users=options.all_users)
301- old_version = registry.get(package)
302+ if options.all_users:
303+ registry = Click.User.for_all_users(db)
304+ else:
305+ registry = Click.User.for_user(db, name=options.user)
306+ try:
307+ old_version = registry.get_version(package)
308+ except GLib.GError:
309+ old_version = None
310 registry.set_version(package, version)
311 if old_version is not None:
312 db.maybe_remove(package, old_version)
313
314=== modified file 'click/commands/unregister.py'
315--- click/commands/unregister.py 2013-09-30 09:14:02 +0000
316+++ click/commands/unregister.py 2014-03-06 07:04:44 +0000
317@@ -21,8 +21,7 @@
318 import os
319 import sys
320
321-from click.database import ClickDB
322-from click.user import ClickUser
323+from gi.repository import Click
324
325
326 def run(argv):
327@@ -44,10 +43,16 @@
328 "remove packages from disk")
329 if options.user is None and "SUDO_USER" in os.environ:
330 options.user = os.environ["SUDO_USER"]
331- db = ClickDB(options.root)
332+ db = Click.DB()
333+ db.read()
334+ if options.root is not None:
335+ db.add(options.root)
336 package = args[0]
337- registry = ClickUser(db, user=options.user, all_users=options.all_users)
338- old_version = registry[package]
339+ if options.all_users:
340+ registry = Click.User.for_all_users(db)
341+ else:
342+ registry = Click.User.for_user(db, name=options.user)
343+ old_version = registry.get_version(package)
344 if len(args) >= 2 and old_version != args[1]:
345 print(
346 "Not removing %s %s; expected version %s" %
347
348=== removed file 'click/database.py'
349--- click/database.py 2013-12-10 14:33:19 +0000
350+++ click/database.py 1970-01-01 00:00:00 +0000
351@@ -1,323 +0,0 @@
352-# Copyright (C) 2013 Canonical Ltd.
353-# Author: Colin Watson <cjwatson@ubuntu.com>
354-
355-# This program is free software: you can redistribute it and/or modify
356-# it under the terms of the GNU General Public License as published by
357-# the Free Software Foundation; version 3 of the License.
358-#
359-# This program is distributed in the hope that it will be useful,
360-# but WITHOUT ANY WARRANTY; without even the implied warranty of
361-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
362-# GNU General Public License for more details.
363-#
364-# You should have received a copy of the GNU General Public License
365-# along with this program. If not, see <http://www.gnu.org/licenses/>.
366-
367-"""Click databases."""
368-
369-from __future__ import print_function
370-
371-__metaclass__ = type
372-__all__ = [
373- "ClickDB",
374- ]
375-
376-
377-from collections import Sequence, defaultdict
378-import io
379-import json
380-import os
381-import pwd
382-import shutil
383-import subprocess
384-import sys
385-
386-try:
387- from configparser import Error as ConfigParserError
388- if sys.version < "3.2":
389- from configparser import SafeConfigParser as ConfigParser
390- else:
391- from configparser import ConfigParser
392-except ImportError:
393- from ConfigParser import Error as ConfigParserError
394- from ConfigParser import SafeConfigParser as ConfigParser
395-
396-from click import osextras
397-from click.paths import db_dir
398-
399-
400-class ClickSingleDB:
401- def __init__(self, root, master_db):
402- self.root = root
403- self.master_db = master_db
404-
405- def path(self, package, version):
406- """Look up a package and version in only this database."""
407- try_path = os.path.join(self.root, package, version)
408- if os.path.exists(try_path):
409- return try_path
410- else:
411- raise KeyError(
412- "%s %s does not exist in %s" % (package, version, self.root))
413-
414- def packages(self, all_versions=False):
415- """Return all current package versions in only this database.
416-
417- If all_versions=True, return all versions, not just current ones.
418- """
419- for package in sorted(osextras.listdir_force(self.root)):
420- if package == ".click":
421- continue
422- if all_versions:
423- package_path = os.path.join(self.root, package)
424- for version in sorted(osextras.listdir_force(package_path)):
425- version_path = os.path.join(package_path, version)
426- if (os.path.islink(version_path) or
427- not os.path.isdir(version_path)):
428- continue
429- yield package, version, version_path
430- else:
431- current_path = os.path.join(self.root, package, "current")
432- if os.path.islink(current_path):
433- version = os.readlink(current_path)
434- if "/" not in version:
435- yield package, version, current_path
436-
437- def _app_running(self, package, app_name, version):
438- app_id = "%s_%s_%s" % (package, app_name, version)
439- command = ["upstart-app-pid", app_id]
440- with open("/dev/null", "w") as devnull:
441- return subprocess.call(command, stdout=devnull) == 0
442-
443- def _any_app_running(self, package, version):
444- if not osextras.find_on_path("upstart-app-pid"):
445- return False
446- manifest_path = os.path.join(
447- self.path(package, version), ".click", "info",
448- "%s.manifest" % package)
449- try:
450- with io.open(manifest_path, encoding="UTF-8") as manifest:
451- manifest_json = json.load(manifest)
452- for app_name in manifest_json.get("hooks", {}):
453- if self._app_running(package, app_name, version):
454- return True
455- except Exception:
456- pass
457- return False
458-
459- def _remove_unless_running(self, package, version, verbose=False):
460- # Circular imports.
461- from click.hooks import package_remove_hooks
462- from click.user import ClickUser, GC_IN_USE_USER
463-
464- if self._any_app_running(package, version):
465- gc_in_use_user_db = ClickUser(self.master_db, user=GC_IN_USE_USER)
466- gc_in_use_user_db.set_version(package, version)
467- return
468-
469- version_path = self.path(package, version)
470- if verbose:
471- print("Removing %s" % version_path)
472- package_remove_hooks(self, package, version)
473- shutil.rmtree(version_path, ignore_errors=True)
474-
475- package_path = os.path.join(self.root, package)
476- current_path = os.path.join(package_path, "current")
477- if (os.path.islink(current_path) and
478- os.readlink(current_path) == version):
479- os.unlink(current_path)
480- # TODO: Perhaps we should relink current to the latest remaining
481- # version. However, that requires version comparison, and it's
482- # not clear whether it's worth it given that current is mostly
483- # superseded by user registration.
484- if not os.listdir(package_path):
485- os.rmdir(package_path)
486-
487- def maybe_remove(self, package, version):
488- """Remove a package version if it is not in use.
489-
490- "In use" may mean registered for another user, or running. In the
491- latter case we construct a fake registration so that we can tell the
492- difference later between a package version that was in use at the
493- time of removal and one that was never registered for any user.
494-
495- (This is unfortunately complex, and perhaps some day we can require
496- that installations always have some kind of registration to avoid
497- this complexity.)
498- """
499- # Circular imports.
500- from click.user import ClickUsers, GC_IN_USE_USER
501-
502- for user_name, user_db in ClickUsers(self.master_db).items():
503- if user_db.get(package) == version:
504- if user_name == GC_IN_USE_USER:
505- # Previously running; we'll check this again shortly.
506- user_db.remove(package)
507- else:
508- # In use.
509- return
510-
511- self._remove_unless_running(package, version)
512-
513- def gc(self, verbose=True):
514- """Remove package versions with no user registrations.
515-
516- To avoid accidentally removing packages that were installed without
517- ever having a user registration, we only garbage-collect packages
518- that were not removed by ClickSingleDB.maybe_remove due to having a
519- running application at the time.
520-
521- (This is unfortunately complex, and perhaps some day we can require
522- that installations always have some kind of registration to avoid
523- this complexity.)
524- """
525- # Circular import.
526- from click.user import ClickUser, ClickUsers, GC_IN_USE_USER
527-
528- user_reg = defaultdict(set)
529- gc_in_use = defaultdict(set)
530- for user_name, user_db in ClickUsers(self.master_db).items():
531- for package, version in user_db.items():
532- if user_name == GC_IN_USE_USER:
533- gc_in_use[package].add(version)
534- else:
535- user_reg[package].add(version)
536-
537- gc_in_use_user_db = ClickUser(self.master_db, user=GC_IN_USE_USER)
538- for package in sorted(osextras.listdir_force(self.root)):
539- if package == ".click":
540- continue
541- package_path = os.path.join(self.root, package)
542- for version in sorted(osextras.listdir_force(package_path)):
543- if version in user_reg[package]:
544- # In use.
545- continue
546- if version not in gc_in_use[package]:
547- version_path = os.path.join(package_path, version)
548- if verbose:
549- print(
550- "Not removing %s (never registered)." %
551- version_path)
552- continue
553- gc_in_use_user_db.remove(package)
554- self._remove_unless_running(package, version, verbose=verbose)
555-
556- def _clickpkg_paths(self):
557- """Yield all paths which should be owned by clickpkg."""
558- if os.path.exists(self.root):
559- yield self.root
560- for package in osextras.listdir_force(self.root):
561- if package == ".click":
562- path = os.path.join(self.root, ".click")
563- yield path
564- log_path = os.path.join(path, "log")
565- if os.path.exists(log_path):
566- yield log_path
567- users_path = os.path.join(path, "users")
568- if os.path.exists(users_path):
569- yield users_path
570- else:
571- path = os.path.join(self.root, package)
572- for dirpath, dirnames, filenames in os.walk(path):
573- yield dirpath
574- for dirname in dirnames:
575- dirname_path = os.path.join(dirpath, dirname)
576- if os.path.islink(dirname_path):
577- yield dirname_path
578- for filename in filenames:
579- yield os.path.join(dirpath, filename)
580-
581- def ensure_ownership(self):
582- """Ensure correct ownership of files in the database.
583-
584- On a system that is upgraded by delivering a new system image rather
585- than by package upgrades, it is possible for the clickpkg UID to
586- change. The overlay database must then be adjusted to account for
587- this.
588- """
589- pw = pwd.getpwnam("clickpkg")
590- try:
591- st = os.stat(self.root)
592- if st.st_uid == pw.pw_uid and st.st_gid == pw.pw_gid:
593- return
594- except OSError:
595- return
596- chown_kwargs = {}
597- if sys.version >= "3.3" and os.chown in os.supports_follow_symlinks:
598- chown_kwargs["follow_symlinks"] = False
599- for path in self._clickpkg_paths():
600- os.chown(path, pw.pw_uid, pw.pw_gid, **chown_kwargs)
601-
602-
603-class ClickDB(Sequence):
604- def __init__(self, extra_root=None, use_system=True, override_db_dir=None):
605- if override_db_dir is None:
606- override_db_dir = db_dir
607- self._db = []
608- if use_system:
609- for entry in sorted(osextras.listdir_force(override_db_dir)):
610- if not entry.endswith(".conf"):
611- continue
612- path = os.path.join(override_db_dir, entry)
613- config = ConfigParser()
614- try:
615- config.read(path)
616- root = config.get("Click Database", "root")
617- except ConfigParserError as e:
618- print(e, file=sys.stderr)
619- continue
620- self.add(root)
621- if extra_root is not None:
622- self.add(extra_root)
623-
624- def __getitem__(self, key):
625- return self._db[key]
626-
627- def __len__(self):
628- return len(self._db)
629-
630- def add(self, root):
631- self._db.append(ClickSingleDB(root, self))
632-
633- @property
634- def overlay(self):
635- """Return the directory where changes should be written."""
636- return self._db[-1].root
637-
638- def path(self, package, version):
639- """Look up a package and version in all databases."""
640- for db in reversed(self._db):
641- try:
642- return db.path(package, version)
643- except KeyError:
644- pass
645- else:
646- raise KeyError(
647- "%s %s does not exist in any database" % (package, version))
648-
649- def packages(self, all_versions=False):
650- """Return current package versions in all databases.
651-
652- If all_versions=True, return all versions, not just current ones.
653- """
654- seen = set()
655- for db in reversed(self._db):
656- writeable = db is self._db[-1]
657- for package, version, path in \
658- db.packages(all_versions=all_versions):
659- if all_versions:
660- seen_id = (package, version)
661- else:
662- seen_id = package
663- if seen_id not in seen:
664- yield package, version, path, writeable
665- seen.add(seen_id)
666-
667- def maybe_remove(self, package, version):
668- self._db[-1].maybe_remove(package, version)
669-
670- def gc(self, verbose=True):
671- self._db[-1].gc(verbose=verbose)
672-
673- def ensure_ownership(self):
674- self._db[-1].ensure_ownership()
675
676=== removed file 'click/hooks.py'
677--- click/hooks.py 2014-02-19 15:31:55 +0000
678+++ click/hooks.py 1970-01-01 00:00:00 +0000
679@@ -1,438 +0,0 @@
680-# Copyright (C) 2013 Canonical Ltd.
681-# Author: Colin Watson <cjwatson@ubuntu.com>
682-
683-# This program is free software: you can redistribute it and/or modify
684-# it under the terms of the GNU General Public License as published by
685-# the Free Software Foundation; version 3 of the License.
686-#
687-# This program is distributed in the hope that it will be useful,
688-# but WITHOUT ANY WARRANTY; without even the implied warranty of
689-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
690-# GNU General Public License for more details.
691-#
692-# You should have received a copy of the GNU General Public License
693-# along with this program. If not, see <http://www.gnu.org/licenses/>.
694-
695-"""Click package hooks.
696-
697-See doc/hooks.rst for the draft specification.
698-"""
699-
700-from __future__ import print_function
701-
702-__metaclass__ = type
703-__all__ = [
704- "ClickHook",
705- "package_install_hooks",
706- "run_system_hooks",
707- "run_user_hooks",
708- ]
709-
710-from functools import partial
711-import grp
712-import io
713-import json
714-import os
715-import pwd
716-import re
717-from string import Formatter
718-import subprocess
719-
720-from debian.deb822 import Deb822
721-
722-from click import osextras
723-from click.paths import hooks_dir
724-from click.user import ClickUser, ClickUsers
725-
726-
727-def _read_manifest_hooks(db, package, version):
728- if version is None:
729- return {}
730- try:
731- manifest_path = os.path.join(
732- db.path(package, version), ".click", "info",
733- "%s.manifest" % package)
734- with io.open(manifest_path, encoding="UTF-8") as manifest:
735- return json.load(manifest).get("hooks", {})
736- except (KeyError, IOError):
737- return {}
738-
739-
740-class ClickPatternFormatter(Formatter):
741- """A Formatter that handles simple $-expansions.
742-
743- `${key}` is replaced by the value of the `key` argument; `$$` is
744- replaced by `$`. Any `$` character not followed by `{...}` or `$` is
745- preserved intact.
746- """
747- _expansion_re = re.compile(r"\$(?:\$|{(.*?)})")
748-
749- def parse(self, format_string):
750- while True:
751- match = self._expansion_re.search(format_string)
752- if match is None:
753- if format_string:
754- yield format_string, None, None, None
755- return
756- start, end = match.span()
757- if format_string[match.start():match.end()] == "$$":
758- yield format_string[:match.start() + 1], None, None, None
759- else:
760- yield format_string[:match.start()], match.group(1), "", None
761- format_string = format_string[match.end():]
762-
763- def get_field(self, field_name, args, kwargs):
764- value = kwargs.get(field_name)
765- if value is None:
766- value = ""
767- return value, field_name
768-
769- def possible_expansion(self, s, format_string, *args, **kwargs):
770- """Check if s is a possible expansion.
771-
772- Any (keyword) arguments have the effect of binding some keys to
773- fixed values; unspecified keys may take any value, and will bind
774- greedily to the longest possible string.
775-
776- If s is a possible expansion, then this method returns a (possibly
777- empty) dictionary mapping all the unspecified keys to their bound
778- values. Otherwise, it returns None.
779- """
780- ret = {}
781- regex_pieces = []
782- group_names = []
783- for literal_text, field_name, format_spec, conversion in \
784- self.parse(format_string):
785- if literal_text:
786- regex_pieces.append(re.escape(literal_text))
787- if field_name is not None:
788- if field_name in kwargs:
789- regex_pieces.append(re.escape(kwargs[field_name]))
790- else:
791- regex_pieces.append("(.*)")
792- group_names.append(field_name)
793- match = re.match("^%s$" % "".join(regex_pieces), s)
794- if match is None:
795- return None
796- for group in range(len(group_names)):
797- ret[group_names[group]] = match.group(group + 1)
798- return ret
799-
800-
801-class ClickHook(Deb822):
802- _formatter = ClickPatternFormatter()
803-
804- def __init__(self, db, name, sequence=None, fields=None, encoding="utf-8"):
805- super(ClickHook, self).__init__(
806- sequence=sequence, fields=fields, encoding=encoding)
807- self.db = db
808- self.name = name
809-
810- @classmethod
811- def open(cls, db, name):
812- try:
813- with open(os.path.join(hooks_dir, "%s.hook" % name)) as f:
814- return cls(db, name, f)
815- except IOError:
816- raise KeyError("No click hook '%s' installed" % name)
817-
818- @classmethod
819- def open_all(cls, db, hook_name=None):
820- for entry in osextras.listdir_force(hooks_dir):
821- if not entry.endswith(".hook"):
822- continue
823- try:
824- with open(os.path.join(hooks_dir, entry)) as f:
825- hook = cls(db, entry[:-5], f)
826- if hook_name is None or hook.hook_name == hook_name:
827- yield hook
828- except IOError:
829- pass
830-
831- @property
832- def user_level(self):
833- return self.get("user-level", "no") == "yes"
834-
835- @property
836- def single_version(self):
837- return self.user_level or self.get("single-version", "no") == "yes"
838-
839- @property
840- def hook_name(self):
841- return self.get("hook-name", self.name)
842-
843- def short_app_id(self, package, app_name):
844- # TODO: perhaps this check belongs further up the stack somewhere?
845- if "_" in app_name or "/" in app_name:
846- raise ValueError(
847- "Application name '%s' may not contain _ or / characters" %
848- app_name)
849- return "%s_%s" % (package, app_name)
850-
851- def app_id(self, package, version, app_name):
852- return "%s_%s" % (self.short_app_id(package, app_name), version)
853-
854- def _user_home(self, user):
855- if user is None:
856- return None
857- # TODO: make robust against removed users
858- # TODO: caching
859- return pwd.getpwnam(user).pw_dir
860-
861- def pattern(self, package, version, app_name, user=None):
862- app_id = self.app_id(package, version, app_name)
863- kwargs = {
864- "id": app_id,
865- "user": user,
866- "home": self._user_home(user),
867- }
868- if self.single_version:
869- kwargs["short-id"] = self.short_app_id(package, app_name)
870- return self._formatter.format(self["pattern"], **kwargs).rstrip(os.sep)
871-
872- def _drop_privileges(self, username):
873- if os.geteuid() != 0:
874- return
875- pw = pwd.getpwnam(username)
876- os.setgroups(
877- [g.gr_gid for g in grp.getgrall() if username in g.gr_mem])
878- # Portability note: this assumes that we have [gs]etres[gu]id, which
879- # is true on Linux but not necessarily elsewhere. If you need to
880- # support something else, there are reasonably standard alternatives
881- # involving other similar calls; see e.g. gnulib/lib/idpriv-drop.c.
882- os.setresgid(pw.pw_gid, pw.pw_gid, pw.pw_gid)
883- os.setresuid(pw.pw_uid, pw.pw_uid, pw.pw_uid)
884- assert os.getresuid() == (pw.pw_uid, pw.pw_uid, pw.pw_uid)
885- assert os.getresgid() == (pw.pw_gid, pw.pw_gid, pw.pw_gid)
886- os.environ["HOME"] = pw.pw_dir
887- os.umask(osextras.get_umask() | 0o002)
888-
889- def _run_commands_user(self, user=None):
890- if self.user_level:
891- return user
892- else:
893- return self["user"]
894-
895- def _run_commands(self, user=None):
896- if "exec" in self:
897- drop_privileges = partial(
898- self._drop_privileges, self._run_commands_user(user=user))
899- subprocess.check_call(
900- self["exec"], preexec_fn=drop_privileges, shell=True)
901- if self.get("trigger", "no") == "yes":
902- raise NotImplementedError("'Trigger: yes' not yet implemented")
903-
904- def _previous_entries(self, user=None):
905- """Find entries that match the structure of our links."""
906- link_dir = os.path.dirname(self.pattern("", "", "", user=user))
907- # TODO: This only works if the app ID only appears, at most, in the
908- # last component of the pattern path.
909- for previous_entry in osextras.listdir_force(link_dir):
910- previous_path = os.path.join(link_dir, previous_entry)
911- previous_exp = self._formatter.possible_expansion(
912- previous_path, self["pattern"], user=user,
913- home=self._user_home(user))
914- if previous_exp is None or "id" not in previous_exp:
915- continue
916- previous_id = previous_exp["id"]
917- try:
918- previous_package, previous_app_name, previous_version = (
919- previous_id.split("_", 2))
920- yield (
921- previous_path,
922- previous_package, previous_version, previous_app_name)
923- except ValueError:
924- continue
925-
926- def _install_link(self, package, version, app_name, relative_path,
927- user=None, user_db=None):
928- """Install a hook symlink.
929-
930- This should be called with dropped privileges if necessary.
931- """
932- if self.user_level:
933- target = os.path.join(user_db.path(package), relative_path)
934- else:
935- target = os.path.join(
936- self.db.path(package, version), relative_path)
937- link = self.pattern(package, version, app_name, user=user)
938- if not os.path.islink(link) or os.readlink(link) != target:
939- osextras.ensuredir(os.path.dirname(link))
940- osextras.symlink_force(target, link)
941-
942- def install_package(self, package, version, app_name, relative_path,
943- user=None):
944- if self.user_level:
945- user_db = ClickUser(self.db, user=user)
946- else:
947- assert user is None
948-
949- # Remove previous versions if necessary.
950- if self.single_version:
951- for path, p_package, p_version, p_app_name in \
952- self._previous_entries(user=user):
953- if (p_package == package and p_app_name == app_name and
954- p_version != version):
955- osextras.unlink_force(path)
956-
957- if self.user_level:
958- with user_db._dropped_privileges():
959- self._install_link(
960- package, version, app_name, relative_path,
961- user=user, user_db=user_db)
962- else:
963- self._install_link(package, version, app_name, relative_path)
964- self._run_commands(user=user)
965-
966- def remove_package(self, package, version, app_name, user=None):
967- osextras.unlink_force(
968- self.pattern(package, version, app_name, user=user))
969- self._run_commands(user=user)
970-
971- def _all_packages(self, user=None):
972- """Return an iterable of all unpacked packages.
973-
974- If running a user-level hook, this returns (package, version, user)
975- for the current version of each package registered for each user, or
976- only for a single user if user is not None.
977-
978- If running a system-level hook, this returns (package, version,
979- None) for each version of each unpacked package.
980- """
981- if self.user_level:
982- if user is not None:
983- user_db = ClickUser(self.db, user=user)
984- for package, version in user_db.items():
985- yield package, version, user
986- else:
987- for user_name, user_db in ClickUsers(self.db).items():
988- if user_name.startswith("@"):
989- continue
990- for package, version in user_db.items():
991- yield package, version, user_name
992- else:
993- for package, version, _, _ in self.db.packages():
994- yield package, version, None
995-
996- def _relevant_apps(self, user=None):
997- """Return an iterable of all applications relevant for this hook."""
998- for package, version, user_name in self._all_packages(user=user):
999- manifest = _read_manifest_hooks(self.db, package, version)
1000- for app_name, hooks in manifest.items():
1001- if self.hook_name in hooks:
1002- yield (
1003- package, version, app_name, user_name,
1004- hooks[self.hook_name])
1005-
1006- def install(self, user=None):
1007- for package, version, app_name, user_name, relative_path in (
1008- self._relevant_apps(user=user)):
1009- self.install_package(
1010- package, version, app_name, relative_path, user=user_name)
1011-
1012- def remove(self, user=None):
1013- for package, version, app_name, user_name, _ in (
1014- self._relevant_apps(user=user)):
1015- self.remove_package(package, version, app_name, user=user_name)
1016-
1017- def sync(self, user=None):
1018- if self.user_level:
1019- user_db = ClickUser(self.db, user=user)
1020- else:
1021- assert user is None
1022-
1023- seen = set()
1024- for package, version, app_name, user_name, relative_path in (
1025- self._relevant_apps(user=user)):
1026- seen.add((package, version, app_name))
1027- if self.user_level:
1028- with user_db._dropped_privileges():
1029- self._install_link(
1030- package, version, app_name, relative_path,
1031- user=user_name, user_db=user_db)
1032- else:
1033- self._install_link(package, version, app_name, relative_path)
1034- for path, package, version, app_name in \
1035- self._previous_entries(user=user):
1036- if (package, version, app_name) not in seen:
1037- osextras.unlink_force(path)
1038- self._run_commands(user=user)
1039-
1040-
1041-def _app_hooks(hooks):
1042- items = set()
1043- for app_name in hooks:
1044- for hook_name in hooks[app_name]:
1045- items.add((app_name, hook_name))
1046- return items
1047-
1048-
1049-def package_install_hooks(db, package, old_version, new_version, user=None):
1050- """Run hooks following installation or upgrade of a Click package.
1051-
1052- If user is None, only run system-level hooks. If user is not None, only
1053- run user-level hooks for that user.
1054- """
1055- old_manifest = _read_manifest_hooks(db, package, old_version)
1056- new_manifest = _read_manifest_hooks(db, package, new_version)
1057-
1058- # Remove any targets for single-version hooks that were in the old
1059- # manifest but not the new one.
1060- for app_name, hook_name in sorted(
1061- _app_hooks(old_manifest) - _app_hooks(new_manifest)):
1062- for hook in ClickHook.open_all(db, hook_name):
1063- if hook.user_level != (user is not None):
1064- continue
1065- if hook.single_version:
1066- hook.remove_package(package, old_version, app_name, user=user)
1067-
1068- for app_name, app_hooks in sorted(new_manifest.items()):
1069- for hook_name, relative_path in sorted(app_hooks.items()):
1070- for hook in ClickHook.open_all(db, hook_name):
1071- if hook.user_level != (user is not None):
1072- continue
1073- hook.install_package(
1074- package, new_version, app_name, relative_path, user=user)
1075-
1076-
1077-def package_remove_hooks(db, package, old_version, user=None):
1078- """Run hooks following removal of a Click package.
1079-
1080- If user is None, only run system-level hooks. If user is not None, only
1081- run user-level hooks for that user.
1082- """
1083- old_manifest = _read_manifest_hooks(db, package, old_version)
1084-
1085- for app_name, app_hooks in sorted(old_manifest.items()):
1086- for hook_name in sorted(app_hooks):
1087- for hook in ClickHook.open_all(db, hook_name):
1088- if hook.user_level != (user is not None):
1089- continue
1090- hook.remove_package(package, old_version, app_name, user=user)
1091-
1092-
1093-def run_system_hooks(db):
1094- """Run system-level hooks for all installed packages.
1095-
1096- This is useful when starting up from images with preinstalled packages
1097- which may not have had their system-level hooks run properly when
1098- building the image. It is suitable for running at system startup.
1099- """
1100- db.ensure_ownership()
1101- for hook in ClickHook.open_all(db):
1102- if not hook.user_level:
1103- hook.sync()
1104-
1105-
1106-def run_user_hooks(db, user=None):
1107- """Run user-level hooks for all packages registered for a user.
1108-
1109- This is useful to catch up with packages that may have been preinstalled
1110- and registered for all users. It is suitable for running at session
1111- startup.
1112- """
1113- if user is None:
1114- user = pwd.getpwuid(os.getuid()).pw_name
1115- for hook in ClickHook.open_all(db):
1116- if hook.user_level:
1117- hook.sync(user=user)
1118
1119=== modified file 'click/install.py'
1120--- click/install.py 2014-01-22 14:02:33 +0000
1121+++ click/install.py 2014-03-06 07:04:44 +0000
1122@@ -43,12 +43,10 @@
1123 import apt_pkg
1124 from debian.debfile import DebFile as _DebFile
1125 from debian.debian_support import Version
1126+from gi.repository import Click
1127
1128-from click import osextras
1129-from click.hooks import package_install_hooks
1130 from click.paths import frameworks_dir, preload_path
1131 from click.preinst import static_preinst_matches
1132-from click.user import ClickUser
1133 from click.versions import spec_version
1134
1135
1136@@ -313,12 +311,14 @@
1137
1138 def install(self, path, user=None, all_users=False):
1139 package_name, package_version = self.audit(path, check_arch=True)
1140- package_dir = os.path.join(self.db.overlay, package_name)
1141+ package_dir = os.path.join(self.db.props.overlay, package_name)
1142 inst_dir = os.path.join(package_dir, package_version)
1143- assert os.path.dirname(os.path.dirname(inst_dir)) == self.db.overlay
1144+ assert (
1145+ os.path.dirname(os.path.dirname(inst_dir)) ==
1146+ self.db.props.overlay)
1147
1148- self._check_write_permissions(self.db.overlay)
1149- root_click = os.path.join(self.db.overlay, ".click")
1150+ self._check_write_permissions(self.db.props.overlay)
1151+ root_click = os.path.join(self.db.props.overlay, ".click")
1152 if not os.path.exists(root_click):
1153 os.makedirs(root_click)
1154 if os.geteuid() == 0:
1155@@ -348,7 +348,7 @@
1156 if "LD_PRELOAD" in env:
1157 preloads.append(env["LD_PRELOAD"])
1158 env["LD_PRELOAD"] = " ".join(preloads)
1159- env["CLICK_BASE_DIR"] = self.db.overlay
1160+ env["CLICK_BASE_DIR"] = self.db.props.overlay
1161 env["CLICK_PACKAGE_PATH"] = path
1162 env["CLICK_PACKAGE_FD"] = str(fd.fileno())
1163 env.pop("HOME", None)
1164@@ -379,11 +379,11 @@
1165 old_version = None
1166 else:
1167 old_version = None
1168- package_install_hooks(
1169+ Click.package_install_hooks(
1170 self.db, package_name, old_version, package_version)
1171
1172 new_path = os.path.join(package_dir, "current.new")
1173- osextras.symlink_force(package_version, new_path)
1174+ Click.symlink_force(package_version, new_path)
1175 if os.geteuid() == 0:
1176 # shutil.chown would be more convenient, but it doesn't support
1177 # follow_symlinks=False in Python 3.3.
1178@@ -393,7 +393,10 @@
1179 os.rename(new_path, current_path)
1180
1181 if user is not None or all_users:
1182- registry = ClickUser(self.db, user=user, all_users=all_users)
1183+ if all_users:
1184+ registry = Click.User.for_all_users(self.db)
1185+ else:
1186+ registry = Click.User.for_user(self.db, name=user)
1187 registry.set_version(package_name, package_version)
1188
1189 if old_version is not None:
1190
1191=== modified file 'click/osextras.py'
1192--- click/osextras.py 2013-09-04 15:59:18 +0000
1193+++ click/osextras.py 2014-03-06 07:04:44 +0000
1194@@ -13,7 +13,15 @@
1195 # You should have received a copy of the GNU General Public License
1196 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1197
1198-"""Extra OS-level utility functions."""
1199+"""Extra OS-level utility functions.
1200+
1201+Usually we can instead use the functions exported from
1202+lib/click/osextras.vala via GObject Introspection. These pure-Python
1203+versions are preserved so that they can be used from code that needs to be
1204+maximally portable: for example, click.build is intended to be usable even
1205+on systems that lack GObject, as long as they have a reasonably recent
1206+version of Python.
1207+"""
1208
1209 __all__ = [
1210 'ensuredir',
1211
1212=== modified file 'click/paths.py.in'
1213--- click/paths.py.in 2013-09-03 12:50:11 +0000
1214+++ click/paths.py.in 2014-03-06 07:04:44 +0000
1215@@ -17,5 +17,3 @@
1216
1217 preload_path = "@pkglibdir@/libclickpreload.so"
1218 frameworks_dir = "@pkgdatadir@/frameworks"
1219-hooks_dir = "@pkgdatadir@/hooks"
1220-db_dir = "@sysconfdir@/click/databases"
1221
1222=== removed file 'click/query.py'
1223--- click/query.py 2013-07-23 18:30:48 +0000
1224+++ click/query.py 1970-01-01 00:00:00 +0000
1225@@ -1,43 +0,0 @@
1226-# Copyright (C) 2013 Canonical Ltd.
1227-# Author: Colin Watson <cjwatson@ubuntu.com>
1228-
1229-# This program is free software: you can redistribute it and/or modify
1230-# it under the terms of the GNU General Public License as published by
1231-# the Free Software Foundation; version 3 of the License.
1232-#
1233-# This program is distributed in the hope that it will be useful,
1234-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1235-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1236-# GNU General Public License for more details.
1237-#
1238-# You should have received a copy of the GNU General Public License
1239-# along with this program. If not, see <http://www.gnu.org/licenses/>.
1240-
1241-"""Query information about installed Click packages."""
1242-
1243-from __future__ import print_function
1244-
1245-__metaclass__ = type
1246-__all__ = [
1247- 'find_package_directory',
1248- ]
1249-
1250-import os
1251-
1252-
1253-def _walk_up(path):
1254- while True:
1255- yield path
1256- newpath = os.path.dirname(path)
1257- if newpath == path:
1258- return
1259- path = newpath
1260-
1261-
1262-def find_package_directory(path):
1263- for directory in _walk_up(os.path.realpath(path)):
1264- if os.path.isdir(os.path.join(directory, ".click", "info")):
1265- return directory
1266- break
1267- else:
1268- raise Exception("No package directory found for %s" % path)
1269
1270=== added file 'click/tests/Makefile.am'
1271--- click/tests/Makefile.am 1970-01-01 00:00:00 +0000
1272+++ click/tests/Makefile.am 2014-03-06 07:04:44 +0000
1273@@ -0,0 +1,10 @@
1274+noinst_DATA = preload.gir
1275+CLEANFILES = $(noinst_DATA)
1276+
1277+preload.gir: preload.h
1278+ PKG_CONFIG_PATH=$(top_builddir)/lib/click g-ir-scanner \
1279+ -n preload --nsversion 0 -l c \
1280+ --pkg glib-2.0 --pkg gee-0.8 --pkg click-0.4 \
1281+ -I$(top_builddir)/lib/click -L$(top_builddir)/lib/click \
1282+ --accept-unprefixed --warn-all \
1283+ $< --output $@
1284
1285=== modified file 'click/tests/__init__.py'
1286--- click/tests/__init__.py 2013-04-10 15:55:06 +0000
1287+++ click/tests/__init__.py 2014-03-06 07:04:44 +0000
1288@@ -0,0 +1,39 @@
1289+from __future__ import print_function
1290+
1291+import os
1292+import sys
1293+
1294+from click.tests import config
1295+
1296+
1297+def _append_env_path(envname, value):
1298+ if envname in os.environ:
1299+ if value in os.environ[envname].split(":"):
1300+ return False
1301+ os.environ[envname] = "%s:%s" % (os.environ[envname], value)
1302+ else:
1303+ os.environ[envname] = value
1304+ return True
1305+
1306+
1307+# Don't do any of this in interactive mode.
1308+if not hasattr(sys, "ps1"):
1309+ _lib_click_dir = os.path.join(config.abs_top_builddir, "lib", "click")
1310+ changed = False
1311+ if _append_env_path(
1312+ "LD_LIBRARY_PATH", os.path.join(_lib_click_dir, ".libs")):
1313+ changed = True
1314+ if _append_env_path("GI_TYPELIB_PATH", _lib_click_dir):
1315+ changed = True
1316+ if changed:
1317+ # We have to re-exec ourselves to get the dynamic loader to pick up
1318+ # the new value of LD_LIBRARY_PATH.
1319+ if "-m unittest" in sys.argv[0]:
1320+ # unittest does horrible things to sys.argv in the name of
1321+ # "usefulness", making the re-exec more painful than it needs to
1322+ # be.
1323+ os.execvp(
1324+ sys.executable, [sys.executable, "-m", "unittest"] + sys.argv[1:])
1325+ else:
1326+ os.execvp(sys.executable, [sys.executable] + sys.argv)
1327+ os._exit(1)
1328
1329=== added file 'click/tests/config.py.in'
1330--- click/tests/config.py.in 1970-01-01 00:00:00 +0000
1331+++ click/tests/config.py.in 2014-03-06 07:04:44 +0000
1332@@ -0,0 +1,20 @@
1333+# Copyright (C) 2014 Canonical Ltd.
1334+# Author: Colin Watson <cjwatson@ubuntu.com>
1335+
1336+# This program is free software: you can redistribute it and/or modify
1337+# it under the terms of the GNU General Public License as published by
1338+# the Free Software Foundation; version 3 of the License.
1339+#
1340+# This program is distributed in the hope that it will be useful,
1341+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1342+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1343+# GNU General Public License for more details.
1344+#
1345+# You should have received a copy of the GNU General Public License
1346+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1347+
1348+abs_top_builddir = "@abs_top_builddir@"
1349+STAT_OFFSET_UID = @STAT_OFFSET_UID@
1350+STAT_OFFSET_GID = @STAT_OFFSET_GID@
1351+STAT64_OFFSET_UID = @STAT64_OFFSET_UID@
1352+STAT64_OFFSET_GID = @STAT64_OFFSET_GID@
1353
1354=== added file 'click/tests/gimock.py'
1355--- click/tests/gimock.py 1970-01-01 00:00:00 +0000
1356+++ click/tests/gimock.py 2014-03-06 07:04:44 +0000
1357@@ -0,0 +1,499 @@
1358+# Copyright (C) 2014 Canonical Ltd.
1359+# Author: Colin Watson <cjwatson@ubuntu.com>
1360+
1361+# This program is free software: you can redistribute it and/or modify
1362+# it under the terms of the GNU General Public License as published by
1363+# the Free Software Foundation; version 3 of the License.
1364+#
1365+# This program is distributed in the hope that it will be useful,
1366+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1367+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1368+# GNU General Public License for more details.
1369+#
1370+# You should have received a copy of the GNU General Public License
1371+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1372+
1373+"""Mock function support based on GObject Introspection.
1374+
1375+(Note to reviewers: I expect to rewrite this from scratch on my own time as
1376+a more generalised set of Python modules for unit testing of C code,
1377+although using similar core ideas. This is a first draft for the purpose of
1378+getting Click's test suite to work expediently, rather than an interface I'm
1379+prepared to commit to long-term.)
1380+
1381+Python is a versatile and concise language for writing tests, and GObject
1382+Introspection (GI) makes it straightforward (often trivial) to bind native
1383+code into Python. However, writing tests for native code quickly runs into
1384+the problem of how to build mock functions. You might reasonably have code
1385+that calls chown(), for instance, and want to test how it's called rather
1386+than worrying about setting up a fakeroot-type environment where chown()
1387+will work. The obvious solution is to use `LD_PRELOAD` wrappers, but there
1388+are various problems to overcome in practice:
1389+
1390+ * You can only set up a new `LD_PRELOAD` by going through the run-time
1391+ linker; you can't just set it for a single in-process test case.
1392+ * Generating the preloaded wrapper involves a fair bit of boilerplate code.
1393+ * Having to write per-test mock code in C is inconvenient, and makes it
1394+ difficult to get information back out of the mock (such as "how often was
1395+ this function called, and with what arguments?").
1396+
1397+The first problem can be solved by a decorator that knows how to run
1398+individual tests in a subprocess. This is made somewhat more inconvenient
1399+by the fact that there is no way for a context manager's `__enter__` method
1400+to avoid executing the context-managed block other than by throwing an
1401+exception, which makes it hard to silently avoid executing the test case in
1402+the parent process, but we can work around this at the cost of an extra line
1403+of code per invocation.
1404+
1405+For the rest, a combination of GI itself and ctypes can help. We can use GI
1406+to keep track of argument and return types of the mocked C functions in a
1407+reasonably sane way, by parsing header files. We're operating in the other
1408+direction from how GI is normally used, so PyGObject can't deal with
1409+bridging the two calling conventions for us. ctypes can: but we still need
1410+to be careful! We have to construct the callback functions in the child
1411+process, ensure that we keep references to them, and inject function
1412+pointers into the preloaded library via specially-named helper functions;
1413+until those function pointers are set up we must make sure to call the libc
1414+functions instead (since some of them might be called during Python
1415+startup).
1416+
1417+The combination of all of this allows us to bridge C functions somewhat
1418+transparently into Python. This lets you supply a Python function or method
1419+as the mock replacement for a C library function, making it much simpler to
1420+record state.
1421+
1422+It's still not perfect:
1423+
1424+ * We're using GI in an upside-down kind of way, and we specifically need
1425+ GIR files rather than typelibs so that we can extract the original C
1426+ type, so some fiddling is required for each new function you want to
1427+ mock.
1428+
1429+ * The subprocess arrangements are unavoidably slow and it's possible that
1430+ they may cause problems with some test runners.
1431+
1432+ * Some C functions (such as `stat`) tend to have multiple underlying entry
1433+ points in the C library which must be preloaded independently.
1434+
1435+ * You have to be careful about how your libraries are linked, because `ld
1436+ -Wl,-Bsymbolic-functions` prevents `LD_PRELOAD` working for intra-library
1437+ calls.
1438+
1439+ * `ctypes should return composite types from callbacks
1440+ <http://bugs.python.org/issue5710>`_. The least awful approach for now
1441+ seems to be to construct the composite type in question, stash a
1442+ reference to it forever, and then return a pointer to it as a void *; we
1443+ can only get away with this because tests are by nature relatively
1444+ short-lived.
1445+
1446+ * The ctypes module's handling of 64-bit pointers is basically just awful.
1447+ The right answer is probably to use a different callback-generation
1448+ framework entirely (maybe extending PyGObject so that we can get at the
1449+ pieces we need), but I've hacked around it for now.
1450+
1451+ * It doesn't appear to be possible to install mock replacements for
1452+ functions that are called directly from Python code using their GI
1453+ wrappers. You can work around this by simply patching the GI wrapper
1454+ instead, using `mock.patch`.
1455+
1456+I think the benefits, in terms of local clarity of tests, are worth the
1457+downsides.
1458+"""
1459+
1460+from __future__ import print_function
1461+
1462+__metaclass__ = type
1463+__all__ = ['GIMockTestCase']
1464+
1465+
1466+import contextlib
1467+import ctypes
1468+import fcntl
1469+from functools import partial
1470+import os
1471+import pickle
1472+import shutil
1473+import subprocess
1474+import sys
1475+import tempfile
1476+from textwrap import dedent
1477+import traceback
1478+import unittest
1479+try:
1480+ from unittest import mock
1481+except ImportError:
1482+ import mock
1483+try:
1484+ import xml.etree.cElementTree as etree
1485+except ImportError:
1486+ import xml.etree.ElementTree as etree
1487+
1488+from click.tests.gimock_types import Stat, Stat64
1489+
1490+
1491+# Borrowed from giscanner.girparser.
1492+CORE_NS = "http://www.gtk.org/introspection/core/1.0"
1493+C_NS = "http://www.gtk.org/introspection/c/1.0"
1494+GLIB_NS = "http://www.gtk.org/introspection/glib/1.0"
1495+
1496+
1497+def _corens(tag):
1498+ return '{%s}%s' % (CORE_NS, tag)
1499+
1500+
1501+def _glibns(tag):
1502+ return '{%s}%s' % (GLIB_NS, tag)
1503+
1504+
1505+def _cns(tag):
1506+ return '{%s}%s' % (C_NS, tag)
1507+
1508+
1509+# Override some c:type annotations that g-ir-scanner gets a bit wrong.
1510+_c_type_override = {
1511+ "passwd*": "struct passwd*",
1512+ "stat*": "struct stat*",
1513+ "stat64*": "struct stat64*",
1514+ }
1515+
1516+
1517+# Mapping of GI type name -> ctypes type.
1518+_typemap = {
1519+ "GError**": ctypes.c_void_p,
1520+ "gboolean": ctypes.c_bool,
1521+ "gint": ctypes.c_int,
1522+ "gint*": ctypes.POINTER(ctypes.c_int),
1523+ "gint32": ctypes.c_int32,
1524+ "gpointer": ctypes.c_void_p,
1525+ "guint": ctypes.c_uint,
1526+ "guint8**": ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)),
1527+ "guint32": ctypes.c_uint32,
1528+ "none": None,
1529+ "utf8": ctypes.c_char_p,
1530+ "utf8*": ctypes.POINTER(ctypes.c_char_p),
1531+ }
1532+
1533+
1534+class GIMockTestCase(unittest.TestCase):
1535+ def setUp(self):
1536+ super(GIMockTestCase, self).setUp()
1537+ self._gimock_temp_dir = tempfile.mkdtemp(prefix="gimock")
1538+ self.addCleanup(shutil.rmtree, self._gimock_temp_dir)
1539+ self._preload_func_refs = []
1540+ self._composite_refs = []
1541+ self._delegate_funcs = {}
1542+
1543+ def tearDown(self):
1544+ self._preload_func_refs = []
1545+ self._composite_refs = []
1546+ self._delegate_funcs = {}
1547+
1548+ def _gir_get_type(self, obj):
1549+ ret = {}
1550+ arrayinfo = obj.find(_corens("array"))
1551+ if arrayinfo is not None:
1552+ typeinfo = arrayinfo.find(_corens("type"))
1553+ raw_ctype = arrayinfo.get(_cns("type"))
1554+ else:
1555+ typeinfo = obj.find(_corens("type"))
1556+ raw_ctype = typeinfo.get(_cns("type"))
1557+ gi_type = typeinfo.get("name")
1558+ if obj.get("direction", "in") == "out":
1559+ gi_type += "*"
1560+ if arrayinfo is not None:
1561+ gi_type += "*"
1562+ ret["gi"] = gi_type
1563+ ret["c"] = _c_type_override.get(raw_ctype, raw_ctype)
1564+ return ret
1565+
1566+ def _parse_gir(self, path):
1567+ # A very, very crude GIR parser. We might have used
1568+ # giscanner.girparser, but it's not importable in Python 3 at the
1569+ # moment.
1570+ tree = etree.parse(path)
1571+ root = tree.getroot()
1572+ assert root.tag == _corens("repository")
1573+ assert root.get("version") == "1.2"
1574+ ns = root.find(_corens("namespace"))
1575+ assert ns is not None
1576+ funcs = {}
1577+ for func in ns.findall(_corens("function")):
1578+ name = func.get(_cns("identifier"))
1579+ # g-ir-scanner skips identifiers starting with "__", which we
1580+ # need in order to mock stat effectively. Work around this.
1581+ name = name.replace("under_under_", "__")
1582+ headers = None
1583+ for attr in func.findall(_corens("attribute")):
1584+ if attr.get("name") == "headers":
1585+ headers = attr.get("value")
1586+ break
1587+ rv = func.find(_corens("return-value"))
1588+ assert rv is not None
1589+ params = []
1590+ paramnode = func.find(_corens("parameters"))
1591+ if paramnode is not None:
1592+ for param in paramnode.findall(_corens("parameter")):
1593+ params.append({
1594+ "name": param.get("name"),
1595+ "type": self._gir_get_type(param),
1596+ })
1597+ if func.get("throws", "0") == "1":
1598+ params.append({
1599+ "name": "error",
1600+ "type": { "gi": "GError**", "c": "GError**" },
1601+ })
1602+ funcs[name] = {
1603+ "name": name,
1604+ "headers": headers,
1605+ "rv": self._gir_get_type(rv),
1606+ "params": params,
1607+ }
1608+ return funcs
1609+
1610+ def _ctypes_type(self, gi_type):
1611+ return _typemap[gi_type["gi"]]
1612+
1613+ def make_preloads(self, preloads):
1614+ rpreloads = []
1615+ std_headers = set([
1616+ "dlfcn.h",
1617+ # Not strictly needed, but convenient for ad-hoc debugging.
1618+ "stdio.h",
1619+ "stdint.h",
1620+ "stdlib.h",
1621+ "sys/types.h",
1622+ "unistd.h",
1623+ ])
1624+ preload_headers = set()
1625+ funcs = self._parse_gir("click/tests/preload.gir")
1626+ for name, func in preloads.items():
1627+ info = funcs[name]
1628+ rpreloads.append([info, func])
1629+ headers = info["headers"]
1630+ if headers is not None:
1631+ preload_headers.update(headers.split(","))
1632+ if "GIMOCK_SUBPROCESS" in os.environ:
1633+ return None, rpreloads
1634+ preloads_dir = os.path.join(self._gimock_temp_dir, "_preloads")
1635+ os.makedirs(preloads_dir)
1636+ c_path = os.path.join(preloads_dir, "gimockpreload.c")
1637+ with open(c_path, "w") as c:
1638+ print("#define _GNU_SOURCE", file=c)
1639+ for header in sorted(std_headers | preload_headers):
1640+ print("#include <%s>" % header, file=c)
1641+ print(file=c)
1642+ for info, _ in rpreloads:
1643+ conv = {}
1644+ conv["name"] = info["name"]
1645+ argtypes = [p["type"]["c"] for p in info["params"]]
1646+ argnames = [p["name"] for p in info["params"]]
1647+ conv["ret"] = info["rv"]["c"]
1648+ conv["bareproto"] = ", ".join(argtypes)
1649+ conv["proto"] = ", ".join(
1650+ "%s %s" % pair for pair in zip(argtypes, argnames))
1651+ conv["args"] = ", ".join(argnames)
1652+ # The delegation scheme used here is needed because trying
1653+ # to pass pointers back and forward through ctypes is a
1654+ # recipe for having them truncated to 32 bits at the drop of
1655+ # a hat. This approach is less obvious but much safer.
1656+ print(dedent("""\
1657+ typedef %(ret)s preloadtype_%(name)s (%(bareproto)s);
1658+ preloadtype_%(name)s *ctypes_%(name)s = (void *) 0;
1659+ preloadtype_%(name)s *real_%(name)s = (void *) 0;
1660+ static volatile int delegate_%(name)s = 0;
1661+
1662+ extern void _gimock_init_%(name)s (preloadtype_%(name)s *f)
1663+ {
1664+ ctypes_%(name)s = f;
1665+ if (! real_%(name)s) {
1666+ /* Retry lookup in case the symbol wasn't
1667+ * resolvable until the program under test was
1668+ * loaded.
1669+ */
1670+ dlerror ();
1671+ real_%(name)s = dlsym (RTLD_NEXT, \"%(name)s\");
1672+ if (dlerror ()) _exit (1);
1673+ }
1674+ }
1675+ """) % conv, file=c)
1676+ if conv["ret"] == "void":
1677+ print(dedent("""\
1678+ void %(name)s (%(proto)s)
1679+ {
1680+ if (ctypes_%(name)s) {
1681+ delegate_%(name)s = 0;
1682+ (*ctypes_%(name)s) (%(args)s);
1683+ if (! delegate_%(name)s)
1684+ return;
1685+ }
1686+ (*real_%(name)s) (%(args)s);
1687+ }
1688+ """) % conv, file=c)
1689+ else:
1690+ print(dedent("""\
1691+ %(ret)s %(name)s (%(proto)s)
1692+ {
1693+ if (ctypes_%(name)s) {
1694+ %(ret)s ret;
1695+ delegate_%(name)s = 0;
1696+ ret = (*ctypes_%(name)s) (%(args)s);
1697+ if (! delegate_%(name)s)
1698+ return ret;
1699+ }
1700+ return (*real_%(name)s) (%(args)s);
1701+ }
1702+ """) % conv, file=c)
1703+ print(dedent("""\
1704+ extern void _gimock_delegate_%(name)s (void)
1705+ {
1706+ delegate_%(name)s = 1;
1707+ }
1708+ """) % conv, file=c)
1709+ print(dedent("""\
1710+ static void __attribute__ ((constructor))
1711+ gimockpreload_init (void)
1712+ {
1713+ dlerror ();
1714+ """), file=c)
1715+ for info, _ in rpreloads:
1716+ name = info["name"]
1717+ print(" real_%s = dlsym (RTLD_NEXT, \"%s\");" %
1718+ (name, name), file=c)
1719+ print(" if (dlerror ()) _exit (1);", file=c)
1720+ print("}", file=c)
1721+ if "GIMOCK_PRELOAD_DEBUG" in os.environ:
1722+ with open(c_path) as c:
1723+ print(c.read())
1724+ # TODO: Use libtool or similar rather than hardcoding gcc invocation.
1725+ lib_path = os.path.join(preloads_dir, "libgimockpreload.so")
1726+ cflags = subprocess.check_output([
1727+ "pkg-config", "--cflags", "glib-2.0", "gee-0.8"],
1728+ universal_newlines=True).rstrip("\n").split()
1729+ subprocess.check_call([
1730+ "gcc", "-O0", "-g", "-shared", "-fPIC", "-DPIC", "-I", "lib/click",
1731+ ] + cflags + [
1732+ "-Wl,-soname", "-Wl,libgimockpreload.so",
1733+ c_path, "-ldl", "-o", lib_path,
1734+ ])
1735+ return lib_path, rpreloads
1736+
1737+ # Use as:
1738+ # with self.run_in_subprocess("func", ...) as (enter, preloads):
1739+ # enter()
1740+ # # test case body; preloads["func"] will be a mock.MagicMock
1741+ # # instance
1742+ @contextlib.contextmanager
1743+ def run_in_subprocess(self, *patches):
1744+ preloads = {}
1745+ for patch in patches:
1746+ preloads[patch] = mock.MagicMock()
1747+ if preloads:
1748+ lib_path, rpreloads = self.make_preloads(preloads)
1749+ else:
1750+ lib_path, rpreloads = None, None
1751+
1752+ class ParentProcess(Exception):
1753+ pass
1754+
1755+ def helper(lib_path, rpreloads):
1756+ if "GIMOCK_SUBPROCESS" in os.environ:
1757+ del os.environ["LD_PRELOAD"]
1758+ preload_lib = ctypes.cdll.LoadLibrary(lib_path)
1759+ delegate_cfunctype = ctypes.CFUNCTYPE(None)
1760+ for info, func in rpreloads:
1761+ signature = [info["rv"]] + [
1762+ p["type"] for p in info["params"]]
1763+ signature = [self._ctypes_type(t) for t in signature]
1764+ cfunctype = ctypes.CFUNCTYPE(*signature)
1765+ init = getattr(
1766+ preload_lib, "_gimock_init_%s" % info["name"])
1767+ cfunc = cfunctype(func)
1768+ self._preload_func_refs.append(cfunc)
1769+ init(cfunc)
1770+ delegate = getattr(
1771+ preload_lib, "_gimock_delegate_%s" % info["name"])
1772+ self._delegate_funcs[info["name"]] = delegate_cfunctype(
1773+ delegate)
1774+ return
1775+ rfd, wfd = os.pipe()
1776+ # It would be cleaner to use subprocess.Popen(pass_fds=[wfd]), but
1777+ # that isn't available in Python 2.7.
1778+ if hasattr(os, "set_inheritable"):
1779+ os.set_inheritable(wfd, True)
1780+ else:
1781+ fcntl.fcntl(rfd, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
1782+ args = [
1783+ sys.executable, "-m", "unittest",
1784+ "%s.%s.%s" % (
1785+ self.__class__.__module__, self.__class__.__name__,
1786+ self._testMethodName)]
1787+ env = os.environ.copy()
1788+ env["GIMOCK_SUBPROCESS"] = str(wfd)
1789+ if lib_path is not None:
1790+ env["LD_PRELOAD"] = lib_path
1791+ subp = subprocess.Popen(args, close_fds=False, env=env)
1792+ os.close(wfd)
1793+ reader = os.fdopen(rfd, "rb")
1794+ subp.communicate()
1795+ exctype = pickle.load(reader)
1796+ if exctype is not None and issubclass(exctype, AssertionError):
1797+ raise AssertionError("Subprocess failed a test!")
1798+ elif exctype is not None or subp.returncode != 0:
1799+ raise Exception("Subprocess returned an error!")
1800+ reader.close()
1801+ raise ParentProcess()
1802+
1803+ try:
1804+ yield partial(helper, lib_path, rpreloads), preloads
1805+ if "GIMOCK_SUBPROCESS" in os.environ:
1806+ wfd = int(os.environ["GIMOCK_SUBPROCESS"])
1807+ writer = os.fdopen(wfd, "wb")
1808+ pickle.dump(None, writer)
1809+ writer.flush()
1810+ os._exit(0)
1811+ except ParentProcess:
1812+ pass
1813+ except Exception as e:
1814+ if "GIMOCK_SUBPROCESS" in os.environ:
1815+ wfd = int(os.environ["GIMOCK_SUBPROCESS"])
1816+ writer = os.fdopen(wfd, "wb")
1817+ # It would be better to use tblib to pickle the traceback so
1818+ # that we can re-raise it properly from the parent process.
1819+ # Until that's packaged and available to us, just print the
1820+ # traceback and send the exception type.
1821+ print()
1822+ traceback.print_exc()
1823+ pickle.dump(type(e), writer)
1824+ writer.flush()
1825+ os._exit(1)
1826+ else:
1827+ raise
1828+
1829+ def make_pointer(self, composite):
1830+ # Store a reference to a composite type and return a pointer to it,
1831+ # working around http://bugs.python.org/issue5710.
1832+ self._composite_refs.append(composite)
1833+ return ctypes.addressof(composite)
1834+
1835+ def make_string(self, s):
1836+ # As make_pointer, but for a string.
1837+ copied = ctypes.create_string_buffer(s.encode())
1838+ self._composite_refs.append(copied)
1839+ return ctypes.addressof(copied)
1840+
1841+ def convert_pointer(self, composite_type, address):
1842+ # Return a ctypes composite type instance at a given address.
1843+ return composite_type.from_address(address)
1844+
1845+ def convert_stat_pointer(self, name, address):
1846+ # As convert_pointer, but for a "struct stat *" or "struct stat64 *"
1847+ # depending on the wrapped function name.
1848+ stat_type = {"__xstat": Stat, "__xstat64": Stat64}
1849+ return self.convert_pointer(stat_type[name], address)
1850+
1851+ def delegate_to_original(self, name):
1852+ # Cause the wrapper function to delegate to the original version
1853+ # after the callback returns. (Note that the callback still needs
1854+ # to return something type-compatible with the declared result type,
1855+ # although the return value will otherwise be ignored.)
1856+ self._delegate_funcs[name]()
1857
1858=== added file 'click/tests/gimock_types.py'
1859--- click/tests/gimock_types.py 1970-01-01 00:00:00 +0000
1860+++ click/tests/gimock_types.py 2014-03-06 07:04:44 +0000
1861@@ -0,0 +1,89 @@
1862+# Copyright (C) 2014 Canonical Ltd.
1863+# Author: Colin Watson <cjwatson@ubuntu.com>
1864+
1865+# This program is free software: you can redistribute it and/or modify
1866+# it under the terms of the GNU General Public License as published by
1867+# the Free Software Foundation; version 3 of the License.
1868+#
1869+# This program is distributed in the hope that it will be useful,
1870+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1871+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1872+# GNU General Public License for more details.
1873+#
1874+# You should have received a copy of the GNU General Public License
1875+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1876+
1877+"""A collection of variously hacky ctypes definitions for use with gimock."""
1878+
1879+import ctypes
1880+
1881+from click.tests.config import (
1882+ STAT_OFFSET_GID,
1883+ STAT_OFFSET_UID,
1884+ STAT64_OFFSET_GID,
1885+ STAT64_OFFSET_UID,
1886+ )
1887+
1888+
1889+class Passwd(ctypes.Structure):
1890+ _fields_ = [
1891+ ("pw_name", ctypes.c_char_p),
1892+ ("pw_passwd", ctypes.c_char_p),
1893+ ("pw_uid", ctypes.c_uint32),
1894+ ("pw_gid", ctypes.c_uint32),
1895+ ("pw_gecos", ctypes.c_char_p),
1896+ ("pw_dir", ctypes.c_char_p),
1897+ ("pw_shell", ctypes.c_char_p),
1898+ ]
1899+
1900+
1901+# TODO: This is pretty awful. The layout of "struct stat" is complicated
1902+# enough that we have to use offsetof() in configure to pick out the fields
1903+# we care about. Fortunately, we only care about a couple of fields, and
1904+# since this is an output parameter it doesn't matter if our structure is
1905+# too short (if we cared about this then we could use AC_CHECK_SIZEOF to
1906+# figure it out).
1907+class Stat(ctypes.Structure):
1908+ _pack_ = 1
1909+ _fields_ = []
1910+ _fields_.append(
1911+ ("pad0", ctypes.c_ubyte * min(STAT_OFFSET_UID, STAT_OFFSET_GID)))
1912+ if STAT_OFFSET_UID < STAT_OFFSET_GID:
1913+ _fields_.append(("st_uid", ctypes.c_uint32))
1914+ pad = (STAT_OFFSET_GID - STAT_OFFSET_UID -
1915+ ctypes.sizeof(ctypes.c_uint32))
1916+ assert pad >= 0
1917+ if pad > 0:
1918+ _fields_.append(("pad1", ctypes.c_ubyte * pad))
1919+ _fields_.append(("st_gid", ctypes.c_uint32))
1920+ else:
1921+ _fields_.append(("st_gid", ctypes.c_uint32))
1922+ pad = (STAT_OFFSET_UID - STAT_OFFSET_GID -
1923+ ctypes.sizeof(ctypes.c_uint32))
1924+ assert pad >= 0
1925+ if pad > 0:
1926+ _fields_.append(("pad1", ctypes.c_ubyte * pad))
1927+ _fields_.append(("st_uid", ctypes.c_uint32))
1928+
1929+
1930+class Stat64(ctypes.Structure):
1931+ _pack_ = 1
1932+ _fields_ = []
1933+ _fields_.append(
1934+ ("pad0", ctypes.c_ubyte * min(STAT64_OFFSET_UID, STAT64_OFFSET_GID)))
1935+ if STAT64_OFFSET_UID < STAT64_OFFSET_GID:
1936+ _fields_.append(("st_uid", ctypes.c_uint32))
1937+ pad = (STAT64_OFFSET_GID - STAT64_OFFSET_UID -
1938+ ctypes.sizeof(ctypes.c_uint32))
1939+ assert pad >= 0
1940+ if pad > 0:
1941+ _fields_.append(("pad1", ctypes.c_ubyte * pad))
1942+ _fields_.append(("st_gid", ctypes.c_uint32))
1943+ else:
1944+ _fields_.append(("st_gid", ctypes.c_uint32))
1945+ pad = (STAT64_OFFSET_UID - STAT64_OFFSET_GID -
1946+ ctypes.sizeof(ctypes.c_uint32))
1947+ assert pad >= 0
1948+ if pad > 0:
1949+ _fields_.append(("pad1", ctypes.c_ubyte * pad))
1950+ _fields_.append(("st_uid", ctypes.c_uint32))
1951
1952=== modified file 'click/tests/helpers.py'
1953--- click/tests/helpers.py 2014-03-01 23:28:24 +0000
1954+++ click/tests/helpers.py 2014-03-06 07:04:44 +0000
1955@@ -39,10 +39,12 @@
1956 except ImportError:
1957 import mock
1958
1959-from click import osextras
1960-
1961-
1962-class TestCase(unittest.TestCase):
1963+from gi.repository import Click, GLib
1964+
1965+from click.tests import gimock
1966+
1967+
1968+class TestCase(gimock.GIMockTestCase):
1969 def setUp(self):
1970 super(TestCase, self).setUp()
1971 self.temp_dir = None
1972@@ -74,6 +76,33 @@
1973 if not hasattr(unittest.TestCase, 'assertRaisesRegex'):
1974 assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
1975
1976+ def assertRaisesGError(self, domain_name, code, callableObj,
1977+ *args, **kwargs):
1978+ with self.assertRaises(GLib.GError) as cm:
1979+ callableObj(*args, **kwargs)
1980+ self.assertEqual(domain_name, cm.exception.domain)
1981+ self.assertEqual(code, cm.exception.code)
1982+
1983+ def assertRaisesFileError(self, code, callableObj, *args, **kwargs):
1984+ self.assertRaisesGError(
1985+ "g-file-error-quark", code, callableObj, *args, **kwargs)
1986+
1987+ def assertRaisesDatabaseError(self, code, callableObj, *args, **kwargs):
1988+ self.assertRaisesGError(
1989+ "click_database_error-quark", code, callableObj, *args, **kwargs)
1990+
1991+ def assertRaisesHooksError(self, code, callableObj, *args, **kwargs):
1992+ self.assertRaisesGError(
1993+ "click_hooks_error-quark", code, callableObj, *args, **kwargs)
1994+
1995+ def assertRaisesQueryError(self, code, callableObj, *args, **kwargs):
1996+ self.assertRaisesGError(
1997+ "click_query_error-quark", code, callableObj, *args, **kwargs)
1998+
1999+ def assertRaisesUserError(self, code, callableObj, *args, **kwargs):
2000+ self.assertRaisesGError(
2001+ "click_user_error-quark", code, callableObj, *args, **kwargs)
2002+
2003
2004 if not hasattr(mock, "call"):
2005 # mock 0.7.2, the version in Ubuntu 12.04 LTS, lacks mock.ANY and
2006@@ -228,14 +257,14 @@
2007
2008 @contextlib.contextmanager
2009 def mkfile(path, mode="w"):
2010- osextras.ensuredir(os.path.dirname(path))
2011+ Click.ensuredir(os.path.dirname(path))
2012 with open(path, mode) as f:
2013 yield f
2014
2015
2016 @contextlib.contextmanager
2017 def mkfile_utf8(path, mode="w"):
2018- osextras.ensuredir(os.path.dirname(path))
2019+ Click.ensuredir(os.path.dirname(path))
2020 if sys.version < "3":
2021 import codecs
2022 with codecs.open(path, mode, "UTF-8") as f:
2023
2024=== added file 'click/tests/preload.h'
2025--- click/tests/preload.h 1970-01-01 00:00:00 +0000
2026+++ click/tests/preload.h 2014-03-06 07:04:44 +0000
2027@@ -0,0 +1,110 @@
2028+#include <sys/stat.h>
2029+#include <sys/types.h>
2030+
2031+#include <glib.h>
2032+
2033+#include "click.h"
2034+
2035+/**
2036+ * chown:
2037+ *
2038+ * Attributes: (headers unistd.h)
2039+ */
2040+extern int chown (const char *file, uid_t owner, gid_t group);
2041+
2042+/* Workaround for g-ir-scanner not picking up the type properly: mode_t is
2043+ * uint32_t on all glibc platforms.
2044+ */
2045+/**
2046+ * mkdir:
2047+ * @mode: (type guint32)
2048+ *
2049+ * Attributes: (headers sys/stat.h,sys/types.h)
2050+ */
2051+extern int mkdir (const char *pathname, mode_t mode);
2052+
2053+/**
2054+ * getpwnam:
2055+ *
2056+ * Attributes: (headers sys/types.h,pwd.h)
2057+ * Returns: (transfer none):
2058+ */
2059+extern struct passwd *getpwnam (const char *name);
2060+
2061+/**
2062+ * under_under_xstat:
2063+ *
2064+ * Attributes: (headers sys/types.h,sys/stat.h,unistd.h)
2065+ */
2066+extern int under_under_xstat (int ver, const char *pathname, struct stat *buf);
2067+
2068+/**
2069+ * under_under_xstat64:
2070+ *
2071+ * Attributes: (headers sys/types.h,sys/stat.h,unistd.h)
2072+ */
2073+extern int under_under_xstat64 (int ver, const char *pathname, struct stat64 *buf);
2074+
2075+const gchar *g_get_user_name (void);
2076+
2077+/**
2078+ * g_spawn_sync:
2079+ * @argv: (array zero-terminated=1):
2080+ * @envp: (array zero-terminated=1):
2081+ * @flags: (type gint)
2082+ * @child_setup: (type gpointer)
2083+ * @standard_output: (out) (array zero-terminated=1) (element-type guint8):
2084+ * @standard_error: (out) (array zero-terminated=1) (element-type guint8):
2085+ * @exit_status: (out):
2086+ *
2087+ * Attributes: (headers glib.h)
2088+ */
2089+gboolean g_spawn_sync (const gchar *working_directory,
2090+ gchar **argv,
2091+ gchar **envp,
2092+ GSpawnFlags flags,
2093+ GSpawnChildSetupFunc child_setup,
2094+ gpointer user_data,
2095+ gchar **standard_output,
2096+ gchar **standard_error,
2097+ gint *exit_status,
2098+ GError **error);
2099+
2100+/**
2101+ * click_find_on_path:
2102+ *
2103+ * Attributes: (headers glib.h)
2104+ */
2105+gboolean click_find_on_path (const gchar *command);
2106+
2107+/**
2108+ * click_get_db_dir:
2109+ *
2110+ * Attributes: (headers glib.h)
2111+ */
2112+gchar *click_get_db_dir (void);
2113+
2114+/**
2115+ * click_get_hooks_dir:
2116+ *
2117+ * Attributes: (headers glib.h)
2118+ */
2119+gchar *click_get_hooks_dir (void);
2120+
2121+/**
2122+ * click_get_user_home:
2123+ *
2124+ * Attributes: (headers glib.h)
2125+ */
2126+gchar *click_get_user_home (const gchar *user_name);
2127+
2128+/**
2129+ * click_package_install_hooks:
2130+ * @db: (type gpointer)
2131+ *
2132+ * Attributes: (headers glib.h,click.h)
2133+ */
2134+void click_package_install_hooks (ClickDB *db, const gchar *package,
2135+ const gchar *old_version,
2136+ const gchar *new_version,
2137+ const gchar *user_name, GError **error);
2138
2139=== modified file 'click/tests/test_database.py'
2140--- click/tests/test_database.py 2013-12-10 14:33:19 +0000
2141+++ click/tests/test_database.py 2014-03-06 07:04:44 +0000
2142@@ -24,45 +24,46 @@
2143 ]
2144
2145
2146+from functools import partial
2147+from itertools import takewhile
2148 import json
2149 import os
2150
2151-from click.database import ClickDB
2152-from click.tests.helpers import TestCase, mkfile, mock, touch
2153-
2154-
2155-class MockPasswd:
2156- def __init__(self, pw_uid, pw_gid):
2157- self.pw_uid = pw_uid
2158- self.pw_gid = pw_gid
2159-
2160-
2161-class MockStatResult:
2162- original_stat = os.stat
2163-
2164- def __init__(self, path, **override):
2165- self.st = self.original_stat(path)
2166- self.override = override
2167-
2168- def __getattr__(self, name):
2169- if name in self.override:
2170- return self.override[name]
2171- else:
2172- return getattr(self.st, name)
2173+from gi.repository import Click
2174+
2175+from click.tests.gimock_types import Passwd
2176+from click.tests.helpers import TestCase, mkfile, touch
2177
2178
2179 class TestClickSingleDB(TestCase):
2180 def setUp(self):
2181 super(TestClickSingleDB, self).setUp()
2182 self.use_temp_dir()
2183- self.master_db = ClickDB(extra_root=self.temp_dir, use_system=False)
2184- self.db = self.master_db._db[-1]
2185+ self.master_db = Click.DB()
2186+ self.master_db.add(self.temp_dir)
2187+ self.db = self.master_db.get(self.master_db.props.size - 1)
2188+ self.spawn_calls = []
2189+
2190+ def g_spawn_sync_side_effect(self, status_map, working_directory, argv,
2191+ envp, flags, child_setup, user_data,
2192+ standard_output, standard_error, exit_status,
2193+ error):
2194+ self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv)))
2195+ if argv[0] in status_map:
2196+ exit_status[0] = status_map[argv[0]]
2197+ else:
2198+ self.delegate_to_original("g_spawn_sync")
2199+ return 0
2200+
2201+ def _installed_packages_tuplify(self, ip):
2202+ return [(p.props.package, p.props.version, p.props.path) for p in ip]
2203
2204 def test_path(self):
2205 path = os.path.join(self.temp_dir, "a", "1.0")
2206 os.makedirs(path)
2207- self.assertEqual(path, self.db.path("a", "1.0"))
2208- self.assertRaises(KeyError, self.db.path, "a", "1.1")
2209+ self.assertEqual(path, self.db.get_path("a", "1.0"))
2210+ self.assertRaisesDatabaseError(
2211+ Click.DatabaseError.DOES_NOT_EXIST, self.db.get_path, "a", "1.1")
2212
2213 def test_packages_current(self):
2214 os.makedirs(os.path.join(self.temp_dir, "a", "1.0"))
2215@@ -76,7 +77,8 @@
2216 self.assertEqual([
2217 ("a", "1.1", a_current),
2218 ("b", "0.1", b_current),
2219- ], list(self.db.packages()))
2220+ ], self._installed_packages_tuplify(
2221+ self.db.get_packages(all_versions=False)))
2222
2223 def test_packages_all(self):
2224 os.makedirs(os.path.join(self.temp_dir, "a", "1.0"))
2225@@ -90,120 +92,145 @@
2226 ("a", "1.1", os.path.join(self.temp_dir, "a", "1.1")),
2227 ("b", "0.1", os.path.join(self.temp_dir, "b", "0.1")),
2228 ("c", "2.0", os.path.join(self.temp_dir, "c", "2.0")),
2229- ], list(self.db.packages(all_versions=True)))
2230-
2231- @mock.patch("subprocess.call")
2232- def test_app_running(self, mock_call):
2233- mock_call.return_value = 0
2234- self.assertTrue(self.db._app_running("foo", "bar", "1.0"))
2235- mock_call.assert_called_once_with(
2236- ["upstart-app-pid", "foo_bar_1.0"], stdout=mock.ANY)
2237- mock_call.return_value = 1
2238- self.assertFalse(self.db._app_running("foo", "bar", "1.0"))
2239-
2240- @mock.patch("click.osextras.find_on_path")
2241- @mock.patch("subprocess.call")
2242- def test_any_app_running(self, mock_call, mock_find_on_path):
2243- manifest_path = os.path.join(
2244- self.temp_dir, "a", "1.0", ".click", "info", "a.manifest")
2245- with mkfile(manifest_path) as manifest:
2246- json.dump({"hooks": {"a-app": {}}}, manifest)
2247- mock_call.return_value = 0
2248- mock_find_on_path.return_value = False
2249- self.assertFalse(self.db._any_app_running("a", "1.0"))
2250- mock_find_on_path.return_value = True
2251- self.assertTrue(self.db._any_app_running("a", "1.0"))
2252- mock_call.assert_called_once_with(
2253- ["upstart-app-pid", "a_a-app_1.0"], stdout=mock.ANY)
2254- mock_call.return_value = 1
2255- self.assertFalse(self.db._any_app_running("a", "1.0"))
2256-
2257- @mock.patch("click.osextras.find_on_path")
2258- @mock.patch("subprocess.call")
2259- def test_maybe_remove_registered(self, mock_call, mock_find_on_path):
2260- version_path = os.path.join(self.temp_dir, "a", "1.0")
2261- manifest_path = os.path.join(
2262- version_path, ".click", "info", "a.manifest")
2263- with mkfile(manifest_path) as manifest:
2264- json.dump({"hooks": {"a-app": {}}}, manifest)
2265- user_path = os.path.join(
2266- self.temp_dir, ".click", "users", "test-user", "a")
2267- os.makedirs(os.path.dirname(user_path))
2268- os.symlink(version_path, user_path)
2269- mock_call.return_value = 0
2270- mock_find_on_path.return_value = True
2271- self.db.maybe_remove("a", "1.0")
2272- self.assertTrue(os.path.exists(version_path))
2273- self.assertTrue(os.path.exists(user_path))
2274-
2275- @mock.patch("click.osextras.find_on_path")
2276- @mock.patch("subprocess.call")
2277- def test_maybe_remove_running(self, mock_call, mock_find_on_path):
2278- version_path = os.path.join(self.temp_dir, "a", "1.0")
2279- manifest_path = os.path.join(
2280- version_path, ".click", "info", "a.manifest")
2281- with mkfile(manifest_path) as manifest:
2282- json.dump({"hooks": {"a-app": {}}}, manifest)
2283- mock_call.return_value = 0
2284- mock_find_on_path.return_value = True
2285- self.db.maybe_remove("a", "1.0")
2286- gcinuse_path = os.path.join(
2287- self.temp_dir, ".click", "users", "@gcinuse", "a")
2288- self.assertTrue(os.path.islink(gcinuse_path))
2289- self.assertEqual(version_path, os.readlink(gcinuse_path))
2290- self.assertTrue(os.path.exists(version_path))
2291- self.db.maybe_remove("a", "1.0")
2292- self.assertTrue(os.path.islink(gcinuse_path))
2293- self.assertEqual(version_path, os.readlink(gcinuse_path))
2294- self.assertTrue(os.path.exists(version_path))
2295-
2296- @mock.patch("click.osextras.find_on_path")
2297- @mock.patch("subprocess.call")
2298- def test_maybe_remove_not_running(self, mock_call, mock_find_on_path):
2299- version_path = os.path.join(self.temp_dir, "a", "1.0")
2300- manifest_path = os.path.join(
2301- version_path, ".click", "info", "a.manifest")
2302- with mkfile(manifest_path) as manifest:
2303- json.dump({"hooks": {"a-app": {}}}, manifest)
2304- current_path = os.path.join(self.temp_dir, "a", "current")
2305- os.symlink("1.0", current_path)
2306- mock_call.return_value = 1
2307- mock_find_on_path.return_value = True
2308- self.db.maybe_remove("a", "1.0")
2309- gcinuse_path = os.path.join(
2310- self.temp_dir, ".click", "users", "@gcinuse", "a")
2311- self.assertFalse(os.path.islink(gcinuse_path))
2312- self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a")))
2313-
2314- @mock.patch("click.osextras.find_on_path")
2315- @mock.patch("subprocess.call")
2316- def test_gc(self, mock_call, mock_find_on_path):
2317- a_path = os.path.join(self.temp_dir, "a", "1.0")
2318- a_manifest_path = os.path.join(a_path, ".click", "info", "a.manifest")
2319- with mkfile(a_manifest_path) as manifest:
2320- json.dump({"hooks": {"a-app": {}}}, manifest)
2321- b_path = os.path.join(self.temp_dir, "b", "1.0")
2322- b_manifest_path = os.path.join(b_path, ".click", "info", "b.manifest")
2323- with mkfile(b_manifest_path) as manifest:
2324- json.dump({"hooks": {"b-app": {}}}, manifest)
2325- c_path = os.path.join(self.temp_dir, "c", "1.0")
2326- c_manifest_path = os.path.join(c_path, ".click", "info", "c.manifest")
2327- with mkfile(c_manifest_path) as manifest:
2328- json.dump({"hooks": {"c-app": {}}}, manifest)
2329- a_user_path = os.path.join(
2330- self.temp_dir, ".click", "users", "test-user", "a")
2331- os.makedirs(os.path.dirname(a_user_path))
2332- os.symlink(a_path, a_user_path)
2333- b_gcinuse_path = os.path.join(
2334- self.temp_dir, ".click", "users", "@gcinuse", "b")
2335- os.makedirs(os.path.dirname(b_gcinuse_path))
2336- os.symlink(b_path, b_gcinuse_path)
2337- mock_call.return_value = 1
2338- mock_find_on_path.return_value = True
2339- self.db.gc(verbose=False)
2340- self.assertTrue(os.path.exists(a_path))
2341- self.assertFalse(os.path.exists(b_path))
2342- self.assertTrue(os.path.exists(c_path))
2343+ ], self._installed_packages_tuplify(
2344+ self.db.get_packages(all_versions=True)))
2345+
2346+ def test_app_running(self):
2347+ with self.run_in_subprocess("g_spawn_sync") as (enter, preloads):
2348+ enter()
2349+ preloads["g_spawn_sync"].side_effect = partial(
2350+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
2351+ self.assertTrue(self.db.app_running("foo", "bar", "1.0"))
2352+ self.assertEqual(
2353+ [[b"upstart-app-pid", b"foo_bar_1.0"]], self.spawn_calls)
2354+ preloads["g_spawn_sync"].side_effect = partial(
2355+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
2356+ self.assertFalse(self.db.app_running("foo", "bar", "1.0"))
2357+
2358+ def test_any_app_running(self):
2359+ with self.run_in_subprocess(
2360+ "click_find_on_path", "g_spawn_sync",
2361+ ) as (enter, preloads):
2362+ enter()
2363+ manifest_path = os.path.join(
2364+ self.temp_dir, "a", "1.0", ".click", "info", "a.manifest")
2365+ with mkfile(manifest_path) as manifest:
2366+ json.dump({"hooks": {"a-app": {}}}, manifest)
2367+ preloads["g_spawn_sync"].side_effect = partial(
2368+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
2369+ preloads["click_find_on_path"].return_value = False
2370+ self.assertFalse(self.db.any_app_running("a", "1.0"))
2371+ preloads["click_find_on_path"].return_value = True
2372+ self.assertTrue(self.db.any_app_running("a", "1.0"))
2373+ self.assertEqual(
2374+ [[b"upstart-app-pid", b"a_a-app_1.0"]], self.spawn_calls)
2375+ preloads["g_spawn_sync"].side_effect = partial(
2376+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
2377+ self.assertFalse(self.db.any_app_running("a", "1.0"))
2378+
2379+ def test_maybe_remove_registered(self):
2380+ with self.run_in_subprocess(
2381+ "click_find_on_path", "g_spawn_sync",
2382+ ) as (enter, preloads):
2383+ enter()
2384+ version_path = os.path.join(self.temp_dir, "a", "1.0")
2385+ manifest_path = os.path.join(
2386+ version_path, ".click", "info", "a.manifest")
2387+ with mkfile(manifest_path) as manifest:
2388+ json.dump({"hooks": {"a-app": {}}}, manifest)
2389+ user_path = os.path.join(
2390+ self.temp_dir, ".click", "users", "test-user", "a")
2391+ os.makedirs(os.path.dirname(user_path))
2392+ os.symlink(version_path, user_path)
2393+ preloads["g_spawn_sync"].side_effect = partial(
2394+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
2395+ preloads["click_find_on_path"].return_value = True
2396+ self.db.maybe_remove("a", "1.0")
2397+ self.assertTrue(os.path.exists(version_path))
2398+ self.assertTrue(os.path.exists(user_path))
2399+
2400+ def test_maybe_remove_running(self):
2401+ with self.run_in_subprocess(
2402+ "click_find_on_path", "g_spawn_sync",
2403+ ) as (enter, preloads):
2404+ enter()
2405+ version_path = os.path.join(self.temp_dir, "a", "1.0")
2406+ manifest_path = os.path.join(
2407+ version_path, ".click", "info", "a.manifest")
2408+ with mkfile(manifest_path) as manifest:
2409+ json.dump({"hooks": {"a-app": {}}}, manifest)
2410+ preloads["g_spawn_sync"].side_effect = partial(
2411+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
2412+ preloads["click_find_on_path"].return_value = True
2413+ self.db.maybe_remove("a", "1.0")
2414+ gcinuse_path = os.path.join(
2415+ self.temp_dir, ".click", "users", "@gcinuse", "a")
2416+ self.assertTrue(os.path.islink(gcinuse_path))
2417+ self.assertEqual(version_path, os.readlink(gcinuse_path))
2418+ self.assertTrue(os.path.exists(version_path))
2419+ self.db.maybe_remove("a", "1.0")
2420+ self.assertTrue(os.path.islink(gcinuse_path))
2421+ self.assertEqual(version_path, os.readlink(gcinuse_path))
2422+ self.assertTrue(os.path.exists(version_path))
2423+
2424+ def test_maybe_remove_not_running(self):
2425+ with self.run_in_subprocess(
2426+ "click_find_on_path", "g_spawn_sync",
2427+ ) as (enter, preloads):
2428+ enter()
2429+ os.environ["TEST_QUIET"] = "1"
2430+ version_path = os.path.join(self.temp_dir, "a", "1.0")
2431+ manifest_path = os.path.join(
2432+ version_path, ".click", "info", "a.manifest")
2433+ with mkfile(manifest_path) as manifest:
2434+ json.dump({"hooks": {"a-app": {}}}, manifest)
2435+ current_path = os.path.join(self.temp_dir, "a", "current")
2436+ os.symlink("1.0", current_path)
2437+ preloads["g_spawn_sync"].side_effect = partial(
2438+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
2439+ preloads["click_find_on_path"].return_value = True
2440+ self.db.maybe_remove("a", "1.0")
2441+ gcinuse_path = os.path.join(
2442+ self.temp_dir, ".click", "users", "@gcinuse", "a")
2443+ self.assertFalse(os.path.islink(gcinuse_path))
2444+ self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a")))
2445+
2446+ def test_gc(self):
2447+ with self.run_in_subprocess(
2448+ "click_find_on_path", "g_spawn_sync",
2449+ ) as (enter, preloads):
2450+ enter()
2451+ os.environ["TEST_QUIET"] = "1"
2452+ a_path = os.path.join(self.temp_dir, "a", "1.0")
2453+ a_manifest_path = os.path.join(
2454+ a_path, ".click", "info", "a.manifest")
2455+ with mkfile(a_manifest_path) as manifest:
2456+ json.dump({"hooks": {"a-app": {}}}, manifest)
2457+ b_path = os.path.join(self.temp_dir, "b", "1.0")
2458+ b_manifest_path = os.path.join(
2459+ b_path, ".click", "info", "b.manifest")
2460+ with mkfile(b_manifest_path) as manifest:
2461+ json.dump({"hooks": {"b-app": {}}}, manifest)
2462+ c_path = os.path.join(self.temp_dir, "c", "1.0")
2463+ c_manifest_path = os.path.join(
2464+ c_path, ".click", "info", "c.manifest")
2465+ with mkfile(c_manifest_path) as manifest:
2466+ json.dump({"hooks": {"c-app": {}}}, manifest)
2467+ a_user_path = os.path.join(
2468+ self.temp_dir, ".click", "users", "test-user", "a")
2469+ os.makedirs(os.path.dirname(a_user_path))
2470+ os.symlink(a_path, a_user_path)
2471+ b_gcinuse_path = os.path.join(
2472+ self.temp_dir, ".click", "users", "@gcinuse", "b")
2473+ os.makedirs(os.path.dirname(b_gcinuse_path))
2474+ os.symlink(b_path, b_gcinuse_path)
2475+ preloads["g_spawn_sync"].side_effect = partial(
2476+ self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
2477+ preloads["click_find_on_path"].return_value = True
2478+ self.db.gc()
2479+ self.assertTrue(os.path.exists(a_path))
2480+ self.assertFalse(os.path.exists(b_path))
2481+ self.assertTrue(os.path.exists(c_path))
2482
2483 def _make_ownership_test(self):
2484 path = os.path.join(self.temp_dir, "a", "1.0")
2485@@ -215,60 +242,80 @@
2486 os.symlink(path, user_path)
2487 touch(os.path.join(self.temp_dir, ".click", "log"))
2488
2489- def test_clickpkg_paths(self):
2490- self._make_ownership_test()
2491- self.assertCountEqual([
2492- self.temp_dir,
2493- os.path.join(self.temp_dir, ".click"),
2494- os.path.join(self.temp_dir, ".click", "log"),
2495- os.path.join(self.temp_dir, ".click", "users"),
2496- os.path.join(self.temp_dir, "a"),
2497- os.path.join(self.temp_dir, "a", "1.0"),
2498- os.path.join(self.temp_dir, "a", "1.0", ".click"),
2499- os.path.join(self.temp_dir, "a", "1.0", ".click", "info"),
2500- os.path.join(
2501- self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"),
2502- os.path.join(self.temp_dir, "a", "current"),
2503- ], list(self.db._clickpkg_paths()))
2504-
2505- @mock.patch("pwd.getpwnam")
2506- @mock.patch("os.chown")
2507- def test_ensure_ownership_quick_if_correct(self, mock_chown,
2508- mock_getpwnam):
2509- mock_getpwnam.return_value = MockPasswd(pw_uid=1, pw_gid=1)
2510- self._make_ownership_test()
2511- with mock.patch("os.stat") as mock_stat:
2512- mock_stat.side_effect = (
2513- lambda path, *args, **kwargs: MockStatResult(
2514- path, st_uid=1, st_gid=1))
2515- self.db.ensure_ownership()
2516- self.assertFalse(mock_chown.called)
2517-
2518- @mock.patch("pwd.getpwnam")
2519- @mock.patch("os.chown")
2520- def test_ensure_ownership(self, mock_chown, mock_getpwnam):
2521- mock_getpwnam.return_value = MockPasswd(pw_uid=1, pw_gid=1)
2522- self._make_ownership_test()
2523- with mock.patch("os.stat") as mock_stat:
2524- mock_stat.side_effect = (
2525- lambda path, *args, **kwargs: MockStatResult(
2526- path, st_uid=2, st_gid=2))
2527- self.db.ensure_ownership()
2528- self.assertCountEqual([
2529- self.temp_dir,
2530- os.path.join(self.temp_dir, ".click"),
2531- os.path.join(self.temp_dir, ".click", "log"),
2532- os.path.join(self.temp_dir, ".click", "users"),
2533- os.path.join(self.temp_dir, "a"),
2534- os.path.join(self.temp_dir, "a", "1.0"),
2535- os.path.join(self.temp_dir, "a", "1.0", ".click"),
2536- os.path.join(self.temp_dir, "a", "1.0", ".click", "info"),
2537- os.path.join(
2538- self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"),
2539- os.path.join(self.temp_dir, "a", "current"),
2540- ], [args[0][0] for args in mock_chown.call_args_list])
2541- self.assertCountEqual(
2542- [(1, 1)], set(args[0][1:] for args in mock_chown.call_args_list))
2543+ def _set_stat_side_effect(self, preloads, side_effect, limit):
2544+ limit = limit.encode()
2545+ preloads["__xstat"].side_effect = (
2546+ lambda ver, path, buf: side_effect(
2547+ "__xstat", limit, ver, path, buf))
2548+ preloads["__xstat64"].side_effect = (
2549+ lambda ver, path, buf: side_effect(
2550+ "__xstat64", limit, ver, path, buf))
2551+
2552+ def test_ensure_ownership_quick_if_correct(self):
2553+ def stat_side_effect(name, limit, ver, path, buf):
2554+ st = self.convert_stat_pointer(name, buf)
2555+ if path == limit:
2556+ st.st_uid = 1
2557+ st.st_gid = 1
2558+ return 0
2559+ else:
2560+ self.delegate_to_original(name)
2561+ return -1
2562+
2563+ with self.run_in_subprocess(
2564+ "chown", "getpwnam", "__xstat", "__xstat64",
2565+ ) as (enter, preloads):
2566+ enter()
2567+ preloads["getpwnam"].side_effect = (
2568+ lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1)))
2569+ self._set_stat_side_effect(
2570+ preloads, stat_side_effect, self.db.props.root)
2571+
2572+ self._make_ownership_test()
2573+ self.db.ensure_ownership()
2574+ self.assertFalse(preloads["chown"].called)
2575+
2576+ def test_ensure_ownership(self):
2577+ def stat_side_effect(name, limit, ver, path, buf):
2578+ st = self.convert_stat_pointer(name, buf)
2579+ if path == limit:
2580+ st.st_uid = 2
2581+ st.st_gid = 2
2582+ return 0
2583+ else:
2584+ self.delegate_to_original(name)
2585+ return -1
2586+
2587+ with self.run_in_subprocess(
2588+ "chown", "getpwnam", "__xstat", "__xstat64",
2589+ ) as (enter, preloads):
2590+ enter()
2591+ preloads["getpwnam"].side_effect = (
2592+ lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1)))
2593+ self._set_stat_side_effect(
2594+ preloads, stat_side_effect, self.db.props.root)
2595+
2596+ self._make_ownership_test()
2597+ self.db.ensure_ownership()
2598+ expected_paths = [
2599+ self.temp_dir,
2600+ os.path.join(self.temp_dir, ".click"),
2601+ os.path.join(self.temp_dir, ".click", "log"),
2602+ os.path.join(self.temp_dir, ".click", "users"),
2603+ os.path.join(self.temp_dir, "a"),
2604+ os.path.join(self.temp_dir, "a", "1.0"),
2605+ os.path.join(self.temp_dir, "a", "1.0", ".click"),
2606+ os.path.join(self.temp_dir, "a", "1.0", ".click", "info"),
2607+ os.path.join(
2608+ self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"),
2609+ os.path.join(self.temp_dir, "a", "current"),
2610+ ]
2611+ self.assertCountEqual(
2612+ [path.encode() for path in expected_paths],
2613+ [args[0][0] for args in preloads["chown"].call_args_list])
2614+ self.assertCountEqual(
2615+ [(1, 1)],
2616+ set(args[0][1:] for args in preloads["chown"].call_args_list))
2617
2618
2619 class TestClickDB(TestCase):
2620@@ -276,6 +323,11 @@
2621 super(TestClickDB, self).setUp()
2622 self.use_temp_dir()
2623
2624+ def _installed_packages_tuplify(self, ip):
2625+ return [
2626+ (p.props.package, p.props.version, p.props.path, p.props.writeable)
2627+ for p in ip]
2628+
2629 def test_read_configuration(self):
2630 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
2631 print("[Click Database]", file=a)
2632@@ -283,23 +335,27 @@
2633 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
2634 print("[Click Database]", file=b)
2635 print("root = /b", file=b)
2636- db = ClickDB(extra_root="/c", override_db_dir=self.temp_dir)
2637- self.assertEqual(3, len(db))
2638- self.assertEqual(["/a", "/b", "/c"], [d.root for d in db])
2639+ db = Click.DB()
2640+ db.read(db_dir=self.temp_dir)
2641+ db.add("/c")
2642+ self.assertEqual(3, db.props.size)
2643+ self.assertEqual(
2644+ ["/a", "/b", "/c"],
2645+ [db.get(i).props.root for i in range(db.props.size)])
2646
2647- def test_no_use_system(self):
2648+ def test_no_read(self):
2649 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
2650 print("[Click Database]", file=a)
2651 print("root = /a", file=a)
2652- db = ClickDB(use_system=False, override_db_dir=self.temp_dir)
2653- self.assertEqual(0, len(db))
2654+ db = Click.DB()
2655+ self.assertEqual(0, db.props.size)
2656
2657 def test_add(self):
2658- db = ClickDB(use_system=False)
2659- self.assertEqual(0, len(db))
2660+ db = Click.DB()
2661+ self.assertEqual(0, db.props.size)
2662 db.add("/new/root")
2663- self.assertEqual(1, len(db))
2664- self.assertEqual(["/new/root"], [d.root for d in db])
2665+ self.assertEqual(1, db.props.size)
2666+ self.assertEqual("/new/root", db.get(0).props.root)
2667
2668 def test_overlay(self):
2669 with open(os.path.join(self.temp_dir, "00_custom.conf"), "w") as f:
2670@@ -308,8 +364,9 @@
2671 with open(os.path.join(self.temp_dir, "99_default.conf"), "w") as f:
2672 print("[Click Database]", file=f)
2673 print("root = /opt/click.ubuntu.com", file=f)
2674- db = ClickDB(override_db_dir=self.temp_dir)
2675- self.assertEqual("/opt/click.ubuntu.com", db.overlay)
2676+ db = Click.DB()
2677+ db.read(db_dir=self.temp_dir)
2678+ self.assertEqual("/opt/click.ubuntu.com", db.props.overlay)
2679
2680 def test_path(self):
2681 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
2682@@ -318,21 +375,24 @@
2683 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
2684 print("[Click Database]", file=b)
2685 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)
2686- db = ClickDB(override_db_dir=self.temp_dir)
2687- self.assertRaises(KeyError, db.path, "pkg", "1.0")
2688+ db = Click.DB()
2689+ db.read(db_dir=self.temp_dir)
2690+ self.assertRaisesDatabaseError(
2691+ Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.0")
2692 os.makedirs(os.path.join(self.temp_dir, "a", "pkg", "1.0"))
2693 self.assertEqual(
2694 os.path.join(self.temp_dir, "a", "pkg", "1.0"),
2695- db.path("pkg", "1.0"))
2696- self.assertRaises(KeyError, db.path, "pkg", "1.1")
2697+ db.get_path("pkg", "1.0"))
2698+ self.assertRaisesDatabaseError(
2699+ Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.1")
2700 os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.0"))
2701 self.assertEqual(
2702 os.path.join(self.temp_dir, "b", "pkg", "1.0"),
2703- db.path("pkg", "1.0"))
2704+ db.get_path("pkg", "1.0"))
2705 os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.1"))
2706 self.assertEqual(
2707 os.path.join(self.temp_dir, "b", "pkg", "1.1"),
2708- db.path("pkg", "1.1"))
2709+ db.get_path("pkg", "1.1"))
2710
2711 def test_packages_current(self):
2712 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
2713@@ -341,8 +401,9 @@
2714 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
2715 print("[Click Database]", file=b)
2716 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)
2717- db = ClickDB(override_db_dir=self.temp_dir)
2718- self.assertEqual([], list(db.packages()))
2719+ db = Click.DB()
2720+ db.read(db_dir=self.temp_dir)
2721+ self.assertEqual([], list(db.get_packages(all_versions=False)))
2722 os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0"))
2723 os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current"))
2724 os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1"))
2725@@ -354,7 +415,8 @@
2726 self.assertEqual([
2727 ("pkg1", "1.1", pkg1_current, True),
2728 ("pkg2", "0.1", pkg2_current, True),
2729- ], list(db.packages()))
2730+ ], self._installed_packages_tuplify(
2731+ db.get_packages(all_versions=False)))
2732
2733 def test_packages_all(self):
2734 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
2735@@ -363,8 +425,9 @@
2736 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
2737 print("[Click Database]", file=b)
2738 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)
2739- db = ClickDB(override_db_dir=self.temp_dir)
2740- self.assertEqual([], list(db.packages()))
2741+ db = Click.DB()
2742+ db.read(db_dir=self.temp_dir)
2743+ self.assertEqual([], list(db.get_packages(all_versions=False)))
2744 os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0"))
2745 os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current"))
2746 os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1"))
2747@@ -378,4 +441,5 @@
2748 True),
2749 ("pkg1", "1.0", os.path.join(self.temp_dir, "a", "pkg1", "1.0"),
2750 False),
2751- ], list(db.packages(all_versions=True)))
2752+ ], self._installed_packages_tuplify(
2753+ db.get_packages(all_versions=True)))
2754
2755=== modified file 'click/tests/test_hooks.py'
2756--- click/tests/test_hooks.py 2014-02-19 15:31:55 +0000
2757+++ click/tests/test_hooks.py 2014-03-06 07:04:44 +0000
2758@@ -27,824 +27,957 @@
2759 ]
2760
2761
2762-import contextlib
2763+from functools import partial
2764+from itertools import takewhile
2765 import json
2766 import os
2767 from textwrap import dedent
2768
2769-from click import hooks
2770-from click.database import ClickDB
2771-from click.hooks import (
2772- ClickHook,
2773- ClickPatternFormatter,
2774- package_install_hooks,
2775- package_remove_hooks,
2776- )
2777-from click.user import ClickUser
2778-from click.tests.helpers import TestCase, mkfile, mkfile_utf8, mock
2779-
2780-
2781-@contextlib.contextmanager
2782-def temp_hooks_dir(new_dir):
2783- old_dir = hooks.hooks_dir
2784- try:
2785- hooks.hooks_dir = new_dir
2786- yield
2787- finally:
2788- hooks.hooks_dir = old_dir
2789+from gi.repository import Click, GLib
2790+
2791+from click.tests.gimock_types import Passwd
2792+from click.tests.helpers import TestCase, mkfile, mkfile_utf8
2793
2794
2795 class TestClickPatternFormatter(TestCase):
2796- def setUp(self):
2797- super(TestClickPatternFormatter, self).setUp()
2798- self.formatter = ClickPatternFormatter()
2799+ def _make_variant(self, **kwargs):
2800+ # pygobject's Variant creator can't handle maybe types, so we have
2801+ # to do this by hand.
2802+ builder = GLib.VariantBuilder.new(GLib.VariantType.new("a{sms}"))
2803+ for key, value in kwargs.items():
2804+ entry = GLib.VariantBuilder.new(GLib.VariantType.new("{sms}"))
2805+ entry.add_value(GLib.Variant.new_string(key))
2806+ entry.add_value(GLib.Variant.new_maybe(
2807+ GLib.VariantType.new("s"),
2808+ None if value is None else GLib.Variant.new_string(value)))
2809+ builder.add_value(entry.end())
2810+ return builder.end()
2811
2812 def test_expands_provided_keys(self):
2813 self.assertEqual(
2814- "foo.bar", self.formatter.format("foo.${key}", key="bar"))
2815+ "foo.bar",
2816+ Click.pattern_format("foo.${key}", self._make_variant(key="bar")))
2817 self.assertEqual(
2818 "foo.barbaz",
2819- self.formatter.format(
2820- "foo.${key1}${key2}", key1="bar", key2="baz"))
2821+ Click.pattern_format(
2822+ "foo.${key1}${key2}",
2823+ self._make_variant(key1="bar", key2="baz")))
2824
2825 def test_expands_missing_keys_to_empty_string(self):
2826- self.assertEqual("xy", self.formatter.format("x${key}y"))
2827+ self.assertEqual(
2828+ "xy", Click.pattern_format("x${key}y", self._make_variant()))
2829
2830 def test_preserves_unmatched_dollar(self):
2831- self.assertEqual("$", self.formatter.format("$"))
2832- self.assertEqual("$ {foo}", self.formatter.format("$ {foo}"))
2833- self.assertEqual("x${y", self.formatter.format("${key}${y", key="x"))
2834+ self.assertEqual("$", Click.pattern_format("$", self._make_variant()))
2835+ self.assertEqual(
2836+ "$ {foo}", Click.pattern_format("$ {foo}", self._make_variant()))
2837+ self.assertEqual(
2838+ "x${y",
2839+ Click.pattern_format("${key}${y", self._make_variant(key="x")))
2840
2841 def test_double_dollar(self):
2842- self.assertEqual("$", self.formatter.format("$$"))
2843- self.assertEqual("${foo}", self.formatter.format("$${foo}"))
2844- self.assertEqual("x$y", self.formatter.format("x$$${key}", key="y"))
2845+ self.assertEqual("$", Click.pattern_format("$$", self._make_variant()))
2846+ self.assertEqual(
2847+ "${foo}", Click.pattern_format("$${foo}", self._make_variant()))
2848+ self.assertEqual(
2849+ "x$y",
2850+ Click.pattern_format("x$$${key}", self._make_variant(key="y")))
2851
2852 def test_possible_expansion(self):
2853 self.assertEqual(
2854 {"id": "abc"},
2855- self.formatter.possible_expansion(
2856- "x_abc_1", "x_${id}_${num}", num="1"))
2857+ Click.pattern_possible_expansion(
2858+ "x_abc_1", "x_${id}_${num}",
2859+ self._make_variant(num="1")).unpack())
2860 self.assertIsNone(
2861- self.formatter.possible_expansion(
2862- "x_abc_1", "x_${id}_${num}", num="2"))
2863+ Click.pattern_possible_expansion(
2864+ "x_abc_1", "x_${id}_${num}", self._make_variant(num="2")))
2865
2866
2867 class TestClickHookBase(TestCase):
2868 def setUp(self):
2869 super(TestClickHookBase, self).setUp()
2870 self.use_temp_dir()
2871- self.db = ClickDB(self.temp_dir)
2872+ self.db = Click.DB()
2873+ self.db.add(self.temp_dir)
2874+ self.spawn_calls = []
2875+
2876+ def _setup_hooks_dir(self, preloads, hooks_dir=None):
2877+ if hooks_dir is None:
2878+ hooks_dir = self.temp_dir
2879+ preloads["click_get_hooks_dir"].side_effect = (
2880+ lambda: self.make_string(hooks_dir))
2881+
2882+ def g_spawn_sync_side_effect(self, status_map, working_directory, argv,
2883+ envp, flags, child_setup, user_data,
2884+ standard_output, standard_error, exit_status,
2885+ error):
2886+ self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv)))
2887+ if argv[0] in status_map:
2888+ exit_status[0] = status_map[argv[0]]
2889+ else:
2890+ self.delegate_to_original("g_spawn_sync")
2891+ return 0
2892
2893
2894 class TestClickHookSystemLevel(TestClickHookBase):
2895 def test_open(self):
2896- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2897- print(dedent("""\
2898- Pattern: /usr/share/test/${id}.test
2899- # Comment
2900- Exec: test-update
2901- User: root
2902- """), file=f)
2903- with temp_hooks_dir(self.temp_dir):
2904- hook = ClickHook.open(self.db, "test")
2905- self.assertCountEqual(["Pattern", "Exec", "User"], hook.keys())
2906- self.assertEqual("/usr/share/test/${id}.test", hook["pattern"])
2907- self.assertEqual("test-update", hook["exec"])
2908- self.assertFalse(hook.user_level)
2909+ with self.run_in_subprocess(
2910+ "click_get_hooks_dir") as (enter, preloads):
2911+ enter()
2912+ self._setup_hooks_dir(preloads)
2913+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2914+ print(dedent("""\
2915+ Pattern: /usr/share/test/${id}.test
2916+ # Comment
2917+ Exec: test-update
2918+ User: root
2919+ """), file=f)
2920+ hook = Click.Hook.open(self.db, "test")
2921+ self.assertCountEqual(
2922+ ["pattern", "exec", "user"], hook.get_fields())
2923+ self.assertEqual(
2924+ "/usr/share/test/${id}.test", hook.get_field("pattern"))
2925+ self.assertEqual("test-update", hook.get_field("exec"))
2926+ self.assertFalse(hook.props.is_user_level)
2927
2928 def test_hook_name_absent(self):
2929- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2930- print("Pattern: /usr/share/test/${id}.test", file=f)
2931- with temp_hooks_dir(self.temp_dir):
2932- hook = ClickHook.open(self.db, "test")
2933- self.assertEqual("test", hook.hook_name)
2934+ with self.run_in_subprocess(
2935+ "click_get_hooks_dir") as (enter, preloads):
2936+ enter()
2937+ self._setup_hooks_dir(preloads)
2938+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2939+ print("Pattern: /usr/share/test/${id}.test", file=f)
2940+ hook = Click.Hook.open(self.db, "test")
2941+ self.assertEqual("test", hook.get_hook_name())
2942
2943 def test_hook_name_present(self):
2944- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2945- print("Pattern: /usr/share/test/${id}.test", file=f)
2946- print("Hook-Name: other", file=f)
2947- with temp_hooks_dir(self.temp_dir):
2948- hook = ClickHook.open(self.db, "test")
2949- self.assertEqual("other", hook.hook_name)
2950+ with self.run_in_subprocess(
2951+ "click_get_hooks_dir") as (enter, preloads):
2952+ enter()
2953+ self._setup_hooks_dir(preloads)
2954+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2955+ print("Pattern: /usr/share/test/${id}.test", file=f)
2956+ print("Hook-Name: other", file=f)
2957+ hook = Click.Hook.open(self.db, "test")
2958+ self.assertEqual("other", hook.get_hook_name())
2959
2960 def test_invalid_app_id(self):
2961- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2962- print(dedent("""\
2963- Pattern: /usr/share/test/${id}.test
2964- # Comment
2965- Exec: test-update
2966- User: root
2967- """), file=f)
2968- with temp_hooks_dir(self.temp_dir):
2969- hook = ClickHook.open(self.db, "test")
2970- self.assertRaises(
2971- ValueError, hook.app_id, "package", "0.1", "app_name")
2972- self.assertRaises(
2973- ValueError, hook.app_id, "package", "0.1", "app/name")
2974+ with self.run_in_subprocess(
2975+ "click_get_hooks_dir") as (enter, preloads):
2976+ enter()
2977+ self._setup_hooks_dir(preloads)
2978+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2979+ print(dedent("""\
2980+ Pattern: /usr/share/test/${id}.test
2981+ # Comment
2982+ Exec: test-update
2983+ User: root
2984+ """), file=f)
2985+ hook = Click.Hook.open(self.db, "test")
2986+ self.assertRaisesHooksError(
2987+ Click.HooksError.BAD_APP_NAME, hook.get_app_id,
2988+ "package", "0.1", "app_name")
2989+ self.assertRaisesHooksError(
2990+ Click.HooksError.BAD_APP_NAME, hook.get_app_id,
2991+ "package", "0.1", "app/name")
2992
2993 def test_short_id_invalid(self):
2994- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
2995- print("Pattern: /usr/share/test/${short-id}.test", file=f)
2996- with temp_hooks_dir(self.temp_dir):
2997- hook = ClickHook.open(self.db, "test")
2998- # It would perhaps be better if unrecognised $-expansions raised
2999- # KeyError, but they don't right now.
3000- self.assertEqual(
3001- "/usr/share/test/.test",
3002- hook.pattern("package", "0.1", "app-name"))
3003+ with self.run_in_subprocess(
3004+ "click_get_hooks_dir") as (enter, preloads):
3005+ enter()
3006+ self._setup_hooks_dir(preloads)
3007+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3008+ print("Pattern: /usr/share/test/${short-id}.test", file=f)
3009+ hook = Click.Hook.open(self.db, "test")
3010+ # It would perhaps be better if unrecognised $-expansions raised
3011+ # KeyError, but they don't right now.
3012+ self.assertEqual(
3013+ "/usr/share/test/.test",
3014+ hook.get_pattern("package", "0.1", "app-name"))
3015
3016 def test_short_id_valid_with_single_version(self):
3017- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3018- print("Pattern: /usr/share/test/${short-id}.test", file=f)
3019- print("Single-Version: yes", file=f)
3020- with temp_hooks_dir(self.temp_dir):
3021- hook = ClickHook.open(self.db, "test")
3022- self.assertEqual(
3023- "/usr/share/test/package_app-name.test",
3024- hook.pattern("package", "0.1", "app-name"))
3025-
3026- @mock.patch("subprocess.check_call")
3027- def test_run_commands(self, mock_check_call):
3028- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3029- print("Exec: test-update", file=f)
3030- print("User: root", file=f)
3031- with temp_hooks_dir(self.temp_dir):
3032- hook = ClickHook.open(self.db, "test")
3033- self.assertEqual("root", hook._run_commands_user(user=None))
3034- hook._run_commands(user=None)
3035- mock_check_call.assert_called_once_with(
3036- "test-update", preexec_fn=mock.ANY, shell=True)
3037-
3038- def test_previous_entries(self):
3039- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3040- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3041- link_one = os.path.join(
3042- self.temp_dir, "org.example.package_test-app_1.0.test")
3043- link_two = os.path.join(
3044- self.temp_dir, "org.example.package_test-app_2.0.test")
3045- os.symlink("dummy", link_one)
3046- os.symlink("dummy", link_two)
3047- os.symlink("dummy", os.path.join(self.temp_dir, "malformed"))
3048- with temp_hooks_dir(self.temp_dir):
3049- hook = ClickHook.open(self.db, "test")
3050- self.assertCountEqual([
3051- (link_one, "org.example.package", "1.0", "test-app"),
3052- (link_two, "org.example.package", "2.0", "test-app"),
3053- ], list(hook._previous_entries()))
3054+ with self.run_in_subprocess(
3055+ "click_get_hooks_dir") as (enter, preloads):
3056+ enter()
3057+ self._setup_hooks_dir(preloads)
3058+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3059+ print("Pattern: /usr/share/test/${short-id}.test", file=f)
3060+ print("Single-Version: yes", file=f)
3061+ hook = Click.Hook.open(self.db, "test")
3062+ self.assertEqual(
3063+ "/usr/share/test/package_app-name.test",
3064+ hook.get_pattern("package", "0.1", "app-name"))
3065+
3066+ def test_run_commands(self):
3067+ with self.run_in_subprocess(
3068+ "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads):
3069+ enter()
3070+ self._setup_hooks_dir(preloads)
3071+ preloads["g_spawn_sync"].side_effect = partial(
3072+ self.g_spawn_sync_side_effect, {b"/bin/sh": 0})
3073+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3074+ print("Exec: test-update", file=f)
3075+ print("User: root", file=f)
3076+ hook = Click.Hook.open(self.db, "test")
3077+ self.assertEqual(
3078+ "root", hook.get_run_commands_user(user_name=None))
3079+ hook.run_commands(user_name=None)
3080+ self.assertEqual(
3081+ [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls)
3082
3083 def test_install_package(self):
3084- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3085- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3086- os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0"))
3087- with temp_hooks_dir(self.temp_dir):
3088- hook = ClickHook.open(self.db, "test")
3089- hook.install_package(
3090- "org.example.package", "1.0", "test-app", "foo/bar")
3091- symlink_path = os.path.join(
3092- self.temp_dir, "org.example.package_test-app_1.0.test")
3093- target_path = os.path.join(
3094- self.temp_dir, "org.example.package", "1.0", "foo", "bar")
3095- self.assertTrue(os.path.islink(symlink_path))
3096- self.assertEqual(target_path, os.readlink(symlink_path))
3097+ with self.run_in_subprocess(
3098+ "click_get_hooks_dir") as (enter, preloads):
3099+ enter()
3100+ self._setup_hooks_dir(preloads)
3101+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3102+ print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3103+ os.makedirs(
3104+ os.path.join(self.temp_dir, "org.example.package", "1.0"))
3105+ hook = Click.Hook.open(self.db, "test")
3106+ hook.install_package(
3107+ "org.example.package", "1.0", "test-app", "foo/bar")
3108+ symlink_path = os.path.join(
3109+ self.temp_dir, "org.example.package_test-app_1.0.test")
3110+ target_path = os.path.join(
3111+ self.temp_dir, "org.example.package", "1.0", "foo", "bar")
3112+ self.assertTrue(os.path.islink(symlink_path))
3113+ self.assertEqual(target_path, os.readlink(symlink_path))
3114
3115 def test_install_package_trailing_slash(self):
3116- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3117- print("Pattern: %s/${id}/" % self.temp_dir, file=f)
3118- os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0"))
3119- with temp_hooks_dir(self.temp_dir):
3120- hook = ClickHook.open(self.db, "test")
3121- hook.install_package("org.example.package", "1.0", "test-app", "foo")
3122- symlink_path = os.path.join(
3123- self.temp_dir, "org.example.package_test-app_1.0")
3124- target_path = os.path.join(
3125- self.temp_dir, "org.example.package", "1.0", "foo")
3126- self.assertTrue(os.path.islink(symlink_path))
3127- self.assertEqual(target_path, os.readlink(symlink_path))
3128+ with self.run_in_subprocess(
3129+ "click_get_hooks_dir") as (enter, preloads):
3130+ enter()
3131+ self._setup_hooks_dir(preloads)
3132+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3133+ print("Pattern: %s/${id}/" % self.temp_dir, file=f)
3134+ os.makedirs(
3135+ os.path.join(self.temp_dir, "org.example.package", "1.0"))
3136+ hook = Click.Hook.open(self.db, "test")
3137+ hook.install_package(
3138+ "org.example.package", "1.0", "test-app", "foo")
3139+ symlink_path = os.path.join(
3140+ self.temp_dir, "org.example.package_test-app_1.0")
3141+ target_path = os.path.join(
3142+ self.temp_dir, "org.example.package", "1.0", "foo")
3143+ self.assertTrue(os.path.islink(symlink_path))
3144+ self.assertEqual(target_path, os.readlink(symlink_path))
3145
3146 def test_upgrade(self):
3147- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3148- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3149- symlink_path = os.path.join(
3150- self.temp_dir, "org.example.package_test-app_1.0.test")
3151- os.symlink("old-target", symlink_path)
3152- os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0"))
3153- with temp_hooks_dir(self.temp_dir):
3154- hook = ClickHook.open(self.db, "test")
3155- hook.install_package(
3156- "org.example.package", "1.0", "test-app", "foo/bar")
3157- target_path = os.path.join(
3158- self.temp_dir, "org.example.package", "1.0", "foo", "bar")
3159- self.assertTrue(os.path.islink(symlink_path))
3160- self.assertEqual(target_path, os.readlink(symlink_path))
3161+ with self.run_in_subprocess(
3162+ "click_get_hooks_dir") as (enter, preloads):
3163+ enter()
3164+ self._setup_hooks_dir(preloads)
3165+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3166+ print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3167+ symlink_path = os.path.join(
3168+ self.temp_dir, "org.example.package_test-app_1.0.test")
3169+ os.symlink("old-target", symlink_path)
3170+ os.makedirs(
3171+ os.path.join(self.temp_dir, "org.example.package", "1.0"))
3172+ hook = Click.Hook.open(self.db, "test")
3173+ hook.install_package(
3174+ "org.example.package", "1.0", "test-app", "foo/bar")
3175+ target_path = os.path.join(
3176+ self.temp_dir, "org.example.package", "1.0", "foo", "bar")
3177+ self.assertTrue(os.path.islink(symlink_path))
3178+ self.assertEqual(target_path, os.readlink(symlink_path))
3179
3180 def test_remove_package(self):
3181- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3182- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3183- symlink_path = os.path.join(
3184- self.temp_dir, "org.example.package_test-app_1.0.test")
3185- os.symlink("old-target", symlink_path)
3186- with temp_hooks_dir(self.temp_dir):
3187- hook = ClickHook.open(self.db, "test")
3188- hook.remove_package("org.example.package", "1.0", "test-app")
3189- self.assertFalse(os.path.exists(symlink_path))
3190+ with self.run_in_subprocess(
3191+ "click_get_hooks_dir") as (enter, preloads):
3192+ enter()
3193+ self._setup_hooks_dir(preloads)
3194+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3195+ print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3196+ symlink_path = os.path.join(
3197+ self.temp_dir, "org.example.package_test-app_1.0.test")
3198+ os.symlink("old-target", symlink_path)
3199+ hook = Click.Hook.open(self.db, "test")
3200+ hook.remove_package("org.example.package", "1.0", "test-app")
3201+ self.assertFalse(os.path.exists(symlink_path))
3202
3203 def test_install(self):
3204- with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:
3205- print("Pattern: %s/${id}.new" % self.temp_dir, file=f)
3206- with mkfile_utf8(os.path.join(
3207- self.temp_dir, "test-1", "1.0", ".click", "info",
3208- "test-1.manifest")) as f:
3209- json.dump({
3210- "maintainer":
3211- b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),
3212- "hooks": {"test1-app": {"new": "target-1"}},
3213- }, f, ensure_ascii=False)
3214- os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3215- with mkfile_utf8(os.path.join(
3216- self.temp_dir, "test-2", "2.0", ".click", "info",
3217- "test-2.manifest")) as f:
3218- json.dump({
3219- "maintainer":
3220- b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),
3221- "hooks": {"test1-app": {"new": "target-2"}},
3222- }, f, ensure_ascii=False)
3223- os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))
3224- with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):
3225- hook = ClickHook.open(self.db, "new")
3226- hook.install()
3227- path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")
3228- self.assertTrue(os.path.lexists(path_1))
3229- self.assertEqual(
3230- os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
3231- os.readlink(path_1))
3232- path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")
3233- self.assertTrue(os.path.lexists(path_2))
3234- self.assertEqual(
3235- os.path.join(self.temp_dir, "test-2", "2.0", "target-2"),
3236- os.readlink(path_2))
3237+ with self.run_in_subprocess(
3238+ "click_get_hooks_dir") as (enter, preloads):
3239+ enter()
3240+ self._setup_hooks_dir(
3241+ preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
3242+ with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:
3243+ print("Pattern: %s/${id}.new" % self.temp_dir, file=f)
3244+ with mkfile_utf8(os.path.join(
3245+ self.temp_dir, "test-1", "1.0", ".click", "info",
3246+ "test-1.manifest")) as f:
3247+ json.dump({
3248+ "maintainer":
3249+ b"Unic\xc3\xb3de <unicode@example.org>".decode(
3250+ "UTF-8"),
3251+ "hooks": {"test1-app": {"new": "target-1"}},
3252+ }, f, ensure_ascii=False)
3253+ os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3254+ with mkfile_utf8(os.path.join(
3255+ self.temp_dir, "test-2", "2.0", ".click", "info",
3256+ "test-2.manifest")) as f:
3257+ json.dump({
3258+ "maintainer":
3259+ b"Unic\xc3\xb3de <unicode@example.org>".decode(
3260+ "UTF-8"),
3261+ "hooks": {"test1-app": {"new": "target-2"}},
3262+ }, f, ensure_ascii=False)
3263+ os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))
3264+ hook = Click.Hook.open(self.db, "new")
3265+ hook.install()
3266+ path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")
3267+ self.assertTrue(os.path.lexists(path_1))
3268+ self.assertEqual(
3269+ os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
3270+ os.readlink(path_1))
3271+ path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")
3272+ self.assertTrue(os.path.lexists(path_2))
3273+ self.assertEqual(
3274+ os.path.join(self.temp_dir, "test-2", "2.0", "target-2"),
3275+ os.readlink(path_2))
3276
3277 def test_remove(self):
3278- with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:
3279- print("Pattern: %s/${id}.old" % self.temp_dir, file=f)
3280- with mkfile(os.path.join(
3281- self.temp_dir, "test-1", "1.0", ".click", "info",
3282- "test-1.manifest")) as f:
3283- json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)
3284- os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3285- path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")
3286- os.symlink(
3287- os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1)
3288- with mkfile(os.path.join(
3289- self.temp_dir, "test-2", "2.0", ".click", "info",
3290- "test-2.manifest")) as f:
3291- json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)
3292- os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))
3293- path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")
3294- os.symlink(
3295- os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), path_2)
3296- with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):
3297- hook = ClickHook.open(self.db, "old")
3298- hook.remove()
3299- self.assertFalse(os.path.exists(path_1))
3300- self.assertFalse(os.path.exists(path_2))
3301+ with self.run_in_subprocess(
3302+ "click_get_hooks_dir") as (enter, preloads):
3303+ enter()
3304+ self._setup_hooks_dir(
3305+ preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
3306+ with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:
3307+ print("Pattern: %s/${id}.old" % self.temp_dir, file=f)
3308+ with mkfile(os.path.join(
3309+ self.temp_dir, "test-1", "1.0", ".click", "info",
3310+ "test-1.manifest")) as f:
3311+ json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)
3312+ os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3313+ path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")
3314+ os.symlink(
3315+ os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
3316+ path_1)
3317+ with mkfile(os.path.join(
3318+ self.temp_dir, "test-2", "2.0", ".click", "info",
3319+ "test-2.manifest")) as f:
3320+ json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)
3321+ os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))
3322+ path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")
3323+ os.symlink(
3324+ os.path.join(self.temp_dir, "test-2", "2.0", "target-2"),
3325+ path_2)
3326+ hook = Click.Hook.open(self.db, "old")
3327+ hook.remove()
3328+ self.assertFalse(os.path.exists(path_1))
3329+ self.assertFalse(os.path.exists(path_2))
3330
3331 def test_sync(self):
3332- with mkfile(os.path.join(self.temp_dir, "hooks", "test.hook")) as f:
3333- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3334- with mkfile(os.path.join(
3335- self.temp_dir, "test-1", "1.0", ".click", "info",
3336- "test-1.manifest")) as f:
3337- json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)
3338- os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3339- with mkfile(os.path.join(
3340- self.temp_dir, "test-2", "1.1", ".click", "info",
3341- "test-2.manifest")) as f:
3342- json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)
3343- os.symlink("1.1", os.path.join(self.temp_dir, "test-2", "current"))
3344- path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")
3345- os.symlink(
3346- os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1)
3347- path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")
3348- path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")
3349- os.symlink(
3350- os.path.join(self.temp_dir, "test-3", "1.0", "target-3"), path_3)
3351- with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):
3352- hook = ClickHook.open(self.db, "test")
3353- hook.sync()
3354- self.assertTrue(os.path.lexists(path_1))
3355- self.assertEqual(
3356- os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
3357- os.readlink(path_1))
3358- self.assertTrue(os.path.lexists(path_2))
3359- self.assertEqual(
3360- os.path.join(self.temp_dir, "test-2", "1.1", "target-2"),
3361- os.readlink(path_2))
3362- self.assertFalse(os.path.lexists(path_3))
3363+ with self.run_in_subprocess(
3364+ "click_get_hooks_dir") as (enter, preloads):
3365+ enter()
3366+ self._setup_hooks_dir(
3367+ preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
3368+ with mkfile(os.path.join(
3369+ self.temp_dir, "hooks", "test.hook")) as f:
3370+ print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3371+ with mkfile(os.path.join(
3372+ self.temp_dir, "test-1", "1.0", ".click", "info",
3373+ "test-1.manifest")) as f:
3374+ json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)
3375+ os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3376+ with mkfile(os.path.join(
3377+ self.temp_dir, "test-2", "1.1", ".click", "info",
3378+ "test-2.manifest")) as f:
3379+ json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)
3380+ os.symlink("1.1", os.path.join(self.temp_dir, "test-2", "current"))
3381+ path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")
3382+ os.symlink(
3383+ os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
3384+ path_1)
3385+ path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")
3386+ path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")
3387+ os.symlink(
3388+ os.path.join(self.temp_dir, "test-3", "1.0", "target-3"),
3389+ path_3)
3390+ hook = Click.Hook.open(self.db, "test")
3391+ hook.sync()
3392+ self.assertTrue(os.path.lexists(path_1))
3393+ self.assertEqual(
3394+ os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
3395+ os.readlink(path_1))
3396+ self.assertTrue(os.path.lexists(path_2))
3397+ self.assertEqual(
3398+ os.path.join(self.temp_dir, "test-2", "1.1", "target-2"),
3399+ os.readlink(path_2))
3400+ self.assertFalse(os.path.lexists(path_3))
3401
3402
3403 class TestClickHookUserLevel(TestClickHookBase):
3404 def test_open(self):
3405- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3406- print(dedent("""\
3407- User-Level: yes
3408- Pattern: ${home}/.local/share/test/${id}.test
3409- # Comment
3410- Exec: test-update
3411- """), file=f)
3412- with temp_hooks_dir(self.temp_dir):
3413- hook = ClickHook.open(self.db, "test")
3414- self.assertCountEqual(["User-Level", "Pattern", "Exec"], hook.keys())
3415- self.assertEqual(
3416- "${home}/.local/share/test/${id}.test", hook["pattern"])
3417- self.assertEqual("test-update", hook["exec"])
3418- self.assertTrue(hook.user_level)
3419+ with self.run_in_subprocess(
3420+ "click_get_hooks_dir") as (enter, preloads):
3421+ enter()
3422+ self._setup_hooks_dir(preloads)
3423+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3424+ print(dedent("""\
3425+ User-Level: yes
3426+ Pattern: ${home}/.local/share/test/${id}.test
3427+ # Comment
3428+ Exec: test-update
3429+ """), file=f)
3430+ hook = Click.Hook.open(self.db, "test")
3431+ self.assertCountEqual(
3432+ ["user-level", "pattern", "exec"], hook.get_fields())
3433+ self.assertEqual(
3434+ "${home}/.local/share/test/${id}.test",
3435+ hook.get_field("pattern"))
3436+ self.assertEqual("test-update", hook.get_field("exec"))
3437+ self.assertTrue(hook.props.is_user_level)
3438
3439 def test_hook_name_absent(self):
3440- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3441- print("User-Level: yes", file=f)
3442- print("Pattern: ${home}/.local/share/test/${id}.test", file=f)
3443- with temp_hooks_dir(self.temp_dir):
3444- hook = ClickHook.open(self.db, "test")
3445- self.assertEqual("test", hook.hook_name)
3446+ with self.run_in_subprocess(
3447+ "click_get_hooks_dir") as (enter, preloads):
3448+ enter()
3449+ self._setup_hooks_dir(preloads)
3450+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3451+ print("User-Level: yes", file=f)
3452+ print("Pattern: ${home}/.local/share/test/${id}.test", file=f)
3453+ hook = Click.Hook.open(self.db, "test")
3454+ self.assertEqual("test", hook.get_hook_name())
3455
3456 def test_hook_name_present(self):
3457- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3458- print("User-Level: yes", file=f)
3459- print("Pattern: ${home}/.local/share/test/${id}.test", file=f)
3460- print("Hook-Name: other", file=f)
3461- with temp_hooks_dir(self.temp_dir):
3462- hook = ClickHook.open(self.db, "test")
3463- self.assertEqual("other", hook.hook_name)
3464+ with self.run_in_subprocess(
3465+ "click_get_hooks_dir") as (enter, preloads):
3466+ enter()
3467+ self._setup_hooks_dir(preloads)
3468+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3469+ print("User-Level: yes", file=f)
3470+ print("Pattern: ${home}/.local/share/test/${id}.test", file=f)
3471+ print("Hook-Name: other", file=f)
3472+ hook = Click.Hook.open(self.db, "test")
3473+ self.assertEqual("other", hook.get_hook_name())
3474
3475 def test_invalid_app_id(self):
3476- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3477- print(dedent("""\
3478- User-Level: yes
3479- Pattern: ${home}/.local/share/test/${id}.test
3480- # Comment
3481- Exec: test-update
3482- """), file=f)
3483- with temp_hooks_dir(self.temp_dir):
3484- hook = ClickHook.open(self.db, "test")
3485- self.assertRaises(
3486- ValueError, hook.app_id, "package", "0.1", "app_name")
3487- self.assertRaises(
3488- ValueError, hook.app_id, "package", "0.1", "app/name")
3489-
3490- @mock.patch("pwd.getpwnam")
3491- def test_short_id_valid(self, mock_getpwnam):
3492- class MockPasswd:
3493- def __init__(self, pw_dir):
3494- self.pw_dir = pw_dir
3495-
3496- mock_getpwnam.return_value = MockPasswd(pw_dir="/mock")
3497- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3498- print("User-Level: yes", file=f)
3499- print(
3500- "Pattern: ${home}/.local/share/test/${short-id}.test", file=f)
3501- with temp_hooks_dir(self.temp_dir):
3502- hook = ClickHook.open(self.db, "test")
3503- self.assertEqual(
3504- "/mock/.local/share/test/package_app-name.test",
3505- hook.pattern("package", "0.1", "app-name", user="mock"))
3506-
3507- @mock.patch("subprocess.check_call")
3508- def test_run_commands(self, mock_check_call):
3509- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3510- print("User-Level: yes", file=f)
3511- print("Exec: test-update", file=f)
3512- with temp_hooks_dir(self.temp_dir):
3513- hook = ClickHook.open(self.db, "test")
3514- self.assertEqual(
3515- "test-user", hook._run_commands_user(user="test-user"))
3516- hook._run_commands(user="test-user")
3517- mock_check_call.assert_called_once_with(
3518- "test-update", preexec_fn=mock.ANY, shell=True)
3519-
3520- @mock.patch("click.hooks.ClickHook._user_home")
3521- def test_previous_entries(self, mock_user_home):
3522- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3523- print("User-Level: yes", file=f)
3524- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3525- link_one = os.path.join(
3526- self.temp_dir, "org.example.package_test-app_1.0.test")
3527- link_two = os.path.join(
3528- self.temp_dir, "org.example.package_test-app_2.0.test")
3529- os.symlink("dummy", link_one)
3530- os.symlink("dummy", link_two)
3531- os.symlink("dummy", os.path.join(self.temp_dir, "malformed"))
3532- with temp_hooks_dir(self.temp_dir):
3533- hook = ClickHook.open(self.db, "test")
3534- self.assertCountEqual([
3535- (link_one, "org.example.package", "1.0", "test-app"),
3536- (link_two, "org.example.package", "2.0", "test-app"),
3537- ], list(hook._previous_entries(user="test-user")))
3538-
3539- @mock.patch("click.hooks.ClickHook._user_home")
3540- def test_install_package(self, mock_user_home):
3541- mock_user_home.return_value = "/home/test-user"
3542- with temp_hooks_dir(self.temp_dir):
3543+ with self.run_in_subprocess(
3544+ "click_get_hooks_dir") as (enter, preloads):
3545+ enter()
3546+ self._setup_hooks_dir(preloads)
3547+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3548+ print(dedent("""\
3549+ User-Level: yes
3550+ Pattern: ${home}/.local/share/test/${id}.test
3551+ # Comment
3552+ Exec: test-update
3553+ """), file=f)
3554+ hook = Click.Hook.open(self.db, "test")
3555+ self.assertRaisesHooksError(
3556+ Click.HooksError.BAD_APP_NAME, hook.get_app_id,
3557+ "package", "0.1", "app_name")
3558+ self.assertRaisesHooksError(
3559+ Click.HooksError.BAD_APP_NAME, hook.get_app_id,
3560+ "package", "0.1", "app/name")
3561+
3562+ def test_short_id_valid(self):
3563+ with self.run_in_subprocess(
3564+ "click_get_hooks_dir", "getpwnam") as (enter, preloads):
3565+ enter()
3566+ self._setup_hooks_dir(preloads)
3567+ preloads["getpwnam"].side_effect = (
3568+ lambda name: self.make_pointer(Passwd(pw_dir=b"/mock")))
3569+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3570+ print("User-Level: yes", file=f)
3571+ print(
3572+ "Pattern: ${home}/.local/share/test/${short-id}.test",
3573+ file=f)
3574+ hook = Click.Hook.open(self.db, "test")
3575+ self.assertEqual(
3576+ "/mock/.local/share/test/package_app-name.test",
3577+ hook.get_pattern(
3578+ "package", "0.1", "app-name", user_name="mock"))
3579+
3580+ def test_run_commands(self):
3581+ with self.run_in_subprocess(
3582+ "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads):
3583+ enter()
3584+ self._setup_hooks_dir(preloads)
3585+ preloads["g_spawn_sync"].side_effect = partial(
3586+ self.g_spawn_sync_side_effect, {b"/bin/sh": 0})
3587+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3588+ print("User-Level: yes", file=f)
3589+ print("Exec: test-update", file=f)
3590+ hook = Click.Hook.open(self.db, "test")
3591+ self.assertEqual(
3592+ "test-user", hook.get_run_commands_user(user_name="test-user"))
3593+ hook.run_commands(user_name="test-user")
3594+ self.assertEqual(
3595+ [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls)
3596+
3597+ def test_install_package(self):
3598+ with self.run_in_subprocess(
3599+ "click_get_hooks_dir", "click_get_user_home",
3600+ ) as (enter, preloads):
3601+ enter()
3602+ self._setup_hooks_dir(preloads)
3603+ preloads["click_get_user_home"].return_value = "/home/test-user"
3604 os.makedirs(os.path.join(
3605 self.temp_dir, "org.example.package", "1.0"))
3606- user_db = ClickUser(self.db, user="test-user")
3607+ user_db = Click.User.for_user(self.db, "test-user")
3608 user_db.set_version("org.example.package", "1.0")
3609 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3610 print("User-Level: yes", file=f)
3611 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3612- hook = ClickHook.open(self.db, "test")
3613- hook.install_package(
3614- "org.example.package", "1.0", "test-app", "foo/bar",
3615- user="test-user")
3616- symlink_path = os.path.join(
3617- self.temp_dir, "org.example.package_test-app_1.0.test")
3618- target_path = os.path.join(
3619- self.temp_dir, ".click", "users", "test-user",
3620- "org.example.package", "foo", "bar")
3621- self.assertTrue(os.path.islink(symlink_path))
3622- self.assertEqual(target_path, os.readlink(symlink_path))
3623+ hook = Click.Hook.open(self.db, "test")
3624+ hook.install_package(
3625+ "org.example.package", "1.0", "test-app", "foo/bar",
3626+ user_name="test-user")
3627+ symlink_path = os.path.join(
3628+ self.temp_dir, "org.example.package_test-app_1.0.test")
3629+ target_path = os.path.join(
3630+ self.temp_dir, ".click", "users", "test-user",
3631+ "org.example.package", "foo", "bar")
3632+ self.assertTrue(os.path.islink(symlink_path))
3633+ self.assertEqual(target_path, os.readlink(symlink_path))
3634
3635- @mock.patch("click.hooks.ClickHook._user_home")
3636- def test_install_package_trailing_slash(self, mock_user_home):
3637- mock_user_home.return_value = "/home/test-user"
3638- with temp_hooks_dir(self.temp_dir):
3639+ def test_install_package_trailing_slash(self):
3640+ with self.run_in_subprocess(
3641+ "click_get_hooks_dir", "click_get_user_home",
3642+ ) as (enter, preloads):
3643+ enter()
3644+ self._setup_hooks_dir(preloads)
3645+ preloads["click_get_user_home"].return_value = "/home/test-user"
3646 os.makedirs(os.path.join(
3647 self.temp_dir, "org.example.package", "1.0"))
3648- user_db = ClickUser(self.db, user="test-user")
3649+ user_db = Click.User.for_user(self.db, "test-user")
3650 user_db.set_version("org.example.package", "1.0")
3651 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3652 print("User-Level: yes", file=f)
3653 print("Pattern: %s/${id}/" % self.temp_dir, file=f)
3654- hook = ClickHook.open(self.db, "test")
3655- hook.install_package(
3656- "org.example.package", "1.0", "test-app", "foo", user="test-user")
3657- symlink_path = os.path.join(
3658- self.temp_dir, "org.example.package_test-app_1.0")
3659- target_path = os.path.join(
3660- self.temp_dir, ".click", "users", "test-user",
3661- "org.example.package", "foo")
3662- self.assertTrue(os.path.islink(symlink_path))
3663- self.assertEqual(target_path, os.readlink(symlink_path))
3664+ hook = Click.Hook.open(self.db, "test")
3665+ hook.install_package(
3666+ "org.example.package", "1.0", "test-app", "foo",
3667+ user_name="test-user")
3668+ symlink_path = os.path.join(
3669+ self.temp_dir, "org.example.package_test-app_1.0")
3670+ target_path = os.path.join(
3671+ self.temp_dir, ".click", "users", "test-user",
3672+ "org.example.package", "foo")
3673+ self.assertTrue(os.path.islink(symlink_path))
3674+ self.assertEqual(target_path, os.readlink(symlink_path))
3675
3676- @mock.patch("click.hooks.ClickHook._user_home")
3677- def test_install_package_removes_previous(self, mock_user_home):
3678- mock_user_home.return_value = "/home/test-user"
3679- with temp_hooks_dir(self.temp_dir):
3680+ def test_install_package_removes_previous(self):
3681+ with self.run_in_subprocess(
3682+ "click_get_hooks_dir", "click_get_user_home",
3683+ ) as (enter, preloads):
3684+ enter()
3685+ self._setup_hooks_dir(preloads)
3686+ preloads["click_get_user_home"].return_value = "/home/test-user"
3687 os.makedirs(os.path.join(
3688 self.temp_dir, "org.example.package", "1.0"))
3689 os.makedirs(os.path.join(
3690 self.temp_dir, "org.example.package", "1.1"))
3691- user_db = ClickUser(self.db, user="test-user")
3692+ user_db = Click.User.for_user(self.db, "test-user")
3693 user_db.set_version("org.example.package", "1.0")
3694 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3695 print("User-Level: yes", file=f)
3696 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3697- hook = ClickHook.open(self.db, "test")
3698- hook.install_package(
3699- "org.example.package", "1.0", "test-app", "foo/bar",
3700- user="test-user")
3701- hook.install_package(
3702- "org.example.package", "1.1", "test-app", "foo/bar",
3703- user="test-user")
3704- old_symlink_path = os.path.join(
3705- self.temp_dir, "org.example.package_test-app_1.0.test")
3706- symlink_path = os.path.join(
3707- self.temp_dir, "org.example.package_test-app_1.1.test")
3708- self.assertFalse(os.path.islink(old_symlink_path))
3709- self.assertTrue(os.path.islink(symlink_path))
3710- target_path = os.path.join(
3711- self.temp_dir, ".click", "users", "test-user",
3712- "org.example.package", "foo", "bar")
3713- self.assertEqual(target_path, os.readlink(symlink_path))
3714+ hook = Click.Hook.open(self.db, "test")
3715+ hook.install_package(
3716+ "org.example.package", "1.0", "test-app", "foo/bar",
3717+ user_name="test-user")
3718+ hook.install_package(
3719+ "org.example.package", "1.1", "test-app", "foo/bar",
3720+ user_name="test-user")
3721+ old_symlink_path = os.path.join(
3722+ self.temp_dir, "org.example.package_test-app_1.0.test")
3723+ symlink_path = os.path.join(
3724+ self.temp_dir, "org.example.package_test-app_1.1.test")
3725+ self.assertFalse(os.path.islink(old_symlink_path))
3726+ self.assertTrue(os.path.islink(symlink_path))
3727+ target_path = os.path.join(
3728+ self.temp_dir, ".click", "users", "test-user",
3729+ "org.example.package", "foo", "bar")
3730+ self.assertEqual(target_path, os.readlink(symlink_path))
3731
3732- @mock.patch("click.hooks.ClickHook._user_home")
3733- def test_upgrade(self, mock_user_home):
3734- mock_user_home.return_value = "/home/test-user"
3735- symlink_path = os.path.join(
3736- self.temp_dir, "org.example.package_test-app_1.0.test")
3737- os.symlink("old-target", symlink_path)
3738- with temp_hooks_dir(self.temp_dir):
3739+ def test_upgrade(self):
3740+ with self.run_in_subprocess(
3741+ "click_get_hooks_dir", "click_get_user_home",
3742+ ) as (enter, preloads):
3743+ enter()
3744+ self._setup_hooks_dir(preloads)
3745+ preloads["click_get_user_home"].return_value = "/home/test-user"
3746+ symlink_path = os.path.join(
3747+ self.temp_dir, "org.example.package_test-app_1.0.test")
3748+ os.symlink("old-target", symlink_path)
3749 os.makedirs(os.path.join(
3750 self.temp_dir, "org.example.package", "1.0"))
3751- user_db = ClickUser(self.db, user="test-user")
3752+ user_db = Click.User.for_user(self.db, "test-user")
3753 user_db.set_version("org.example.package", "1.0")
3754 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3755 print("User-Level: yes", file=f)
3756 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3757- hook = ClickHook.open(self.db, "test")
3758- hook.install_package(
3759- "org.example.package", "1.0", "test-app", "foo/bar",
3760- user="test-user")
3761- target_path = os.path.join(
3762- self.temp_dir, ".click", "users", "test-user",
3763- "org.example.package", "foo", "bar")
3764- self.assertTrue(os.path.islink(symlink_path))
3765- self.assertEqual(target_path, os.readlink(symlink_path))
3766-
3767- @mock.patch("click.hooks.ClickHook._user_home")
3768- def test_remove_package(self, mock_user_home):
3769- mock_user_home.return_value = "/home/test-user"
3770- with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3771- print("User-Level: yes", file=f)
3772- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3773- symlink_path = os.path.join(
3774- self.temp_dir, "org.example.package_test-app_1.0.test")
3775- os.symlink("old-target", symlink_path)
3776- with temp_hooks_dir(self.temp_dir):
3777- hook = ClickHook.open(self.db, "test")
3778- hook.remove_package(
3779- "org.example.package", "1.0", "test-app", user="test-user")
3780- self.assertFalse(os.path.exists(symlink_path))
3781-
3782- @mock.patch("click.hooks.ClickHook._user_home")
3783- def test_install(self, mock_user_home):
3784- mock_user_home.return_value = "/home/test-user"
3785- with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:
3786- print("User-Level: yes", file=f)
3787- print("Pattern: %s/${id}.new" % self.temp_dir, file=f)
3788- user_db = ClickUser(self.db, user="test-user")
3789- with mkfile_utf8(os.path.join(
3790- self.temp_dir, "test-1", "1.0", ".click", "info",
3791- "test-1.manifest")) as f:
3792- json.dump({
3793- "maintainer":
3794- b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),
3795- "hooks": {"test1-app": {"new": "target-1"}},
3796- }, f, ensure_ascii=False)
3797- user_db.set_version("test-1", "1.0")
3798- with mkfile_utf8(os.path.join(
3799- self.temp_dir, "test-2", "2.0", ".click", "info",
3800- "test-2.manifest")) as f:
3801- json.dump({
3802- "maintainer":
3803- b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),
3804- "hooks": {"test1-app": {"new": "target-2"}},
3805- }, f, ensure_ascii=False)
3806- user_db.set_version("test-2", "2.0")
3807- with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):
3808- hook = ClickHook.open(self.db, "new")
3809- hook.install()
3810- path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")
3811- self.assertTrue(os.path.lexists(path_1))
3812- self.assertEqual(
3813- os.path.join(
3814- self.temp_dir, ".click", "users", "test-user", "test-1",
3815- "target-1"),
3816- os.readlink(path_1))
3817- path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")
3818- self.assertTrue(os.path.lexists(path_2))
3819- self.assertEqual(
3820- os.path.join(
3821- self.temp_dir, ".click", "users", "test-user", "test-2",
3822- "target-2"),
3823- os.readlink(path_2))
3824-
3825- os.unlink(path_1)
3826- os.unlink(path_2)
3827- hook.install(user="another-user")
3828- self.assertFalse(os.path.lexists(path_1))
3829- self.assertFalse(os.path.lexists(path_2))
3830-
3831- hook.install(user="test-user")
3832- self.assertTrue(os.path.lexists(path_1))
3833- self.assertEqual(
3834- os.path.join(
3835- self.temp_dir, ".click", "users", "test-user", "test-1",
3836- "target-1"),
3837- os.readlink(path_1))
3838- self.assertTrue(os.path.lexists(path_2))
3839- self.assertEqual(
3840- os.path.join(
3841- self.temp_dir, ".click", "users", "test-user", "test-2",
3842- "target-2"),
3843- os.readlink(path_2))
3844-
3845- @mock.patch("click.hooks.ClickHook._user_home")
3846- def test_remove(self, mock_user_home):
3847- mock_user_home.return_value = "/home/test-user"
3848- with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:
3849- print("User-Level: yes", file=f)
3850- print("Pattern: %s/${id}.old" % self.temp_dir, file=f)
3851- user_db = ClickUser(self.db, user="test-user")
3852- with mkfile(os.path.join(
3853- self.temp_dir, "test-1", "1.0", ".click", "info",
3854- "test-1.manifest")) as f:
3855- json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)
3856- user_db.set_version("test-1", "1.0")
3857- os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
3858- path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")
3859- os.symlink(os.path.join(user_db.path("test-1"), "target-1"), path_1)
3860- with mkfile(os.path.join(
3861- self.temp_dir, "test-2", "2.0", ".click", "info",
3862- "test-2.manifest")) as f:
3863- json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)
3864- user_db.set_version("test-2", "2.0")
3865- path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")
3866- os.symlink(os.path.join(user_db.path("test-2"), "target-2"), path_2)
3867- with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):
3868- hook = ClickHook.open(self.db, "old")
3869- hook.remove()
3870- self.assertFalse(os.path.exists(path_1))
3871- self.assertFalse(os.path.exists(path_2))
3872-
3873- @mock.patch("click.hooks.ClickHook._user_home")
3874- def test_sync(self, mock_user_home):
3875- mock_user_home.return_value = "/home/test-user"
3876- with mkfile(os.path.join(self.temp_dir, "hooks", "test.hook")) as f:
3877- print("User-Level: yes", file=f)
3878- print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3879- user_db = ClickUser(self.db, user="test-user")
3880- with mkfile(os.path.join(
3881- self.temp_dir, "test-1", "1.0", ".click", "info",
3882- "test-1.manifest")) as f:
3883- json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)
3884- user_db.set_version("test-1", "1.0")
3885- with mkfile(os.path.join(
3886- self.temp_dir, "test-2", "1.1", ".click", "info",
3887- "test-2.manifest")) as f:
3888- json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)
3889- user_db.set_version("test-2", "1.1")
3890- path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")
3891- os.symlink(
3892- os.path.join(
3893- self.temp_dir, ".click", "users", "test-user", "test-1",
3894- "target-1"),
3895- path_1)
3896- path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")
3897- path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")
3898- os.symlink(
3899- os.path.join(
3900- self.temp_dir, ".click", "users", "test-user", "test-3",
3901- "target-3"),
3902- path_3)
3903- with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):
3904- hook = ClickHook.open(self.db, "test")
3905- hook.sync(user="test-user")
3906- self.assertTrue(os.path.lexists(path_1))
3907- self.assertEqual(
3908- os.path.join(
3909- self.temp_dir, ".click", "users", "test-user", "test-1",
3910- "target-1"),
3911- os.readlink(path_1))
3912- self.assertTrue(os.path.lexists(path_2))
3913- self.assertEqual(
3914- os.path.join(
3915- self.temp_dir, ".click", "users", "test-user", "test-2",
3916- "target-2"),
3917- os.readlink(path_2))
3918- self.assertFalse(os.path.lexists(path_3))
3919+ hook = Click.Hook.open(self.db, "test")
3920+ hook.install_package(
3921+ "org.example.package", "1.0", "test-app", "foo/bar",
3922+ user_name="test-user")
3923+ target_path = os.path.join(
3924+ self.temp_dir, ".click", "users", "test-user",
3925+ "org.example.package", "foo", "bar")
3926+ self.assertTrue(os.path.islink(symlink_path))
3927+ self.assertEqual(target_path, os.readlink(symlink_path))
3928+
3929+ def test_remove_package(self):
3930+ with self.run_in_subprocess(
3931+ "click_get_hooks_dir", "click_get_user_home",
3932+ ) as (enter, preloads):
3933+ enter()
3934+ self._setup_hooks_dir(preloads)
3935+ preloads["click_get_user_home"].return_value = "/home/test-user"
3936+ with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
3937+ print("User-Level: yes", file=f)
3938+ print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
3939+ symlink_path = os.path.join(
3940+ self.temp_dir, "org.example.package_test-app_1.0.test")
3941+ os.symlink("old-target", symlink_path)
3942+ hook = Click.Hook.open(self.db, "test")
3943+ hook.remove_package(
3944+ "org.example.package", "1.0", "test-app",
3945+ user_name="test-user")
3946+ self.assertFalse(os.path.exists(symlink_path))
3947+
3948+ def test_install(self):
3949+ with self.run_in_subprocess(
3950+ "click_get_hooks_dir", "click_get_user_home",
3951+ ) as (enter, preloads):
3952+ enter()
3953+ self._setup_hooks_dir(preloads)
3954+ preloads["click_get_user_home"].return_value = "/home/test-user"
3955+ with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:
3956+ print("User-Level: yes", file=f)
3957+ print("Pattern: %s/${id}.new" % self.temp_dir, file=f)
3958+ user_db = Click.User.for_user(self.db, "test-user")
3959+ with mkfile_utf8(os.path.join(
3960+ self.temp_dir, "test-1", "1.0", ".click", "info",
3961+ "test-1.manifest")) as f:
3962+ json.dump({
3963+ "maintainer":
3964+ b"Unic\xc3\xb3de <unicode@example.org>".decode(
3965+ "UTF-8"),
3966+ "hooks": {"test1-app": {"new": "target-1"}},
3967+ }, f, ensure_ascii=False)
3968+ user_db.set_version("test-1", "1.0")
3969+ with mkfile_utf8(os.path.join(
3970+ self.temp_dir, "test-2", "2.0", ".click", "info",
3971+ "test-2.manifest")) as f:
3972+ json.dump({
3973+ "maintainer":
3974+ b"Unic\xc3\xb3de <unicode@example.org>".decode(
3975+ "UTF-8"),
3976+ "hooks": {"test1-app": {"new": "target-2"}},
3977+ }, f, ensure_ascii=False)
3978+ user_db.set_version("test-2", "2.0")
3979+ self._setup_hooks_dir(
3980+ preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
3981+ hook = Click.Hook.open(self.db, "new")
3982+ hook.install()
3983+ path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")
3984+ self.assertTrue(os.path.lexists(path_1))
3985+ self.assertEqual(
3986+ os.path.join(
3987+ self.temp_dir, ".click", "users", "test-user", "test-1",
3988+ "target-1"),
3989+ os.readlink(path_1))
3990+ path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")
3991+ self.assertTrue(os.path.lexists(path_2))
3992+ self.assertEqual(
3993+ os.path.join(
3994+ self.temp_dir, ".click", "users", "test-user", "test-2",
3995+ "target-2"),
3996+ os.readlink(path_2))
3997+
3998+ os.unlink(path_1)
3999+ os.unlink(path_2)
4000+ hook.install(user_name="another-user")
4001+ self.assertFalse(os.path.lexists(path_1))
4002+ self.assertFalse(os.path.lexists(path_2))
4003+
4004+ hook.install(user_name="test-user")
4005+ self.assertTrue(os.path.lexists(path_1))
4006+ self.assertEqual(
4007+ os.path.join(
4008+ self.temp_dir, ".click", "users", "test-user", "test-1",
4009+ "target-1"),
4010+ os.readlink(path_1))
4011+ self.assertTrue(os.path.lexists(path_2))
4012+ self.assertEqual(
4013+ os.path.join(
4014+ self.temp_dir, ".click", "users", "test-user", "test-2",
4015+ "target-2"),
4016+ os.readlink(path_2))
4017+
4018+ def test_remove(self):
4019+ with self.run_in_subprocess(
4020+ "click_get_hooks_dir", "click_get_user_home",
4021+ ) as (enter, preloads):
4022+ enter()
4023+ self._setup_hooks_dir(preloads)
4024+ preloads["click_get_user_home"].return_value = "/home/test-user"
4025+ with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:
4026+ print("User-Level: yes", file=f)
4027+ print("Pattern: %s/${id}.old" % self.temp_dir, file=f)
4028+ user_db = Click.User.for_user(self.db, "test-user")
4029+ with mkfile(os.path.join(
4030+ self.temp_dir, "test-1", "1.0", ".click", "info",
4031+ "test-1.manifest")) as f:
4032+ json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)
4033+ user_db.set_version("test-1", "1.0")
4034+ os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
4035+ path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")
4036+ os.symlink(
4037+ os.path.join(user_db.get_path("test-1"), "target-1"), path_1)
4038+ with mkfile(os.path.join(
4039+ self.temp_dir, "test-2", "2.0", ".click", "info",
4040+ "test-2.manifest")) as f:
4041+ json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)
4042+ user_db.set_version("test-2", "2.0")
4043+ path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")
4044+ os.symlink(
4045+ os.path.join(user_db.get_path("test-2"), "target-2"), path_2)
4046+ self._setup_hooks_dir(
4047+ preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
4048+ hook = Click.Hook.open(self.db, "old")
4049+ hook.remove()
4050+ self.assertFalse(os.path.exists(path_1))
4051+ self.assertFalse(os.path.exists(path_2))
4052+
4053+ def test_sync(self):
4054+ with self.run_in_subprocess(
4055+ "click_get_hooks_dir", "click_get_user_home",
4056+ ) as (enter, preloads):
4057+ enter()
4058+ preloads["click_get_user_home"].return_value = "/home/test-user"
4059+ self._setup_hooks_dir(preloads)
4060+ with mkfile(
4061+ os.path.join(self.temp_dir, "hooks", "test.hook")) as f:
4062+ print("User-Level: yes", file=f)
4063+ print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
4064+ user_db = Click.User.for_user(self.db, "test-user")
4065+ with mkfile(os.path.join(
4066+ self.temp_dir, "test-1", "1.0", ".click", "info",
4067+ "test-1.manifest")) as f:
4068+ json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)
4069+ user_db.set_version("test-1", "1.0")
4070+ with mkfile(os.path.join(
4071+ self.temp_dir, "test-2", "1.1", ".click", "info",
4072+ "test-2.manifest")) as f:
4073+ json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)
4074+ user_db.set_version("test-2", "1.1")
4075+ path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")
4076+ os.symlink(
4077+ os.path.join(
4078+ self.temp_dir, ".click", "users", "test-user", "test-1",
4079+ "target-1"),
4080+ path_1)
4081+ path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")
4082+ path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")
4083+ os.symlink(
4084+ os.path.join(
4085+ self.temp_dir, ".click", "users", "test-user", "test-3",
4086+ "target-3"),
4087+ path_3)
4088+ self._setup_hooks_dir(
4089+ preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
4090+ hook = Click.Hook.open(self.db, "test")
4091+ hook.sync(user_name="test-user")
4092+ self.assertTrue(os.path.lexists(path_1))
4093+ self.assertEqual(
4094+ os.path.join(
4095+ self.temp_dir, ".click", "users", "test-user", "test-1",
4096+ "target-1"),
4097+ os.readlink(path_1))
4098+ self.assertTrue(os.path.lexists(path_2))
4099+ self.assertEqual(
4100+ os.path.join(
4101+ self.temp_dir, ".click", "users", "test-user", "test-2",
4102+ "target-2"),
4103+ os.readlink(path_2))
4104+ self.assertFalse(os.path.lexists(path_3))
4105
4106
4107 class TestPackageInstallHooks(TestClickHookBase):
4108 def test_removes_old_hooks(self):
4109- hooks_dir = os.path.join(self.temp_dir, "hooks")
4110- with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:
4111- print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)
4112- print("Single-Version: yes", file=f)
4113- with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:
4114- print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f)
4115- print("Single-Version: yes", file=f)
4116- print("Hook-Name: yelp", file=f)
4117- with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:
4118- print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f)
4119- print("Single-Version: yes", file=f)
4120- print("Hook-Name: yelp", file=f)
4121- os.mkdir(os.path.join(self.temp_dir, "unity"))
4122- unity_path = os.path.join(self.temp_dir, "unity", "test_app_1.0.scope")
4123- os.symlink("dummy", unity_path)
4124- os.mkdir(os.path.join(self.temp_dir, "yelp"))
4125- yelp_docs_path = os.path.join(
4126- self.temp_dir, "yelp", "docs-test_app_1.0.txt")
4127- os.symlink("dummy", yelp_docs_path)
4128- yelp_other_path = os.path.join(
4129- self.temp_dir, "yelp", "other-test_app_1.0.txt")
4130- os.symlink("dummy", yelp_other_path)
4131- package_dir = os.path.join(self.temp_dir, "test")
4132- with mkfile(os.path.join(
4133- package_dir, "1.0", ".click", "info", "test.manifest")) as f:
4134- json.dump(
4135- {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}},
4136- f)
4137- with mkfile(os.path.join(
4138- package_dir, "1.1", ".click", "info", "test.manifest")) as f:
4139- json.dump({}, f)
4140- with temp_hooks_dir(hooks_dir):
4141- package_install_hooks(self.db, "test", "1.0", "1.1")
4142- self.assertFalse(os.path.lexists(unity_path))
4143- self.assertFalse(os.path.lexists(yelp_docs_path))
4144- self.assertFalse(os.path.lexists(yelp_other_path))
4145+ with self.run_in_subprocess(
4146+ "click_get_hooks_dir") as (enter, preloads):
4147+ enter()
4148+ hooks_dir = os.path.join(self.temp_dir, "hooks")
4149+ self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
4150+ with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:
4151+ print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)
4152+ print("Single-Version: yes", file=f)
4153+ with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:
4154+ print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir,
4155+ file=f)
4156+ print("Single-Version: yes", file=f)
4157+ print("Hook-Name: yelp", file=f)
4158+ with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:
4159+ print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir,
4160+ file=f)
4161+ print("Single-Version: yes", file=f)
4162+ print("Hook-Name: yelp", file=f)
4163+ os.mkdir(os.path.join(self.temp_dir, "unity"))
4164+ unity_path = os.path.join(
4165+ self.temp_dir, "unity", "test_app_1.0.scope")
4166+ os.symlink("dummy", unity_path)
4167+ os.mkdir(os.path.join(self.temp_dir, "yelp"))
4168+ yelp_docs_path = os.path.join(
4169+ self.temp_dir, "yelp", "docs-test_app_1.0.txt")
4170+ os.symlink("dummy", yelp_docs_path)
4171+ yelp_other_path = os.path.join(
4172+ self.temp_dir, "yelp", "other-test_app_1.0.txt")
4173+ os.symlink("dummy", yelp_other_path)
4174+ package_dir = os.path.join(self.temp_dir, "test")
4175+ with mkfile(os.path.join(
4176+ package_dir, "1.0", ".click", "info",
4177+ "test.manifest")) as f:
4178+ json.dump(
4179+ {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}},
4180+ f)
4181+ with mkfile(os.path.join(
4182+ package_dir, "1.1", ".click", "info",
4183+ "test.manifest")) as f:
4184+ json.dump({}, f)
4185+ Click.package_install_hooks(self.db, "test", "1.0", "1.1")
4186+ self.assertFalse(os.path.lexists(unity_path))
4187+ self.assertFalse(os.path.lexists(yelp_docs_path))
4188+ self.assertFalse(os.path.lexists(yelp_other_path))
4189
4190 def test_installs_new_hooks(self):
4191- hooks_dir = os.path.join(self.temp_dir, "hooks")
4192- with mkfile(os.path.join(hooks_dir, "a.hook")) as f:
4193- print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)
4194- with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:
4195- print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)
4196- print("Hook-Name: b", file=f)
4197- with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:
4198- print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)
4199- print("Hook-Name: b", file=f)
4200- os.mkdir(os.path.join(self.temp_dir, "a"))
4201- os.mkdir(os.path.join(self.temp_dir, "b"))
4202- package_dir = os.path.join(self.temp_dir, "test")
4203- with mkfile(os.path.join(
4204- package_dir, "1.0", ".click", "info", "test.manifest")) as f:
4205- json.dump({"hooks": {}}, f)
4206- with mkfile(os.path.join(
4207- package_dir, "1.1", ".click", "info", "test.manifest")) as f:
4208- json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)
4209- with temp_hooks_dir(hooks_dir):
4210- package_install_hooks(self.db, "test", "1.0", "1.1")
4211- self.assertTrue(os.path.lexists(
4212- os.path.join(self.temp_dir, "a", "test_app_1.1.a")))
4213- self.assertTrue(os.path.lexists(
4214- os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))
4215- self.assertTrue(os.path.lexists(
4216- os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))
4217+ with self.run_in_subprocess(
4218+ "click_get_hooks_dir") as (enter, preloads):
4219+ enter()
4220+ hooks_dir = os.path.join(self.temp_dir, "hooks")
4221+ self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
4222+ with mkfile(os.path.join(hooks_dir, "a.hook")) as f:
4223+ print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)
4224+ with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:
4225+ print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)
4226+ print("Hook-Name: b", file=f)
4227+ with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:
4228+ print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)
4229+ print("Hook-Name: b", file=f)
4230+ os.mkdir(os.path.join(self.temp_dir, "a"))
4231+ os.mkdir(os.path.join(self.temp_dir, "b"))
4232+ package_dir = os.path.join(self.temp_dir, "test")
4233+ with mkfile(os.path.join(
4234+ package_dir, "1.0", ".click", "info",
4235+ "test.manifest")) as f:
4236+ json.dump({"hooks": {}}, f)
4237+ with mkfile(os.path.join(
4238+ package_dir, "1.1", ".click", "info",
4239+ "test.manifest")) as f:
4240+ json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)
4241+ Click.package_install_hooks(self.db, "test", "1.0", "1.1")
4242+ self.assertTrue(os.path.lexists(
4243+ os.path.join(self.temp_dir, "a", "test_app_1.1.a")))
4244+ self.assertTrue(os.path.lexists(
4245+ os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))
4246+ self.assertTrue(os.path.lexists(
4247+ os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))
4248
4249 def test_upgrades_existing_hooks(self):
4250- hooks_dir = os.path.join(self.temp_dir, "hooks")
4251- with mkfile(os.path.join(hooks_dir, "a.hook")) as f:
4252- print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)
4253- print("Single-Version: yes", file=f)
4254- with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:
4255- print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)
4256- print("Single-Version: yes", file=f)
4257- print("Hook-Name: b", file=f)
4258- with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:
4259- print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)
4260- print("Single-Version: yes", file=f)
4261- print("Hook-Name: b", file=f)
4262- with mkfile(os.path.join(hooks_dir, "c.hook")) as f:
4263- print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f)
4264- print("Single-Version: yes", file=f)
4265- os.mkdir(os.path.join(self.temp_dir, "a"))
4266- a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a")
4267- os.symlink("dummy", a_path)
4268- os.mkdir(os.path.join(self.temp_dir, "b"))
4269- b_irrelevant_path = os.path.join(
4270- self.temp_dir, "b", "1-test_other-app_1.0.b")
4271- os.symlink("dummy", b_irrelevant_path)
4272- b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b")
4273- os.symlink("dummy", b_1_path)
4274- b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b")
4275- os.symlink("dummy", b_2_path)
4276- os.mkdir(os.path.join(self.temp_dir, "c"))
4277- package_dir = os.path.join(self.temp_dir, "test")
4278- with mkfile(os.path.join(
4279- package_dir, "1.0", ".click", "info", "test.manifest")) as f:
4280- json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)
4281- with mkfile(os.path.join(
4282- package_dir, "1.1", ".click", "info", "test.manifest")) as f:
4283- json.dump(
4284- {"hooks": {
4285- "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}}
4286- }, f)
4287- with temp_hooks_dir(hooks_dir):
4288- package_install_hooks(self.db, "test", "1.0", "1.1")
4289- self.assertFalse(os.path.lexists(a_path))
4290- self.assertTrue(os.path.lexists(b_irrelevant_path))
4291- self.assertFalse(os.path.lexists(b_1_path))
4292- self.assertFalse(os.path.lexists(b_2_path))
4293- self.assertTrue(os.path.lexists(
4294- os.path.join(self.temp_dir, "a", "test_app_1.1.a")))
4295- self.assertTrue(os.path.lexists(
4296- os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))
4297- self.assertTrue(os.path.lexists(
4298- os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))
4299- self.assertTrue(os.path.lexists(
4300- os.path.join(self.temp_dir, "c", "test_app_1.1.c")))
4301+ with self.run_in_subprocess(
4302+ "click_get_hooks_dir") as (enter, preloads):
4303+ enter()
4304+ hooks_dir = os.path.join(self.temp_dir, "hooks")
4305+ self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
4306+ with mkfile(os.path.join(hooks_dir, "a.hook")) as f:
4307+ print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)
4308+ print("Single-Version: yes", file=f)
4309+ with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:
4310+ print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)
4311+ print("Single-Version: yes", file=f)
4312+ print("Hook-Name: b", file=f)
4313+ with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:
4314+ print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)
4315+ print("Single-Version: yes", file=f)
4316+ print("Hook-Name: b", file=f)
4317+ with mkfile(os.path.join(hooks_dir, "c.hook")) as f:
4318+ print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f)
4319+ print("Single-Version: yes", file=f)
4320+ os.mkdir(os.path.join(self.temp_dir, "a"))
4321+ a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a")
4322+ os.symlink("dummy", a_path)
4323+ os.mkdir(os.path.join(self.temp_dir, "b"))
4324+ b_irrelevant_path = os.path.join(
4325+ self.temp_dir, "b", "1-test_other-app_1.0.b")
4326+ os.symlink("dummy", b_irrelevant_path)
4327+ b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b")
4328+ os.symlink("dummy", b_1_path)
4329+ b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b")
4330+ os.symlink("dummy", b_2_path)
4331+ os.mkdir(os.path.join(self.temp_dir, "c"))
4332+ package_dir = os.path.join(self.temp_dir, "test")
4333+ with mkfile(os.path.join(
4334+ package_dir, "1.0", ".click", "info",
4335+ "test.manifest")) as f:
4336+ json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)
4337+ with mkfile(os.path.join(
4338+ package_dir, "1.1", ".click", "info",
4339+ "test.manifest")) as f:
4340+ json.dump(
4341+ {"hooks": {
4342+ "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}}
4343+ }, f)
4344+ Click.package_install_hooks(self.db, "test", "1.0", "1.1")
4345+ self.assertFalse(os.path.lexists(a_path))
4346+ self.assertTrue(os.path.lexists(b_irrelevant_path))
4347+ self.assertFalse(os.path.lexists(b_1_path))
4348+ self.assertFalse(os.path.lexists(b_2_path))
4349+ self.assertTrue(os.path.lexists(
4350+ os.path.join(self.temp_dir, "a", "test_app_1.1.a")))
4351+ self.assertTrue(os.path.lexists(
4352+ os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))
4353+ self.assertTrue(os.path.lexists(
4354+ os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))
4355+ self.assertTrue(os.path.lexists(
4356+ os.path.join(self.temp_dir, "c", "test_app_1.1.c")))
4357
4358
4359 class TestPackageRemoveHooks(TestClickHookBase):
4360 def test_removes_hooks(self):
4361- hooks_dir = os.path.join(self.temp_dir, "hooks")
4362- with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:
4363- print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)
4364- with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:
4365- print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f)
4366- print("Hook-Name: yelp", file=f)
4367- with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:
4368- print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f)
4369- print("Hook-Name: yelp", file=f)
4370- os.mkdir(os.path.join(self.temp_dir, "unity"))
4371- unity_path = os.path.join(self.temp_dir, "unity", "test_app_1.0.scope")
4372- os.symlink("dummy", unity_path)
4373- os.mkdir(os.path.join(self.temp_dir, "yelp"))
4374- yelp_docs_path = os.path.join(
4375- self.temp_dir, "yelp", "docs-test_app_1.0.txt")
4376- os.symlink("dummy", yelp_docs_path)
4377- yelp_other_path = os.path.join(
4378- self.temp_dir, "yelp", "other-test_app_1.0.txt")
4379- os.symlink("dummy", yelp_other_path)
4380- package_dir = os.path.join(self.temp_dir, "test")
4381- with mkfile(os.path.join(
4382- package_dir, "1.0", ".click", "info", "test.manifest")) as f:
4383- json.dump(
4384- {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}},
4385- f)
4386- with temp_hooks_dir(hooks_dir):
4387- package_remove_hooks(self.db, "test", "1.0")
4388- self.assertFalse(os.path.lexists(unity_path))
4389- self.assertFalse(os.path.lexists(yelp_docs_path))
4390- self.assertFalse(os.path.lexists(yelp_other_path))
4391+ with self.run_in_subprocess(
4392+ "click_get_hooks_dir") as (enter, preloads):
4393+ enter()
4394+ hooks_dir = os.path.join(self.temp_dir, "hooks")
4395+ self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
4396+ with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:
4397+ print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)
4398+ with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:
4399+ print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir,
4400+ file=f)
4401+ print("Hook-Name: yelp", file=f)
4402+ with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:
4403+ print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir,
4404+ file=f)
4405+ print("Hook-Name: yelp", file=f)
4406+ os.mkdir(os.path.join(self.temp_dir, "unity"))
4407+ unity_path = os.path.join(
4408+ self.temp_dir, "unity", "test_app_1.0.scope")
4409+ os.symlink("dummy", unity_path)
4410+ os.mkdir(os.path.join(self.temp_dir, "yelp"))
4411+ yelp_docs_path = os.path.join(
4412+ self.temp_dir, "yelp", "docs-test_app_1.0.txt")
4413+ os.symlink("dummy", yelp_docs_path)
4414+ yelp_other_path = os.path.join(
4415+ self.temp_dir, "yelp", "other-test_app_1.0.txt")
4416+ os.symlink("dummy", yelp_other_path)
4417+ package_dir = os.path.join(self.temp_dir, "test")
4418+ with mkfile(os.path.join(
4419+ package_dir, "1.0", ".click", "info",
4420+ "test.manifest")) as f:
4421+ json.dump(
4422+ {"hooks": {
4423+ "app": {"yelp": "foo.txt", "unity": "foo.scope"}}
4424+ }, f)
4425+ Click.package_remove_hooks(self.db, "test", "1.0")
4426+ self.assertFalse(os.path.lexists(unity_path))
4427+ self.assertFalse(os.path.lexists(yelp_docs_path))
4428+ self.assertFalse(os.path.lexists(yelp_other_path))
4429
4430=== modified file 'click/tests/test_install.py'
4431--- click/tests/test_install.py 2014-01-22 14:02:33 +0000
4432+++ click/tests/test_install.py 2014-03-06 07:04:44 +0000
4433@@ -34,10 +34,10 @@
4434 from unittest import skipUnless
4435
4436 from debian.deb822 import Deb822
4437+from gi.repository import Click
4438
4439-from click import install, osextras
4440+from click import install
4441 from click.build import ClickBuilder
4442-from click.database import ClickDB
4443 from click.install import (
4444 ClickInstaller,
4445 ClickInstallerAuditError,
4446@@ -68,7 +68,8 @@
4447 def setUp(self):
4448 super(TestClickInstaller, self).setUp()
4449 self.use_temp_dir()
4450- self.db = ClickDB(self.temp_dir)
4451+ self.db = Click.DB()
4452+ self.db.add(self.temp_dir)
4453
4454 def make_fake_package(self, control_fields=None, manifest=None,
4455 control_scripts=None, data_files=None):
4456@@ -90,7 +91,7 @@
4457 for name, contents in control_scripts.items():
4458 with mkfile(os.path.join(control_dir, name)) as script:
4459 script.write(contents)
4460- osextras.ensuredir(data_dir)
4461+ Click.ensuredir(data_dir)
4462 for name, path in data_files.items():
4463 if path is None:
4464 touch(os.path.join(data_dir, name))
4465@@ -108,11 +109,11 @@
4466 old_dir = install.frameworks_dir
4467 try:
4468 install.frameworks_dir = os.path.join(self.temp_dir, "frameworks")
4469- osextras.ensuredir(install.frameworks_dir)
4470+ Click.ensuredir(install.frameworks_dir)
4471 touch(os.path.join(install.frameworks_dir, "%s.framework" % name))
4472 yield
4473 finally:
4474- osextras.unlink_force(
4475+ Click.unlink_force(
4476 os.path.join(install.frameworks_dir, "%s.framework" % name))
4477 install.frameworks_dir = old_dir
4478
4479@@ -342,7 +343,7 @@
4480 @skipUnless(
4481 os.path.exists(ClickInstaller(None)._preload_path()),
4482 "preload bits not built; installing packages will fail")
4483- @mock.patch("click.install.package_install_hooks")
4484+ @mock.patch("gi.repository.Click.package_install_hooks")
4485 def test_install(self, mock_package_install_hooks):
4486 path = self.make_fake_package(
4487 control_fields={
4488@@ -361,7 +362,8 @@
4489 control_scripts={"preinst": static_preinst},
4490 data_files={"foo": None})
4491 root = os.path.join(self.temp_dir, "root")
4492- db = ClickDB(root)
4493+ db = Click.DB()
4494+ db.add(root)
4495 installer = ClickInstaller(db)
4496 with self.make_framework("ubuntu-sdk-13.10"), \
4497 mock_quiet_subprocess_call():
4498@@ -425,7 +427,9 @@
4499 control_scripts={"preinst": static_preinst},
4500 data_files={"foo": None})
4501 root = os.path.join(self.temp_dir, "root")
4502- installer = ClickInstaller(ClickDB(root))
4503+ db = Click.DB()
4504+ db.add(root)
4505+ installer = ClickInstaller(db)
4506 with self.make_framework("ubuntu-sdk-13.10"), \
4507 mock.patch("subprocess.call") as mock_call:
4508 mock_call.side_effect = call_side_effect
4509@@ -437,8 +441,9 @@
4510 @skipUnless(
4511 os.path.exists(ClickInstaller(None)._preload_path()),
4512 "preload bits not built; installing packages will fail")
4513- @mock.patch("click.install.package_install_hooks")
4514+ @mock.patch("gi.repository.Click.package_install_hooks")
4515 def test_upgrade(self, mock_package_install_hooks):
4516+ os.environ["TEST_QUIET"] = "1"
4517 path = self.make_fake_package(
4518 control_fields={
4519 "Package": "test-package",
4520@@ -460,7 +465,8 @@
4521 inst_dir = os.path.join(package_dir, "current")
4522 os.makedirs(os.path.join(package_dir, "1.0"))
4523 os.symlink("1.0", inst_dir)
4524- db = ClickDB(root)
4525+ db = Click.DB()
4526+ db.add(root)
4527 installer = ClickInstaller(db)
4528 with self.make_framework("ubuntu-sdk-13.10"), \
4529 mock_quiet_subprocess_call():
4530@@ -494,7 +500,7 @@
4531 @skipUnless(
4532 os.path.exists(ClickInstaller(None)._preload_path()),
4533 "preload bits not built; installing packages will fail")
4534- @mock.patch("click.install.package_install_hooks")
4535+ @mock.patch("gi.repository.Click.package_install_hooks")
4536 def test_world_readable(self, mock_package_install_hooks):
4537 owner_only_file = os.path.join(self.temp_dir, "owner-only-file")
4538 touch(owner_only_file)
4539@@ -521,7 +527,8 @@
4540 "world-readable-dir": owner_only_dir,
4541 })
4542 root = os.path.join(self.temp_dir, "root")
4543- db = ClickDB(root)
4544+ db = Click.DB()
4545+ db.add(root)
4546 installer = ClickInstaller(db)
4547 with self.make_framework("ubuntu-sdk-13.10"), \
4548 mock_quiet_subprocess_call():
4549@@ -538,7 +545,7 @@
4550 @skipUnless(
4551 os.path.exists(ClickInstaller(None)._preload_path()),
4552 "preload bits not built; installing packages will fail")
4553- @mock.patch("click.install.package_install_hooks")
4554+ @mock.patch("gi.repository.Click.package_install_hooks")
4555 @mock.patch("click.install.ClickInstaller._dpkg_architecture")
4556 def test_single_architecture(self, mock_dpkg_architecture,
4557 mock_package_install_hooks):
4558@@ -560,7 +567,8 @@
4559 },
4560 control_scripts={"preinst": static_preinst})
4561 root = os.path.join(self.temp_dir, "root")
4562- db = ClickDB(root)
4563+ db = Click.DB()
4564+ db.add(root)
4565 installer = ClickInstaller(db)
4566 with self.make_framework("ubuntu-sdk-13.10"), \
4567 mock_quiet_subprocess_call():
4568@@ -571,7 +579,7 @@
4569 @skipUnless(
4570 os.path.exists(ClickInstaller(None)._preload_path()),
4571 "preload bits not built; installing packages will fail")
4572- @mock.patch("click.install.package_install_hooks")
4573+ @mock.patch("gi.repository.Click.package_install_hooks")
4574 @mock.patch("click.install.ClickInstaller._dpkg_architecture")
4575 def test_multiple_architectures(self, mock_dpkg_architecture,
4576 mock_package_install_hooks):
4577@@ -593,7 +601,8 @@
4578 },
4579 control_scripts={"preinst": static_preinst})
4580 root = os.path.join(self.temp_dir, "root")
4581- db = ClickDB(root)
4582+ db = Click.DB()
4583+ db.add(root)
4584 installer = ClickInstaller(db)
4585 with self.make_framework("ubuntu-sdk-13.10"), \
4586 mock_quiet_subprocess_call():
4587
4588=== modified file 'click/tests/test_osextras.py'
4589--- click/tests/test_osextras.py 2013-09-04 15:59:18 +0000
4590+++ click/tests/test_osextras.py 2014-03-06 07:04:44 +0000
4591@@ -17,35 +17,34 @@
4592
4593 from __future__ import print_function
4594 __all__ = [
4595- 'TestOSExtras',
4596+ 'TestOSExtrasNative',
4597+ 'TestOSExtrasPython',
4598 ]
4599
4600
4601 import os
4602
4603+from gi.repository import Click, GLib
4604+
4605 from click import osextras
4606-from click.tests.helpers import TestCase, touch
4607-
4608-
4609-class TestOSExtras(TestCase):
4610- def setUp(self):
4611- super(TestOSExtras, self).setUp()
4612- self.use_temp_dir()
4613-
4614+from click.tests.helpers import TestCase, mock, touch
4615+
4616+
4617+class TestOSExtrasBaseMixin:
4618 def test_ensuredir_previously_missing(self):
4619 new_dir = os.path.join(self.temp_dir, "dir")
4620- osextras.ensuredir(new_dir)
4621+ self.mod.ensuredir(new_dir)
4622 self.assertTrue(os.path.isdir(new_dir))
4623
4624 def test_ensuredir_previously_present(self):
4625 new_dir = os.path.join(self.temp_dir, "dir")
4626 os.mkdir(new_dir)
4627- osextras.ensuredir(new_dir)
4628+ self.mod.ensuredir(new_dir)
4629 self.assertTrue(os.path.isdir(new_dir))
4630
4631 def test_find_on_path_missing_environment(self):
4632 os.environ.pop("PATH", None)
4633- self.assertFalse(osextras.find_on_path("ls"))
4634+ self.assertFalse(self.mod.find_on_path("ls"))
4635
4636 def test_find_on_path_present_executable(self):
4637 bin_dir = os.path.join(self.temp_dir, "bin")
4638@@ -53,69 +52,112 @@
4639 touch(program)
4640 os.chmod(program, 0o755)
4641 os.environ["PATH"] = bin_dir
4642- self.assertTrue(osextras.find_on_path("program"))
4643+ self.assertTrue(self.mod.find_on_path("program"))
4644
4645 def test_find_on_path_present_not_executable(self):
4646 bin_dir = os.path.join(self.temp_dir, "bin")
4647 touch(os.path.join(bin_dir, "program"))
4648 os.environ["PATH"] = bin_dir
4649- self.assertFalse(osextras.find_on_path("program"))
4650-
4651- def test_listdir_directory_present(self):
4652- new_dir = os.path.join(self.temp_dir, "dir")
4653- touch(os.path.join(new_dir, "file"))
4654- self.assertEqual(["file"], osextras.listdir_force(new_dir))
4655-
4656- def test_listdir_directory_missing(self):
4657- new_dir = os.path.join(self.temp_dir, "dir")
4658- self.assertEqual([], osextras.listdir_force(new_dir))
4659-
4660- def test_listdir_oserror(self):
4661- not_dir = os.path.join(self.temp_dir, "file")
4662- touch(not_dir)
4663- self.assertRaises(OSError, osextras.listdir_force, not_dir)
4664+ self.assertFalse(self.mod.find_on_path("program"))
4665+
4666+ def test_find_on_path_requires_regular_file(self):
4667+ bin_dir = os.path.join(self.temp_dir, "bin")
4668+ self.mod.ensuredir(os.path.join(bin_dir, "subdir"))
4669+ os.environ["PATH"] = bin_dir
4670+ self.assertFalse(self.mod.find_on_path("subdir"))
4671
4672 def test_unlink_file_present(self):
4673 path = os.path.join(self.temp_dir, "file")
4674 touch(path)
4675- osextras.unlink_force(path)
4676+ self.mod.unlink_force(path)
4677 self.assertFalse(os.path.exists(path))
4678
4679 def test_unlink_file_missing(self):
4680 path = os.path.join(self.temp_dir, "file")
4681- osextras.unlink_force(path)
4682+ self.mod.unlink_force(path)
4683 self.assertFalse(os.path.exists(path))
4684
4685- def test_unlink_oserror(self):
4686- path = os.path.join(self.temp_dir, "dir")
4687- os.mkdir(path)
4688- self.assertRaises(OSError, osextras.unlink_force, path)
4689-
4690 def test_symlink_file_present(self):
4691 path = os.path.join(self.temp_dir, "link")
4692 touch(path)
4693- osextras.symlink_force("source", path)
4694+ self.mod.symlink_force("source", path)
4695 self.assertTrue(os.path.islink(path))
4696 self.assertEqual("source", os.readlink(path))
4697
4698 def test_symlink_link_present(self):
4699 path = os.path.join(self.temp_dir, "link")
4700 os.symlink("old", path)
4701- osextras.symlink_force("source", path)
4702+ self.mod.symlink_force("source", path)
4703 self.assertTrue(os.path.islink(path))
4704 self.assertEqual("source", os.readlink(path))
4705
4706 def test_symlink_missing(self):
4707 path = os.path.join(self.temp_dir, "link")
4708- osextras.symlink_force("source", path)
4709+ self.mod.symlink_force("source", path)
4710 self.assertTrue(os.path.islink(path))
4711 self.assertEqual("source", os.readlink(path))
4712
4713 def test_umask(self):
4714 old_mask = os.umask(0o040)
4715 try:
4716- self.assertEqual(0o040, osextras.get_umask())
4717+ self.assertEqual(0o040, self.mod.get_umask())
4718 os.umask(0o002)
4719- self.assertEqual(0o002, osextras.get_umask())
4720+ self.assertEqual(0o002, self.mod.get_umask())
4721 finally:
4722 os.umask(old_mask)
4723+
4724+
4725+class TestOSExtrasNative(TestCase, TestOSExtrasBaseMixin):
4726+ def setUp(self):
4727+ super(TestOSExtrasNative, self).setUp()
4728+ self.use_temp_dir()
4729+ self.mod = Click
4730+
4731+ def test_dir_read_name_directory_present(self):
4732+ new_dir = os.path.join(self.temp_dir, "dir")
4733+ touch(os.path.join(new_dir, "file"))
4734+ d = Click.Dir.open(new_dir, 0)
4735+ self.assertEqual("file", d.read_name())
4736+ self.assertIsNone(d.read_name())
4737+
4738+ def test_dir_read_name_directory_missing(self):
4739+ new_dir = os.path.join(self.temp_dir, "dir")
4740+ d = Click.Dir.open(new_dir, 0)
4741+ self.assertIsNone(d.read_name())
4742+
4743+ def test_dir_open_error(self):
4744+ not_dir = os.path.join(self.temp_dir, "file")
4745+ touch(not_dir)
4746+ self.assertRaisesFileError(
4747+ GLib.FileError.NOTDIR, Click.Dir.open, not_dir, 0)
4748+
4749+ def test_unlink_error(self):
4750+ path = os.path.join(self.temp_dir, "dir")
4751+ os.mkdir(path)
4752+ self.assertRaisesFileError(mock.ANY, self.mod.unlink_force, path)
4753+
4754+
4755+class TestOSExtrasPython(TestCase, TestOSExtrasBaseMixin):
4756+ def setUp(self):
4757+ super(TestOSExtrasPython, self).setUp()
4758+ self.use_temp_dir()
4759+ self.mod = osextras
4760+
4761+ def test_listdir_directory_present(self):
4762+ new_dir = os.path.join(self.temp_dir, "dir")
4763+ touch(os.path.join(new_dir, "file"))
4764+ self.assertEqual(["file"], osextras.listdir_force(new_dir))
4765+
4766+ def test_listdir_directory_missing(self):
4767+ new_dir = os.path.join(self.temp_dir, "dir")
4768+ self.assertEqual([], osextras.listdir_force(new_dir))
4769+
4770+ def test_listdir_oserror(self):
4771+ not_dir = os.path.join(self.temp_dir, "file")
4772+ touch(not_dir)
4773+ self.assertRaises(OSError, osextras.listdir_force, not_dir)
4774+
4775+ def test_unlink_oserror(self):
4776+ path = os.path.join(self.temp_dir, "dir")
4777+ os.mkdir(path)
4778+ self.assertRaises(OSError, self.mod.unlink_force, path)
4779
4780=== added file 'click/tests/test_query.py'
4781--- click/tests/test_query.py 1970-01-01 00:00:00 +0000
4782+++ click/tests/test_query.py 2014-03-06 07:04:44 +0000
4783@@ -0,0 +1,52 @@
4784+# Copyright (C) 2014 Canonical Ltd.
4785+# Author: Colin Watson <cjwatson@ubuntu.com>
4786+
4787+# This program is free software: you can redistribute it and/or modify
4788+# it under the terms of the GNU General Public License as published by
4789+# the Free Software Foundation; version 3 of the License.
4790+#
4791+# This program is distributed in the hope that it will be useful,
4792+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4793+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4794+# GNU General Public License for more details.
4795+#
4796+# You should have received a copy of the GNU General Public License
4797+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4798+
4799+"""Unit tests for click.query."""
4800+
4801+from __future__ import print_function
4802+__all__ = [
4803+ 'TestQuery',
4804+ ]
4805+
4806+
4807+import os
4808+
4809+from gi.repository import Click
4810+
4811+from click.tests.helpers import TestCase, touch
4812+
4813+
4814+class TestQuery(TestCase):
4815+ def setUp(self):
4816+ super(TestQuery, self).setUp()
4817+ self.use_temp_dir()
4818+
4819+ def test_find_package_directory_missing(self):
4820+ path = os.path.join(self.temp_dir, "nonexistent")
4821+ self.assertRaisesQueryError(
4822+ Click.QueryError.PATH, Click.find_package_directory, path)
4823+
4824+ def test_find_package_directory(self):
4825+ info = os.path.join(self.temp_dir, ".click", "info")
4826+ path = os.path.join(self.temp_dir, "file")
4827+ Click.ensuredir(info)
4828+ touch(path)
4829+ pkgdir = Click.find_package_directory(path)
4830+ self.assertEqual(self.temp_dir, pkgdir)
4831+
4832+ def test_find_package_directory_outside(self):
4833+ self.assertRaisesQueryError(
4834+ Click.QueryError.NO_PACKAGE_DIR, Click.find_package_directory,
4835+ "/bin")
4836
4837=== modified file 'click/tests/test_user.py'
4838--- click/tests/test_user.py 2014-03-01 23:54:08 +0000
4839+++ click/tests/test_user.py 2014-03-06 07:04:44 +0000
4840@@ -25,24 +25,26 @@
4841
4842 import os
4843
4844-from click.database import ClickDB
4845+from gi.repository import Click
4846+
4847 from click.tests.helpers import TestCase
4848-from click.user import ClickUser
4849
4850
4851 class TestClickUser(TestCase):
4852 def setUp(self):
4853 super(TestClickUser, self).setUp()
4854 self.use_temp_dir()
4855- self.db = ClickDB(self.temp_dir, use_system=False)
4856+ self.db = Click.DB()
4857+ self.db.add(self.temp_dir)
4858
4859 def _setUpMultiDB(self):
4860- self.multi_db = ClickDB(use_system=False)
4861+ self.multi_db = Click.DB()
4862 self.multi_db.add(os.path.join(self.temp_dir, "custom"))
4863 self.multi_db.add(os.path.join(self.temp_dir, "click"))
4864 user_dbs = [
4865- os.path.join(d.root, ".click", "users", "user")
4866- for d in self.multi_db
4867+ os.path.join(
4868+ self.multi_db.get(i).props.root, ".click", "users", "user")
4869+ for i in range(self.multi_db.props.size)
4870 ]
4871 a_1_0 = os.path.join(self.temp_dir, "custom", "a", "1.0")
4872 os.makedirs(a_1_0)
4873@@ -58,80 +60,90 @@
4874 os.makedirs(user_dbs[1])
4875 os.symlink(a_1_1, os.path.join(user_dbs[1], "a"))
4876 os.symlink(c_0_1, os.path.join(user_dbs[1], "c"))
4877- return user_dbs, ClickUser(self.multi_db, "user")
4878-
4879- def test_overlay_db(self):
4880+ return user_dbs, Click.User.for_user(self.multi_db, "user")
4881+
4882+ def test_new_no_db(self):
4883+ with self.run_in_subprocess(
4884+ "click_get_db_dir", "g_get_user_name") as (enter, preloads):
4885+ enter()
4886+ preloads["click_get_db_dir"].side_effect = (
4887+ lambda: self.make_string(self.temp_dir))
4888+ preloads["g_get_user_name"].side_effect = (
4889+ lambda: self.make_string("test-user"))
4890+ db_root = os.path.join(self.temp_dir, "db")
4891+ os.makedirs(db_root)
4892+ with open(os.path.join(self.temp_dir, "db.conf"), "w") as f:
4893+ print("[Click Database]", file=f)
4894+ print("root = %s" % db_root, file=f)
4895+ registry = Click.User.for_user()
4896+ self.assertEqual(
4897+ os.path.join(db_root, ".click", "users", "test-user"),
4898+ registry.get_overlay_db())
4899+
4900+ def test_get_overlay_db(self):
4901 self.assertEqual(
4902 os.path.join(self.temp_dir, ".click", "users", "user"),
4903- ClickUser(self.db, "user").overlay_db)
4904-
4905- def test_iter_missing(self):
4906- db = ClickDB(
4907- os.path.join(self.temp_dir, "nonexistent"), use_system=False)
4908- registry = ClickUser(db)
4909- self.assertEqual([], list(registry))
4910-
4911- def test_iter(self):
4912- registry = ClickUser(self.db, "user")
4913- os.makedirs(registry.overlay_db)
4914- os.symlink("/1.0", os.path.join(registry.overlay_db, "a"))
4915- os.symlink("/1.1", os.path.join(registry.overlay_db, "b"))
4916- self.assertCountEqual(["a", "b"], list(registry))
4917-
4918- def test_iter_multiple_root(self):
4919- _, registry = self._setUpMultiDB()
4920- self.assertCountEqual(["a", "b", "c"], list(registry))
4921-
4922- def test_len_missing(self):
4923- db = ClickDB(
4924- os.path.join(self.temp_dir, "nonexistent"), use_system=False)
4925- registry = ClickUser(db)
4926- self.assertEqual(0, len(registry))
4927-
4928- def test_len(self):
4929- registry = ClickUser(self.db, "user")
4930- os.makedirs(registry.overlay_db)
4931- os.symlink("/1.0", os.path.join(registry.overlay_db, "a"))
4932- os.symlink("/1.1", os.path.join(registry.overlay_db, "b"))
4933- self.assertEqual(2, len(registry))
4934-
4935- def test_len_multiple_root(self):
4936- _, registry = self._setUpMultiDB()
4937- self.assertEqual(3, len(registry))
4938-
4939- def test_getitem_missing(self):
4940- registry = ClickUser(self.db, "user")
4941- self.assertRaises(KeyError, registry.__getitem__, "a")
4942-
4943- def test_getitem(self):
4944- registry = ClickUser(self.db, "user")
4945- os.makedirs(registry.overlay_db)
4946- os.symlink("/1.0", os.path.join(registry.overlay_db, "a"))
4947- self.assertEqual("1.0", registry["a"])
4948-
4949- def test_getitem_multiple_root(self):
4950- _, registry = self._setUpMultiDB()
4951- self.assertEqual("1.1", registry["a"])
4952- self.assertEqual("2.0", registry["b"])
4953- self.assertEqual("0.1", registry["c"])
4954+ Click.User.for_user(self.db, "user").get_overlay_db())
4955+
4956+ def test_get_package_names_missing(self):
4957+ db = Click.DB()
4958+ db.add(os.path.join(self.temp_dir, "nonexistent"))
4959+ registry = Click.User.for_user(db)
4960+ self.assertEqual([], list(registry.get_package_names()))
4961+
4962+ def test_get_package_names(self):
4963+ registry = Click.User.for_user(self.db, "user")
4964+ os.makedirs(registry.get_overlay_db())
4965+ os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a"))
4966+ os.symlink("/1.1", os.path.join(registry.get_overlay_db(), "b"))
4967+ self.assertCountEqual(["a", "b"], list(registry.get_package_names()))
4968+
4969+ def test_get_package_names_multiple_root(self):
4970+ _, registry = self._setUpMultiDB()
4971+ self.assertCountEqual(
4972+ ["a", "b", "c"], list(registry.get_package_names()))
4973+
4974+ def test_get_version_missing(self):
4975+ registry = Click.User.for_user(self.db, "user")
4976+ self.assertRaisesUserError(
4977+ Click.UserError.NO_SUCH_PACKAGE, registry.get_version, "a")
4978+ self.assertFalse(registry.has_package_name("a"))
4979+
4980+ def test_get_version(self):
4981+ registry = Click.User.for_user(self.db, "user")
4982+ os.makedirs(registry.get_overlay_db())
4983+ os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a"))
4984+ self.assertEqual("1.0", registry.get_version("a"))
4985+ self.assertTrue(registry.has_package_name("a"))
4986+
4987+ def test_get_version_multiple_root(self):
4988+ _, registry = self._setUpMultiDB()
4989+ self.assertEqual("1.1", registry.get_version("a"))
4990+ self.assertEqual("2.0", registry.get_version("b"))
4991+ self.assertEqual("0.1", registry.get_version("c"))
4992+ self.assertTrue(registry.has_package_name("a"))
4993+ self.assertTrue(registry.has_package_name("b"))
4994+ self.assertTrue(registry.has_package_name("c"))
4995
4996 def test_set_version_missing_target(self):
4997- registry = ClickUser(self.db, "user")
4998- self.assertRaises(KeyError, registry.set_version, "a", "1.0")
4999+ registry = Click.User.for_user(self.db, "user")
5000+ self.assertRaisesDatabaseError(
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: