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
=== modified file '.bzrignore'
--- .bzrignore 2014-03-05 16:36:20 +0000
+++ .bzrignore 2014-03-06 07:04:44 +0000
@@ -27,6 +27,8 @@
27dist27dist
28.tox28.tox
29click/paths.py29click/paths.py
30click/tests/config.py
31click/tests/preload.gir
30build-aux/compile32build-aux/compile
31build-aux/config.guess33build-aux/config.guess
32build-aux/config.rpath34build-aux/config.rpath
@@ -44,6 +46,9 @@
44debian/click46debian/click
45debian/click-doc47debian/click-doc
46debian/files48debian/files
49debian/gir1.2-click-0.4
50debian/libclick-0.4-0
51debian/libclick-0.4-dev
47debian/packagekit-plugin-click52debian/packagekit-plugin-click
48debian/python3-click53debian/python3-click
49debian/tmp54debian/tmp
@@ -51,6 +56,15 @@
51init/systemd/click-user-hooks.service56init/systemd/click-user-hooks.service
52init/upstart/click-system-hooks.conf57init/upstart/click-system-hooks.conf
53init/upstart/click-user-hooks.conf58init/upstart/click-user-hooks.conf
59lib/click/*.c
60lib/click/*.gir
61lib/click/*.typelib
62lib/click/click.h
63lib/click/click-0.4.pc
64lib/click/click-0.4.vapi
65lib/click/libclick_0_4_la_vala.stamp
66lib/click/paths.vala
67lib/click/valac-wrapper
54m4/*68m4/*
55pk-plugin/com.ubuntu.click.policy69pk-plugin/com.ubuntu.click.policy
56po/.intltool-merge-cache70po/.intltool-merge-cache
5771
=== modified file 'Makefile.am'
--- Makefile.am 2014-03-05 16:36:20 +0000
+++ Makefile.am 2014-03-06 07:04:44 +0000
@@ -1,4 +1,4 @@
1SUBDIRS = click conf debhelper init po preload schroot1SUBDIRS = lib preload click conf debhelper init po schroot
2if PACKAGEKIT2if PACKAGEKIT
3SUBDIRS += pk-plugin3SUBDIRS += pk-plugin
4endif4endif
55
=== modified file 'click/Makefile.am'
--- click/Makefile.am 2013-09-03 12:50:11 +0000
+++ click/Makefile.am 2014-03-06 07:04:44 +0000
@@ -1,3 +1,5 @@
1SUBDIRS = tests
2
1noinst_SCRIPTS = paths.py3noinst_SCRIPTS = paths.py
2CLEANFILES = $(noinst_SCRIPTS)4CLEANFILES = $(noinst_SCRIPTS)
35
46
=== modified file 'click/commands/desktophook.py'
--- click/commands/desktophook.py 2013-09-30 09:14:02 +0000
+++ click/commands/desktophook.py 2014-03-06 07:04:44 +0000
@@ -23,8 +23,9 @@
23from optparse import OptionParser23from optparse import OptionParser
24import os24import os
2525
26from gi.repository import Click
27
26from click import osextras28from click import osextras
27from click.query import find_package_directory
2829
2930
30COMMENT = \31COMMENT = \
@@ -71,7 +72,7 @@
7172
72def read_hooks_for(path, package, app_name):73def read_hooks_for(path, package, app_name):
73 try:74 try:
74 directory = find_package_directory(path)75 directory = Click.find_package_directory(path)
75 manifest_path = os.path.join(76 manifest_path = os.path.join(
76 directory, ".click", "info", "%s.manifest" % package)77 directory, ".click", "info", "%s.manifest" % package)
77 with io.open(manifest_path, encoding="UTF-8") as manifest:78 with io.open(manifest_path, encoding="UTF-8") as manifest:
@@ -111,10 +112,10 @@
111# TODO: This is a very crude .desktop file mangler; we should instead112# TODO: This is a very crude .desktop file mangler; we should instead
112# implement proper (de)serialisation.113# implement proper (de)serialisation.
113def write_desktop_file(target_path, source_path, profile):114def write_desktop_file(target_path, source_path, profile):
114 osextras.ensuredir(os.path.dirname(target_path))115 Click.ensuredir(os.path.dirname(target_path))
115 with io.open(source_path, encoding="UTF-8") as source, \116 with io.open(source_path, encoding="UTF-8") as source, \
116 io.open(target_path, "w", encoding="UTF-8") as target:117 io.open(target_path, "w", encoding="UTF-8") as target:
117 source_dir = find_package_directory(source_path)118 source_dir = Click.find_package_directory(source_path)
118 written_comment = False119 written_comment = False
119 seen_path = False120 seen_path = False
120 for line in source:121 for line in source:
121122
=== modified file 'click/commands/hook.py'
--- click/commands/hook.py 2013-09-20 11:07:55 +0000
+++ click/commands/hook.py 2014-03-06 07:04:44 +0000
@@ -20,8 +20,7 @@
20from optparse import OptionParser20from optparse import OptionParser
21from textwrap import dedent21from textwrap import dedent
2222
23from click.database import ClickDB23from gi.repository import Click
24from click.hooks import ClickHook, run_system_hooks, run_user_hooks
2524
2625
27per_hook_subcommands = {26per_hook_subcommands = {
@@ -54,16 +53,25 @@
54 if subcommand in per_hook_subcommands:53 if subcommand in per_hook_subcommands:
55 if len(args) < 2:54 if len(args) < 2:
56 parser.error("need hook name")55 parser.error("need hook name")
57 db = ClickDB(options.root)56 db = Click.DB()
57 db.read()
58 if options.root is not None:
59 db.add(options.root)
58 name = args[1]60 name = args[1]
59 hook = ClickHook.open(db, name)61 hook = Click.Hook.open(db, name)
60 getattr(hook, per_hook_subcommands[subcommand])()62 getattr(hook, per_hook_subcommands[subcommand])()
61 elif subcommand == "run-system":63 elif subcommand == "run-system":
62 db = ClickDB(options.root)64 db = Click.DB()
63 run_system_hooks(db)65 db.read()
66 if options.root is not None:
67 db.add(options.root)
68 Click.run_system_hooks(db)
64 elif subcommand == "run-user":69 elif subcommand == "run-user":
65 db = ClickDB(options.root)70 db = Click.DB()
66 run_user_hooks(db, user=options.user)71 db.read()
72 if options.root is not None:
73 db.add(options.root)
74 Click.run_user_hooks(db, user_name=options.user)
67 else:75 else:
68 parser.error(76 parser.error(
69 "unknown subcommand '%s' (known: install, remove, run-system,"77 "unknown subcommand '%s' (known: install, remove, run-system,"
7078
=== modified file 'click/commands/info.py'
--- click/commands/info.py 2013-09-30 09:20:16 +0000
+++ click/commands/info.py 2014-03-06 07:04:44 +0000
@@ -24,18 +24,21 @@
24import os24import os
25import sys25import sys
2626
27from click.database import ClickDB27from gi.repository import Click
28
28from click.install import DebFile29from click.install import DebFile
29from click.user import ClickUser
3030
3131
32def get_manifest(options, arg):32def get_manifest(options, arg):
33 if "/" not in arg:33 if "/" not in arg:
34 db = ClickDB(options.root)34 db = Click.DB()
35 registry = ClickUser(db, user=options.user)35 db.read()
36 if arg in registry:36 if options.root is not None:
37 db.add(options.root)
38 registry = Click.User.for_user(db, name=options.user)
39 if registry.has_package_name(arg):
37 manifest_path = os.path.join(40 manifest_path = os.path.join(
38 registry.path(arg), ".click", "info", "%s.manifest" % arg)41 registry.get_path(arg), ".click", "info", "%s.manifest" % arg)
39 with io.open(manifest_path, encoding="UTF-8") as manifest:42 with io.open(manifest_path, encoding="UTF-8") as manifest:
40 return json.load(manifest)43 return json.load(manifest)
4144
4245
=== modified file 'click/commands/install.py'
--- click/commands/install.py 2013-10-22 10:44:43 +0000
+++ click/commands/install.py 2014-03-06 07:04:44 +0000
@@ -21,7 +21,8 @@
21import sys21import sys
22from textwrap import dedent22from textwrap import dedent
2323
24from click.database import ClickDB24from gi.repository import Click
25
25from click.install import ClickInstaller, ClickInstallerError26from click.install import ClickInstaller, ClickInstallerError
2627
2728
@@ -45,7 +46,10 @@
45 options, args = parser.parse_args(argv)46 options, args = parser.parse_args(argv)
46 if len(args) < 1:47 if len(args) < 1:
47 parser.error("need package file name")48 parser.error("need package file name")
48 db = ClickDB(options.root)49 db = Click.DB()
50 db.read()
51 if options.root is not None:
52 db.add(options.root)
49 package_path = args[0]53 package_path = args[0]
50 installer = ClickInstaller(db, options.force_missing_framework)54 installer = ClickInstaller(db, options.force_missing_framework)
51 try:55 try:
5256
=== modified file 'click/commands/list.py'
--- click/commands/list.py 2013-09-30 09:14:02 +0000
+++ click/commands/list.py 2014-03-06 07:04:44 +0000
@@ -23,22 +23,25 @@
23import os23import os
24import sys24import sys
2525
26from click.database import ClickDB26from gi.repository import Click
27from click.user import ClickUser
2827
2928
30def list_packages(options):29def list_packages(options):
31 db = ClickDB(options.root)30 db = Click.DB()
31 db.read()
32 if options.root is not None:
33 db.add(options.root)
32 if options.all:34 if options.all:
33 for package, version, path, writeable in \35 for inst in db.get_packages(all_versions=True):
34 db.packages(all_versions=True):36 yield (
35 yield package, version, path, writeable37 inst.props.package, inst.props.version, inst.props.path,
38 inst.props.writeable)
36 else:39 else:
37 registry = ClickUser(db, user=options.user)40 registry = Click.User.for_user(db, name=options.user)
38 for package, version in sorted(registry.items()):41 for package in sorted(registry.get_package_names()):
39 yield (42 yield (
40 package, version, registry.path(package),43 package, registry.get_version(package),
41 registry.removable(package))44 registry.get_path(package), registry.is_removable(package))
4245
4346
44def run(argv):47def run(argv):
4548
=== modified file 'click/commands/pkgdir.py'
--- click/commands/pkgdir.py 2013-09-16 12:44:40 +0000
+++ click/commands/pkgdir.py 2014-03-06 07:04:44 +0000
@@ -21,9 +21,7 @@
21from optparse import OptionParser21from optparse import OptionParser
22import sys22import sys
2323
24from click.database import ClickDB24from gi.repository import Click
25from click.query import find_package_directory
26from click.user import ClickUser
2725
2826
29def run(argv):27def run(argv):
@@ -39,12 +37,15 @@
39 parser.error("need package name")37 parser.error("need package name")
40 try:38 try:
41 if "/" in args[0]:39 if "/" in args[0]:
42 print(find_package_directory(args[0]))40 print(Click.find_package_directory(args[0]))
43 else:41 else:
44 db = ClickDB(options.root)42 db = Click.DB()
43 db.read()
44 if options.root is not None:
45 db.add(options.root)
45 package_name = args[0]46 package_name = args[0]
46 registry = ClickUser(db, user=options.user)47 registry = Click.User(db, name=options.user)
47 print(registry.path(package_name))48 print(registry.get_path(package_name))
48 except Exception as e:49 except Exception as e:
49 print(e, file=sys.stderr)50 print(e, file=sys.stderr)
50 return 151 return 1
5152
=== modified file 'click/commands/register.py'
--- click/commands/register.py 2013-09-30 09:14:02 +0000
+++ click/commands/register.py 2014-03-06 07:04:44 +0000
@@ -19,8 +19,7 @@
1919
20from optparse import OptionParser20from optparse import OptionParser
2121
22from click.database import ClickDB22from gi.repository import Click, GLib
23from click.user import ClickUser
2423
2524
26def run(argv):25def run(argv):
@@ -38,11 +37,20 @@
38 parser.error("need package name")37 parser.error("need package name")
39 if len(args) < 2:38 if len(args) < 2:
40 parser.error("need version")39 parser.error("need version")
41 db = ClickDB(options.root)40 db = Click.DB()
41 db.read()
42 if options.root is not None:
43 db.add(options.root)
42 package = args[0]44 package = args[0]
43 version = args[1]45 version = args[1]
44 registry = ClickUser(db, user=options.user, all_users=options.all_users)46 if options.all_users:
45 old_version = registry.get(package)47 registry = Click.User.for_all_users(db)
48 else:
49 registry = Click.User.for_user(db, name=options.user)
50 try:
51 old_version = registry.get_version(package)
52 except GLib.GError:
53 old_version = None
46 registry.set_version(package, version)54 registry.set_version(package, version)
47 if old_version is not None:55 if old_version is not None:
48 db.maybe_remove(package, old_version)56 db.maybe_remove(package, old_version)
4957
=== modified file 'click/commands/unregister.py'
--- click/commands/unregister.py 2013-09-30 09:14:02 +0000
+++ click/commands/unregister.py 2014-03-06 07:04:44 +0000
@@ -21,8 +21,7 @@
21import os21import os
22import sys22import sys
2323
24from click.database import ClickDB24from gi.repository import Click
25from click.user import ClickUser
2625
2726
28def run(argv):27def run(argv):
@@ -44,10 +43,16 @@
44 "remove packages from disk")43 "remove packages from disk")
45 if options.user is None and "SUDO_USER" in os.environ:44 if options.user is None and "SUDO_USER" in os.environ:
46 options.user = os.environ["SUDO_USER"]45 options.user = os.environ["SUDO_USER"]
47 db = ClickDB(options.root)46 db = Click.DB()
47 db.read()
48 if options.root is not None:
49 db.add(options.root)
48 package = args[0]50 package = args[0]
49 registry = ClickUser(db, user=options.user, all_users=options.all_users)51 if options.all_users:
50 old_version = registry[package]52 registry = Click.User.for_all_users(db)
53 else:
54 registry = Click.User.for_user(db, name=options.user)
55 old_version = registry.get_version(package)
51 if len(args) >= 2 and old_version != args[1]:56 if len(args) >= 2 and old_version != args[1]:
52 print(57 print(
53 "Not removing %s %s; expected version %s" %58 "Not removing %s %s; expected version %s" %
5459
=== removed file 'click/database.py'
--- click/database.py 2013-12-10 14:33:19 +0000
+++ click/database.py 1970-01-01 00:00:00 +0000
@@ -1,323 +0,0 @@
1# Copyright (C) 2013 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""Click databases."""
17
18from __future__ import print_function
19
20__metaclass__ = type
21__all__ = [
22 "ClickDB",
23 ]
24
25
26from collections import Sequence, defaultdict
27import io
28import json
29import os
30import pwd
31import shutil
32import subprocess
33import sys
34
35try:
36 from configparser import Error as ConfigParserError
37 if sys.version < "3.2":
38 from configparser import SafeConfigParser as ConfigParser
39 else:
40 from configparser import ConfigParser
41except ImportError:
42 from ConfigParser import Error as ConfigParserError
43 from ConfigParser import SafeConfigParser as ConfigParser
44
45from click import osextras
46from click.paths import db_dir
47
48
49class ClickSingleDB:
50 def __init__(self, root, master_db):
51 self.root = root
52 self.master_db = master_db
53
54 def path(self, package, version):
55 """Look up a package and version in only this database."""
56 try_path = os.path.join(self.root, package, version)
57 if os.path.exists(try_path):
58 return try_path
59 else:
60 raise KeyError(
61 "%s %s does not exist in %s" % (package, version, self.root))
62
63 def packages(self, all_versions=False):
64 """Return all current package versions in only this database.
65
66 If all_versions=True, return all versions, not just current ones.
67 """
68 for package in sorted(osextras.listdir_force(self.root)):
69 if package == ".click":
70 continue
71 if all_versions:
72 package_path = os.path.join(self.root, package)
73 for version in sorted(osextras.listdir_force(package_path)):
74 version_path = os.path.join(package_path, version)
75 if (os.path.islink(version_path) or
76 not os.path.isdir(version_path)):
77 continue
78 yield package, version, version_path
79 else:
80 current_path = os.path.join(self.root, package, "current")
81 if os.path.islink(current_path):
82 version = os.readlink(current_path)
83 if "/" not in version:
84 yield package, version, current_path
85
86 def _app_running(self, package, app_name, version):
87 app_id = "%s_%s_%s" % (package, app_name, version)
88 command = ["upstart-app-pid", app_id]
89 with open("/dev/null", "w") as devnull:
90 return subprocess.call(command, stdout=devnull) == 0
91
92 def _any_app_running(self, package, version):
93 if not osextras.find_on_path("upstart-app-pid"):
94 return False
95 manifest_path = os.path.join(
96 self.path(package, version), ".click", "info",
97 "%s.manifest" % package)
98 try:
99 with io.open(manifest_path, encoding="UTF-8") as manifest:
100 manifest_json = json.load(manifest)
101 for app_name in manifest_json.get("hooks", {}):
102 if self._app_running(package, app_name, version):
103 return True
104 except Exception:
105 pass
106 return False
107
108 def _remove_unless_running(self, package, version, verbose=False):
109 # Circular imports.
110 from click.hooks import package_remove_hooks
111 from click.user import ClickUser, GC_IN_USE_USER
112
113 if self._any_app_running(package, version):
114 gc_in_use_user_db = ClickUser(self.master_db, user=GC_IN_USE_USER)
115 gc_in_use_user_db.set_version(package, version)
116 return
117
118 version_path = self.path(package, version)
119 if verbose:
120 print("Removing %s" % version_path)
121 package_remove_hooks(self, package, version)
122 shutil.rmtree(version_path, ignore_errors=True)
123
124 package_path = os.path.join(self.root, package)
125 current_path = os.path.join(package_path, "current")
126 if (os.path.islink(current_path) and
127 os.readlink(current_path) == version):
128 os.unlink(current_path)
129 # TODO: Perhaps we should relink current to the latest remaining
130 # version. However, that requires version comparison, and it's
131 # not clear whether it's worth it given that current is mostly
132 # superseded by user registration.
133 if not os.listdir(package_path):
134 os.rmdir(package_path)
135
136 def maybe_remove(self, package, version):
137 """Remove a package version if it is not in use.
138
139 "In use" may mean registered for another user, or running. In the
140 latter case we construct a fake registration so that we can tell the
141 difference later between a package version that was in use at the
142 time of removal and one that was never registered for any user.
143
144 (This is unfortunately complex, and perhaps some day we can require
145 that installations always have some kind of registration to avoid
146 this complexity.)
147 """
148 # Circular imports.
149 from click.user import ClickUsers, GC_IN_USE_USER
150
151 for user_name, user_db in ClickUsers(self.master_db).items():
152 if user_db.get(package) == version:
153 if user_name == GC_IN_USE_USER:
154 # Previously running; we'll check this again shortly.
155 user_db.remove(package)
156 else:
157 # In use.
158 return
159
160 self._remove_unless_running(package, version)
161
162 def gc(self, verbose=True):
163 """Remove package versions with no user registrations.
164
165 To avoid accidentally removing packages that were installed without
166 ever having a user registration, we only garbage-collect packages
167 that were not removed by ClickSingleDB.maybe_remove due to having a
168 running application at the time.
169
170 (This is unfortunately complex, and perhaps some day we can require
171 that installations always have some kind of registration to avoid
172 this complexity.)
173 """
174 # Circular import.
175 from click.user import ClickUser, ClickUsers, GC_IN_USE_USER
176
177 user_reg = defaultdict(set)
178 gc_in_use = defaultdict(set)
179 for user_name, user_db in ClickUsers(self.master_db).items():
180 for package, version in user_db.items():
181 if user_name == GC_IN_USE_USER:
182 gc_in_use[package].add(version)
183 else:
184 user_reg[package].add(version)
185
186 gc_in_use_user_db = ClickUser(self.master_db, user=GC_IN_USE_USER)
187 for package in sorted(osextras.listdir_force(self.root)):
188 if package == ".click":
189 continue
190 package_path = os.path.join(self.root, package)
191 for version in sorted(osextras.listdir_force(package_path)):
192 if version in user_reg[package]:
193 # In use.
194 continue
195 if version not in gc_in_use[package]:
196 version_path = os.path.join(package_path, version)
197 if verbose:
198 print(
199 "Not removing %s (never registered)." %
200 version_path)
201 continue
202 gc_in_use_user_db.remove(package)
203 self._remove_unless_running(package, version, verbose=verbose)
204
205 def _clickpkg_paths(self):
206 """Yield all paths which should be owned by clickpkg."""
207 if os.path.exists(self.root):
208 yield self.root
209 for package in osextras.listdir_force(self.root):
210 if package == ".click":
211 path = os.path.join(self.root, ".click")
212 yield path
213 log_path = os.path.join(path, "log")
214 if os.path.exists(log_path):
215 yield log_path
216 users_path = os.path.join(path, "users")
217 if os.path.exists(users_path):
218 yield users_path
219 else:
220 path = os.path.join(self.root, package)
221 for dirpath, dirnames, filenames in os.walk(path):
222 yield dirpath
223 for dirname in dirnames:
224 dirname_path = os.path.join(dirpath, dirname)
225 if os.path.islink(dirname_path):
226 yield dirname_path
227 for filename in filenames:
228 yield os.path.join(dirpath, filename)
229
230 def ensure_ownership(self):
231 """Ensure correct ownership of files in the database.
232
233 On a system that is upgraded by delivering a new system image rather
234 than by package upgrades, it is possible for the clickpkg UID to
235 change. The overlay database must then be adjusted to account for
236 this.
237 """
238 pw = pwd.getpwnam("clickpkg")
239 try:
240 st = os.stat(self.root)
241 if st.st_uid == pw.pw_uid and st.st_gid == pw.pw_gid:
242 return
243 except OSError:
244 return
245 chown_kwargs = {}
246 if sys.version >= "3.3" and os.chown in os.supports_follow_symlinks:
247 chown_kwargs["follow_symlinks"] = False
248 for path in self._clickpkg_paths():
249 os.chown(path, pw.pw_uid, pw.pw_gid, **chown_kwargs)
250
251
252class ClickDB(Sequence):
253 def __init__(self, extra_root=None, use_system=True, override_db_dir=None):
254 if override_db_dir is None:
255 override_db_dir = db_dir
256 self._db = []
257 if use_system:
258 for entry in sorted(osextras.listdir_force(override_db_dir)):
259 if not entry.endswith(".conf"):
260 continue
261 path = os.path.join(override_db_dir, entry)
262 config = ConfigParser()
263 try:
264 config.read(path)
265 root = config.get("Click Database", "root")
266 except ConfigParserError as e:
267 print(e, file=sys.stderr)
268 continue
269 self.add(root)
270 if extra_root is not None:
271 self.add(extra_root)
272
273 def __getitem__(self, key):
274 return self._db[key]
275
276 def __len__(self):
277 return len(self._db)
278
279 def add(self, root):
280 self._db.append(ClickSingleDB(root, self))
281
282 @property
283 def overlay(self):
284 """Return the directory where changes should be written."""
285 return self._db[-1].root
286
287 def path(self, package, version):
288 """Look up a package and version in all databases."""
289 for db in reversed(self._db):
290 try:
291 return db.path(package, version)
292 except KeyError:
293 pass
294 else:
295 raise KeyError(
296 "%s %s does not exist in any database" % (package, version))
297
298 def packages(self, all_versions=False):
299 """Return current package versions in all databases.
300
301 If all_versions=True, return all versions, not just current ones.
302 """
303 seen = set()
304 for db in reversed(self._db):
305 writeable = db is self._db[-1]
306 for package, version, path in \
307 db.packages(all_versions=all_versions):
308 if all_versions:
309 seen_id = (package, version)
310 else:
311 seen_id = package
312 if seen_id not in seen:
313 yield package, version, path, writeable
314 seen.add(seen_id)
315
316 def maybe_remove(self, package, version):
317 self._db[-1].maybe_remove(package, version)
318
319 def gc(self, verbose=True):
320 self._db[-1].gc(verbose=verbose)
321
322 def ensure_ownership(self):
323 self._db[-1].ensure_ownership()
3240
=== removed file 'click/hooks.py'
--- click/hooks.py 2014-02-19 15:31:55 +0000
+++ click/hooks.py 1970-01-01 00:00:00 +0000
@@ -1,438 +0,0 @@
1# Copyright (C) 2013 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""Click package hooks.
17
18See doc/hooks.rst for the draft specification.
19"""
20
21from __future__ import print_function
22
23__metaclass__ = type
24__all__ = [
25 "ClickHook",
26 "package_install_hooks",
27 "run_system_hooks",
28 "run_user_hooks",
29 ]
30
31from functools import partial
32import grp
33import io
34import json
35import os
36import pwd
37import re
38from string import Formatter
39import subprocess
40
41from debian.deb822 import Deb822
42
43from click import osextras
44from click.paths import hooks_dir
45from click.user import ClickUser, ClickUsers
46
47
48def _read_manifest_hooks(db, package, version):
49 if version is None:
50 return {}
51 try:
52 manifest_path = os.path.join(
53 db.path(package, version), ".click", "info",
54 "%s.manifest" % package)
55 with io.open(manifest_path, encoding="UTF-8") as manifest:
56 return json.load(manifest).get("hooks", {})
57 except (KeyError, IOError):
58 return {}
59
60
61class ClickPatternFormatter(Formatter):
62 """A Formatter that handles simple $-expansions.
63
64 `${key}` is replaced by the value of the `key` argument; `$$` is
65 replaced by `$`. Any `$` character not followed by `{...}` or `$` is
66 preserved intact.
67 """
68 _expansion_re = re.compile(r"\$(?:\$|{(.*?)})")
69
70 def parse(self, format_string):
71 while True:
72 match = self._expansion_re.search(format_string)
73 if match is None:
74 if format_string:
75 yield format_string, None, None, None
76 return
77 start, end = match.span()
78 if format_string[match.start():match.end()] == "$$":
79 yield format_string[:match.start() + 1], None, None, None
80 else:
81 yield format_string[:match.start()], match.group(1), "", None
82 format_string = format_string[match.end():]
83
84 def get_field(self, field_name, args, kwargs):
85 value = kwargs.get(field_name)
86 if value is None:
87 value = ""
88 return value, field_name
89
90 def possible_expansion(self, s, format_string, *args, **kwargs):
91 """Check if s is a possible expansion.
92
93 Any (keyword) arguments have the effect of binding some keys to
94 fixed values; unspecified keys may take any value, and will bind
95 greedily to the longest possible string.
96
97 If s is a possible expansion, then this method returns a (possibly
98 empty) dictionary mapping all the unspecified keys to their bound
99 values. Otherwise, it returns None.
100 """
101 ret = {}
102 regex_pieces = []
103 group_names = []
104 for literal_text, field_name, format_spec, conversion in \
105 self.parse(format_string):
106 if literal_text:
107 regex_pieces.append(re.escape(literal_text))
108 if field_name is not None:
109 if field_name in kwargs:
110 regex_pieces.append(re.escape(kwargs[field_name]))
111 else:
112 regex_pieces.append("(.*)")
113 group_names.append(field_name)
114 match = re.match("^%s$" % "".join(regex_pieces), s)
115 if match is None:
116 return None
117 for group in range(len(group_names)):
118 ret[group_names[group]] = match.group(group + 1)
119 return ret
120
121
122class ClickHook(Deb822):
123 _formatter = ClickPatternFormatter()
124
125 def __init__(self, db, name, sequence=None, fields=None, encoding="utf-8"):
126 super(ClickHook, self).__init__(
127 sequence=sequence, fields=fields, encoding=encoding)
128 self.db = db
129 self.name = name
130
131 @classmethod
132 def open(cls, db, name):
133 try:
134 with open(os.path.join(hooks_dir, "%s.hook" % name)) as f:
135 return cls(db, name, f)
136 except IOError:
137 raise KeyError("No click hook '%s' installed" % name)
138
139 @classmethod
140 def open_all(cls, db, hook_name=None):
141 for entry in osextras.listdir_force(hooks_dir):
142 if not entry.endswith(".hook"):
143 continue
144 try:
145 with open(os.path.join(hooks_dir, entry)) as f:
146 hook = cls(db, entry[:-5], f)
147 if hook_name is None or hook.hook_name == hook_name:
148 yield hook
149 except IOError:
150 pass
151
152 @property
153 def user_level(self):
154 return self.get("user-level", "no") == "yes"
155
156 @property
157 def single_version(self):
158 return self.user_level or self.get("single-version", "no") == "yes"
159
160 @property
161 def hook_name(self):
162 return self.get("hook-name", self.name)
163
164 def short_app_id(self, package, app_name):
165 # TODO: perhaps this check belongs further up the stack somewhere?
166 if "_" in app_name or "/" in app_name:
167 raise ValueError(
168 "Application name '%s' may not contain _ or / characters" %
169 app_name)
170 return "%s_%s" % (package, app_name)
171
172 def app_id(self, package, version, app_name):
173 return "%s_%s" % (self.short_app_id(package, app_name), version)
174
175 def _user_home(self, user):
176 if user is None:
177 return None
178 # TODO: make robust against removed users
179 # TODO: caching
180 return pwd.getpwnam(user).pw_dir
181
182 def pattern(self, package, version, app_name, user=None):
183 app_id = self.app_id(package, version, app_name)
184 kwargs = {
185 "id": app_id,
186 "user": user,
187 "home": self._user_home(user),
188 }
189 if self.single_version:
190 kwargs["short-id"] = self.short_app_id(package, app_name)
191 return self._formatter.format(self["pattern"], **kwargs).rstrip(os.sep)
192
193 def _drop_privileges(self, username):
194 if os.geteuid() != 0:
195 return
196 pw = pwd.getpwnam(username)
197 os.setgroups(
198 [g.gr_gid for g in grp.getgrall() if username in g.gr_mem])
199 # Portability note: this assumes that we have [gs]etres[gu]id, which
200 # is true on Linux but not necessarily elsewhere. If you need to
201 # support something else, there are reasonably standard alternatives
202 # involving other similar calls; see e.g. gnulib/lib/idpriv-drop.c.
203 os.setresgid(pw.pw_gid, pw.pw_gid, pw.pw_gid)
204 os.setresuid(pw.pw_uid, pw.pw_uid, pw.pw_uid)
205 assert os.getresuid() == (pw.pw_uid, pw.pw_uid, pw.pw_uid)
206 assert os.getresgid() == (pw.pw_gid, pw.pw_gid, pw.pw_gid)
207 os.environ["HOME"] = pw.pw_dir
208 os.umask(osextras.get_umask() | 0o002)
209
210 def _run_commands_user(self, user=None):
211 if self.user_level:
212 return user
213 else:
214 return self["user"]
215
216 def _run_commands(self, user=None):
217 if "exec" in self:
218 drop_privileges = partial(
219 self._drop_privileges, self._run_commands_user(user=user))
220 subprocess.check_call(
221 self["exec"], preexec_fn=drop_privileges, shell=True)
222 if self.get("trigger", "no") == "yes":
223 raise NotImplementedError("'Trigger: yes' not yet implemented")
224
225 def _previous_entries(self, user=None):
226 """Find entries that match the structure of our links."""
227 link_dir = os.path.dirname(self.pattern("", "", "", user=user))
228 # TODO: This only works if the app ID only appears, at most, in the
229 # last component of the pattern path.
230 for previous_entry in osextras.listdir_force(link_dir):
231 previous_path = os.path.join(link_dir, previous_entry)
232 previous_exp = self._formatter.possible_expansion(
233 previous_path, self["pattern"], user=user,
234 home=self._user_home(user))
235 if previous_exp is None or "id" not in previous_exp:
236 continue
237 previous_id = previous_exp["id"]
238 try:
239 previous_package, previous_app_name, previous_version = (
240 previous_id.split("_", 2))
241 yield (
242 previous_path,
243 previous_package, previous_version, previous_app_name)
244 except ValueError:
245 continue
246
247 def _install_link(self, package, version, app_name, relative_path,
248 user=None, user_db=None):
249 """Install a hook symlink.
250
251 This should be called with dropped privileges if necessary.
252 """
253 if self.user_level:
254 target = os.path.join(user_db.path(package), relative_path)
255 else:
256 target = os.path.join(
257 self.db.path(package, version), relative_path)
258 link = self.pattern(package, version, app_name, user=user)
259 if not os.path.islink(link) or os.readlink(link) != target:
260 osextras.ensuredir(os.path.dirname(link))
261 osextras.symlink_force(target, link)
262
263 def install_package(self, package, version, app_name, relative_path,
264 user=None):
265 if self.user_level:
266 user_db = ClickUser(self.db, user=user)
267 else:
268 assert user is None
269
270 # Remove previous versions if necessary.
271 if self.single_version:
272 for path, p_package, p_version, p_app_name in \
273 self._previous_entries(user=user):
274 if (p_package == package and p_app_name == app_name and
275 p_version != version):
276 osextras.unlink_force(path)
277
278 if self.user_level:
279 with user_db._dropped_privileges():
280 self._install_link(
281 package, version, app_name, relative_path,
282 user=user, user_db=user_db)
283 else:
284 self._install_link(package, version, app_name, relative_path)
285 self._run_commands(user=user)
286
287 def remove_package(self, package, version, app_name, user=None):
288 osextras.unlink_force(
289 self.pattern(package, version, app_name, user=user))
290 self._run_commands(user=user)
291
292 def _all_packages(self, user=None):
293 """Return an iterable of all unpacked packages.
294
295 If running a user-level hook, this returns (package, version, user)
296 for the current version of each package registered for each user, or
297 only for a single user if user is not None.
298
299 If running a system-level hook, this returns (package, version,
300 None) for each version of each unpacked package.
301 """
302 if self.user_level:
303 if user is not None:
304 user_db = ClickUser(self.db, user=user)
305 for package, version in user_db.items():
306 yield package, version, user
307 else:
308 for user_name, user_db in ClickUsers(self.db).items():
309 if user_name.startswith("@"):
310 continue
311 for package, version in user_db.items():
312 yield package, version, user_name
313 else:
314 for package, version, _, _ in self.db.packages():
315 yield package, version, None
316
317 def _relevant_apps(self, user=None):
318 """Return an iterable of all applications relevant for this hook."""
319 for package, version, user_name in self._all_packages(user=user):
320 manifest = _read_manifest_hooks(self.db, package, version)
321 for app_name, hooks in manifest.items():
322 if self.hook_name in hooks:
323 yield (
324 package, version, app_name, user_name,
325 hooks[self.hook_name])
326
327 def install(self, user=None):
328 for package, version, app_name, user_name, relative_path in (
329 self._relevant_apps(user=user)):
330 self.install_package(
331 package, version, app_name, relative_path, user=user_name)
332
333 def remove(self, user=None):
334 for package, version, app_name, user_name, _ in (
335 self._relevant_apps(user=user)):
336 self.remove_package(package, version, app_name, user=user_name)
337
338 def sync(self, user=None):
339 if self.user_level:
340 user_db = ClickUser(self.db, user=user)
341 else:
342 assert user is None
343
344 seen = set()
345 for package, version, app_name, user_name, relative_path in (
346 self._relevant_apps(user=user)):
347 seen.add((package, version, app_name))
348 if self.user_level:
349 with user_db._dropped_privileges():
350 self._install_link(
351 package, version, app_name, relative_path,
352 user=user_name, user_db=user_db)
353 else:
354 self._install_link(package, version, app_name, relative_path)
355 for path, package, version, app_name in \
356 self._previous_entries(user=user):
357 if (package, version, app_name) not in seen:
358 osextras.unlink_force(path)
359 self._run_commands(user=user)
360
361
362def _app_hooks(hooks):
363 items = set()
364 for app_name in hooks:
365 for hook_name in hooks[app_name]:
366 items.add((app_name, hook_name))
367 return items
368
369
370def package_install_hooks(db, package, old_version, new_version, user=None):
371 """Run hooks following installation or upgrade of a Click package.
372
373 If user is None, only run system-level hooks. If user is not None, only
374 run user-level hooks for that user.
375 """
376 old_manifest = _read_manifest_hooks(db, package, old_version)
377 new_manifest = _read_manifest_hooks(db, package, new_version)
378
379 # Remove any targets for single-version hooks that were in the old
380 # manifest but not the new one.
381 for app_name, hook_name in sorted(
382 _app_hooks(old_manifest) - _app_hooks(new_manifest)):
383 for hook in ClickHook.open_all(db, hook_name):
384 if hook.user_level != (user is not None):
385 continue
386 if hook.single_version:
387 hook.remove_package(package, old_version, app_name, user=user)
388
389 for app_name, app_hooks in sorted(new_manifest.items()):
390 for hook_name, relative_path in sorted(app_hooks.items()):
391 for hook in ClickHook.open_all(db, hook_name):
392 if hook.user_level != (user is not None):
393 continue
394 hook.install_package(
395 package, new_version, app_name, relative_path, user=user)
396
397
398def package_remove_hooks(db, package, old_version, user=None):
399 """Run hooks following removal of a Click package.
400
401 If user is None, only run system-level hooks. If user is not None, only
402 run user-level hooks for that user.
403 """
404 old_manifest = _read_manifest_hooks(db, package, old_version)
405
406 for app_name, app_hooks in sorted(old_manifest.items()):
407 for hook_name in sorted(app_hooks):
408 for hook in ClickHook.open_all(db, hook_name):
409 if hook.user_level != (user is not None):
410 continue
411 hook.remove_package(package, old_version, app_name, user=user)
412
413
414def run_system_hooks(db):
415 """Run system-level hooks for all installed packages.
416
417 This is useful when starting up from images with preinstalled packages
418 which may not have had their system-level hooks run properly when
419 building the image. It is suitable for running at system startup.
420 """
421 db.ensure_ownership()
422 for hook in ClickHook.open_all(db):
423 if not hook.user_level:
424 hook.sync()
425
426
427def run_user_hooks(db, user=None):
428 """Run user-level hooks for all packages registered for a user.
429
430 This is useful to catch up with packages that may have been preinstalled
431 and registered for all users. It is suitable for running at session
432 startup.
433 """
434 if user is None:
435 user = pwd.getpwuid(os.getuid()).pw_name
436 for hook in ClickHook.open_all(db):
437 if hook.user_level:
438 hook.sync(user=user)
4390
=== modified file 'click/install.py'
--- click/install.py 2014-01-22 14:02:33 +0000
+++ click/install.py 2014-03-06 07:04:44 +0000
@@ -43,12 +43,10 @@
43import apt_pkg43import apt_pkg
44from debian.debfile import DebFile as _DebFile44from debian.debfile import DebFile as _DebFile
45from debian.debian_support import Version45from debian.debian_support import Version
46from gi.repository import Click
4647
47from click import osextras
48from click.hooks import package_install_hooks
49from click.paths import frameworks_dir, preload_path48from click.paths import frameworks_dir, preload_path
50from click.preinst import static_preinst_matches49from click.preinst import static_preinst_matches
51from click.user import ClickUser
52from click.versions import spec_version50from click.versions import spec_version
5351
5452
@@ -313,12 +311,14 @@
313311
314 def install(self, path, user=None, all_users=False):312 def install(self, path, user=None, all_users=False):
315 package_name, package_version = self.audit(path, check_arch=True)313 package_name, package_version = self.audit(path, check_arch=True)
316 package_dir = os.path.join(self.db.overlay, package_name)314 package_dir = os.path.join(self.db.props.overlay, package_name)
317 inst_dir = os.path.join(package_dir, package_version)315 inst_dir = os.path.join(package_dir, package_version)
318 assert os.path.dirname(os.path.dirname(inst_dir)) == self.db.overlay316 assert (
317 os.path.dirname(os.path.dirname(inst_dir)) ==
318 self.db.props.overlay)
319319
320 self._check_write_permissions(self.db.overlay)320 self._check_write_permissions(self.db.props.overlay)
321 root_click = os.path.join(self.db.overlay, ".click")321 root_click = os.path.join(self.db.props.overlay, ".click")
322 if not os.path.exists(root_click):322 if not os.path.exists(root_click):
323 os.makedirs(root_click)323 os.makedirs(root_click)
324 if os.geteuid() == 0:324 if os.geteuid() == 0:
@@ -348,7 +348,7 @@
348 if "LD_PRELOAD" in env:348 if "LD_PRELOAD" in env:
349 preloads.append(env["LD_PRELOAD"])349 preloads.append(env["LD_PRELOAD"])
350 env["LD_PRELOAD"] = " ".join(preloads)350 env["LD_PRELOAD"] = " ".join(preloads)
351 env["CLICK_BASE_DIR"] = self.db.overlay351 env["CLICK_BASE_DIR"] = self.db.props.overlay
352 env["CLICK_PACKAGE_PATH"] = path352 env["CLICK_PACKAGE_PATH"] = path
353 env["CLICK_PACKAGE_FD"] = str(fd.fileno())353 env["CLICK_PACKAGE_FD"] = str(fd.fileno())
354 env.pop("HOME", None)354 env.pop("HOME", None)
@@ -379,11 +379,11 @@
379 old_version = None379 old_version = None
380 else:380 else:
381 old_version = None381 old_version = None
382 package_install_hooks(382 Click.package_install_hooks(
383 self.db, package_name, old_version, package_version)383 self.db, package_name, old_version, package_version)
384384
385 new_path = os.path.join(package_dir, "current.new")385 new_path = os.path.join(package_dir, "current.new")
386 osextras.symlink_force(package_version, new_path)386 Click.symlink_force(package_version, new_path)
387 if os.geteuid() == 0:387 if os.geteuid() == 0:
388 # shutil.chown would be more convenient, but it doesn't support388 # shutil.chown would be more convenient, but it doesn't support
389 # follow_symlinks=False in Python 3.3.389 # follow_symlinks=False in Python 3.3.
@@ -393,7 +393,10 @@
393 os.rename(new_path, current_path)393 os.rename(new_path, current_path)
394394
395 if user is not None or all_users:395 if user is not None or all_users:
396 registry = ClickUser(self.db, user=user, all_users=all_users)396 if all_users:
397 registry = Click.User.for_all_users(self.db)
398 else:
399 registry = Click.User.for_user(self.db, name=user)
397 registry.set_version(package_name, package_version)400 registry.set_version(package_name, package_version)
398401
399 if old_version is not None:402 if old_version is not None:
400403
=== modified file 'click/osextras.py'
--- click/osextras.py 2013-09-04 15:59:18 +0000
+++ click/osextras.py 2014-03-06 07:04:44 +0000
@@ -13,7 +13,15 @@
13# You should have received a copy of the GNU General Public License13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.14# along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16"""Extra OS-level utility functions."""16"""Extra OS-level utility functions.
17
18Usually we can instead use the functions exported from
19lib/click/osextras.vala via GObject Introspection. These pure-Python
20versions are preserved so that they can be used from code that needs to be
21maximally portable: for example, click.build is intended to be usable even
22on systems that lack GObject, as long as they have a reasonably recent
23version of Python.
24"""
1725
18__all__ = [26__all__ = [
19 'ensuredir',27 'ensuredir',
2028
=== modified file 'click/paths.py.in'
--- click/paths.py.in 2013-09-03 12:50:11 +0000
+++ click/paths.py.in 2014-03-06 07:04:44 +0000
@@ -17,5 +17,3 @@
1717
18preload_path = "@pkglibdir@/libclickpreload.so"18preload_path = "@pkglibdir@/libclickpreload.so"
19frameworks_dir = "@pkgdatadir@/frameworks"19frameworks_dir = "@pkgdatadir@/frameworks"
20hooks_dir = "@pkgdatadir@/hooks"
21db_dir = "@sysconfdir@/click/databases"
2220
=== removed file 'click/query.py'
--- click/query.py 2013-07-23 18:30:48 +0000
+++ click/query.py 1970-01-01 00:00:00 +0000
@@ -1,43 +0,0 @@
1# Copyright (C) 2013 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""Query information about installed Click packages."""
17
18from __future__ import print_function
19
20__metaclass__ = type
21__all__ = [
22 'find_package_directory',
23 ]
24
25import os
26
27
28def _walk_up(path):
29 while True:
30 yield path
31 newpath = os.path.dirname(path)
32 if newpath == path:
33 return
34 path = newpath
35
36
37def find_package_directory(path):
38 for directory in _walk_up(os.path.realpath(path)):
39 if os.path.isdir(os.path.join(directory, ".click", "info")):
40 return directory
41 break
42 else:
43 raise Exception("No package directory found for %s" % path)
440
=== added file 'click/tests/Makefile.am'
--- click/tests/Makefile.am 1970-01-01 00:00:00 +0000
+++ click/tests/Makefile.am 2014-03-06 07:04:44 +0000
@@ -0,0 +1,10 @@
1noinst_DATA = preload.gir
2CLEANFILES = $(noinst_DATA)
3
4preload.gir: preload.h
5 PKG_CONFIG_PATH=$(top_builddir)/lib/click g-ir-scanner \
6 -n preload --nsversion 0 -l c \
7 --pkg glib-2.0 --pkg gee-0.8 --pkg click-0.4 \
8 -I$(top_builddir)/lib/click -L$(top_builddir)/lib/click \
9 --accept-unprefixed --warn-all \
10 $< --output $@
011
=== modified file 'click/tests/__init__.py'
--- click/tests/__init__.py 2013-04-10 15:55:06 +0000
+++ click/tests/__init__.py 2014-03-06 07:04:44 +0000
@@ -0,0 +1,39 @@
1from __future__ import print_function
2
3import os
4import sys
5
6from click.tests import config
7
8
9def _append_env_path(envname, value):
10 if envname in os.environ:
11 if value in os.environ[envname].split(":"):
12 return False
13 os.environ[envname] = "%s:%s" % (os.environ[envname], value)
14 else:
15 os.environ[envname] = value
16 return True
17
18
19# Don't do any of this in interactive mode.
20if not hasattr(sys, "ps1"):
21 _lib_click_dir = os.path.join(config.abs_top_builddir, "lib", "click")
22 changed = False
23 if _append_env_path(
24 "LD_LIBRARY_PATH", os.path.join(_lib_click_dir, ".libs")):
25 changed = True
26 if _append_env_path("GI_TYPELIB_PATH", _lib_click_dir):
27 changed = True
28 if changed:
29 # We have to re-exec ourselves to get the dynamic loader to pick up
30 # the new value of LD_LIBRARY_PATH.
31 if "-m unittest" in sys.argv[0]:
32 # unittest does horrible things to sys.argv in the name of
33 # "usefulness", making the re-exec more painful than it needs to
34 # be.
35 os.execvp(
36 sys.executable, [sys.executable, "-m", "unittest"] + sys.argv[1:])
37 else:
38 os.execvp(sys.executable, [sys.executable] + sys.argv)
39 os._exit(1)
040
=== added file 'click/tests/config.py.in'
--- click/tests/config.py.in 1970-01-01 00:00:00 +0000
+++ click/tests/config.py.in 2014-03-06 07:04:44 +0000
@@ -0,0 +1,20 @@
1# Copyright (C) 2014 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16abs_top_builddir = "@abs_top_builddir@"
17STAT_OFFSET_UID = @STAT_OFFSET_UID@
18STAT_OFFSET_GID = @STAT_OFFSET_GID@
19STAT64_OFFSET_UID = @STAT64_OFFSET_UID@
20STAT64_OFFSET_GID = @STAT64_OFFSET_GID@
021
=== added file 'click/tests/gimock.py'
--- click/tests/gimock.py 1970-01-01 00:00:00 +0000
+++ click/tests/gimock.py 2014-03-06 07:04:44 +0000
@@ -0,0 +1,499 @@
1# Copyright (C) 2014 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""Mock function support based on GObject Introspection.
17
18(Note to reviewers: I expect to rewrite this from scratch on my own time as
19a more generalised set of Python modules for unit testing of C code,
20although using similar core ideas. This is a first draft for the purpose of
21getting Click's test suite to work expediently, rather than an interface I'm
22prepared to commit to long-term.)
23
24Python is a versatile and concise language for writing tests, and GObject
25Introspection (GI) makes it straightforward (often trivial) to bind native
26code into Python. However, writing tests for native code quickly runs into
27the problem of how to build mock functions. You might reasonably have code
28that calls chown(), for instance, and want to test how it's called rather
29than worrying about setting up a fakeroot-type environment where chown()
30will work. The obvious solution is to use `LD_PRELOAD` wrappers, but there
31are various problems to overcome in practice:
32
33 * You can only set up a new `LD_PRELOAD` by going through the run-time
34 linker; you can't just set it for a single in-process test case.
35 * Generating the preloaded wrapper involves a fair bit of boilerplate code.
36 * Having to write per-test mock code in C is inconvenient, and makes it
37 difficult to get information back out of the mock (such as "how often was
38 this function called, and with what arguments?").
39
40The first problem can be solved by a decorator that knows how to run
41individual tests in a subprocess. This is made somewhat more inconvenient
42by the fact that there is no way for a context manager's `__enter__` method
43to avoid executing the context-managed block other than by throwing an
44exception, which makes it hard to silently avoid executing the test case in
45the parent process, but we can work around this at the cost of an extra line
46of code per invocation.
47
48For the rest, a combination of GI itself and ctypes can help. We can use GI
49to keep track of argument and return types of the mocked C functions in a
50reasonably sane way, by parsing header files. We're operating in the other
51direction from how GI is normally used, so PyGObject can't deal with
52bridging the two calling conventions for us. ctypes can: but we still need
53to be careful! We have to construct the callback functions in the child
54process, ensure that we keep references to them, and inject function
55pointers into the preloaded library via specially-named helper functions;
56until those function pointers are set up we must make sure to call the libc
57functions instead (since some of them might be called during Python
58startup).
59
60The combination of all of this allows us to bridge C functions somewhat
61transparently into Python. This lets you supply a Python function or method
62as the mock replacement for a C library function, making it much simpler to
63record state.
64
65It's still not perfect:
66
67 * We're using GI in an upside-down kind of way, and we specifically need
68 GIR files rather than typelibs so that we can extract the original C
69 type, so some fiddling is required for each new function you want to
70 mock.
71
72 * The subprocess arrangements are unavoidably slow and it's possible that
73 they may cause problems with some test runners.
74
75 * Some C functions (such as `stat`) tend to have multiple underlying entry
76 points in the C library which must be preloaded independently.
77
78 * You have to be careful about how your libraries are linked, because `ld
79 -Wl,-Bsymbolic-functions` prevents `LD_PRELOAD` working for intra-library
80 calls.
81
82 * `ctypes should return composite types from callbacks
83 <http://bugs.python.org/issue5710>`_. The least awful approach for now
84 seems to be to construct the composite type in question, stash a
85 reference to it forever, and then return a pointer to it as a void *; we
86 can only get away with this because tests are by nature relatively
87 short-lived.
88
89 * The ctypes module's handling of 64-bit pointers is basically just awful.
90 The right answer is probably to use a different callback-generation
91 framework entirely (maybe extending PyGObject so that we can get at the
92 pieces we need), but I've hacked around it for now.
93
94 * It doesn't appear to be possible to install mock replacements for
95 functions that are called directly from Python code using their GI
96 wrappers. You can work around this by simply patching the GI wrapper
97 instead, using `mock.patch`.
98
99I think the benefits, in terms of local clarity of tests, are worth the
100downsides.
101"""
102
103from __future__ import print_function
104
105__metaclass__ = type
106__all__ = ['GIMockTestCase']
107
108
109import contextlib
110import ctypes
111import fcntl
112from functools import partial
113import os
114import pickle
115import shutil
116import subprocess
117import sys
118import tempfile
119from textwrap import dedent
120import traceback
121import unittest
122try:
123 from unittest import mock
124except ImportError:
125 import mock
126try:
127 import xml.etree.cElementTree as etree
128except ImportError:
129 import xml.etree.ElementTree as etree
130
131from click.tests.gimock_types import Stat, Stat64
132
133
134# Borrowed from giscanner.girparser.
135CORE_NS = "http://www.gtk.org/introspection/core/1.0"
136C_NS = "http://www.gtk.org/introspection/c/1.0"
137GLIB_NS = "http://www.gtk.org/introspection/glib/1.0"
138
139
140def _corens(tag):
141 return '{%s}%s' % (CORE_NS, tag)
142
143
144def _glibns(tag):
145 return '{%s}%s' % (GLIB_NS, tag)
146
147
148def _cns(tag):
149 return '{%s}%s' % (C_NS, tag)
150
151
152# Override some c:type annotations that g-ir-scanner gets a bit wrong.
153_c_type_override = {
154 "passwd*": "struct passwd*",
155 "stat*": "struct stat*",
156 "stat64*": "struct stat64*",
157 }
158
159
160# Mapping of GI type name -> ctypes type.
161_typemap = {
162 "GError**": ctypes.c_void_p,
163 "gboolean": ctypes.c_bool,
164 "gint": ctypes.c_int,
165 "gint*": ctypes.POINTER(ctypes.c_int),
166 "gint32": ctypes.c_int32,
167 "gpointer": ctypes.c_void_p,
168 "guint": ctypes.c_uint,
169 "guint8**": ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)),
170 "guint32": ctypes.c_uint32,
171 "none": None,
172 "utf8": ctypes.c_char_p,
173 "utf8*": ctypes.POINTER(ctypes.c_char_p),
174 }
175
176
177class GIMockTestCase(unittest.TestCase):
178 def setUp(self):
179 super(GIMockTestCase, self).setUp()
180 self._gimock_temp_dir = tempfile.mkdtemp(prefix="gimock")
181 self.addCleanup(shutil.rmtree, self._gimock_temp_dir)
182 self._preload_func_refs = []
183 self._composite_refs = []
184 self._delegate_funcs = {}
185
186 def tearDown(self):
187 self._preload_func_refs = []
188 self._composite_refs = []
189 self._delegate_funcs = {}
190
191 def _gir_get_type(self, obj):
192 ret = {}
193 arrayinfo = obj.find(_corens("array"))
194 if arrayinfo is not None:
195 typeinfo = arrayinfo.find(_corens("type"))
196 raw_ctype = arrayinfo.get(_cns("type"))
197 else:
198 typeinfo = obj.find(_corens("type"))
199 raw_ctype = typeinfo.get(_cns("type"))
200 gi_type = typeinfo.get("name")
201 if obj.get("direction", "in") == "out":
202 gi_type += "*"
203 if arrayinfo is not None:
204 gi_type += "*"
205 ret["gi"] = gi_type
206 ret["c"] = _c_type_override.get(raw_ctype, raw_ctype)
207 return ret
208
209 def _parse_gir(self, path):
210 # A very, very crude GIR parser. We might have used
211 # giscanner.girparser, but it's not importable in Python 3 at the
212 # moment.
213 tree = etree.parse(path)
214 root = tree.getroot()
215 assert root.tag == _corens("repository")
216 assert root.get("version") == "1.2"
217 ns = root.find(_corens("namespace"))
218 assert ns is not None
219 funcs = {}
220 for func in ns.findall(_corens("function")):
221 name = func.get(_cns("identifier"))
222 # g-ir-scanner skips identifiers starting with "__", which we
223 # need in order to mock stat effectively. Work around this.
224 name = name.replace("under_under_", "__")
225 headers = None
226 for attr in func.findall(_corens("attribute")):
227 if attr.get("name") == "headers":
228 headers = attr.get("value")
229 break
230 rv = func.find(_corens("return-value"))
231 assert rv is not None
232 params = []
233 paramnode = func.find(_corens("parameters"))
234 if paramnode is not None:
235 for param in paramnode.findall(_corens("parameter")):
236 params.append({
237 "name": param.get("name"),
238 "type": self._gir_get_type(param),
239 })
240 if func.get("throws", "0") == "1":
241 params.append({
242 "name": "error",
243 "type": { "gi": "GError**", "c": "GError**" },
244 })
245 funcs[name] = {
246 "name": name,
247 "headers": headers,
248 "rv": self._gir_get_type(rv),
249 "params": params,
250 }
251 return funcs
252
253 def _ctypes_type(self, gi_type):
254 return _typemap[gi_type["gi"]]
255
256 def make_preloads(self, preloads):
257 rpreloads = []
258 std_headers = set([
259 "dlfcn.h",
260 # Not strictly needed, but convenient for ad-hoc debugging.
261 "stdio.h",
262 "stdint.h",
263 "stdlib.h",
264 "sys/types.h",
265 "unistd.h",
266 ])
267 preload_headers = set()
268 funcs = self._parse_gir("click/tests/preload.gir")
269 for name, func in preloads.items():
270 info = funcs[name]
271 rpreloads.append([info, func])
272 headers = info["headers"]
273 if headers is not None:
274 preload_headers.update(headers.split(","))
275 if "GIMOCK_SUBPROCESS" in os.environ:
276 return None, rpreloads
277 preloads_dir = os.path.join(self._gimock_temp_dir, "_preloads")
278 os.makedirs(preloads_dir)
279 c_path = os.path.join(preloads_dir, "gimockpreload.c")
280 with open(c_path, "w") as c:
281 print("#define _GNU_SOURCE", file=c)
282 for header in sorted(std_headers | preload_headers):
283 print("#include <%s>" % header, file=c)
284 print(file=c)
285 for info, _ in rpreloads:
286 conv = {}
287 conv["name"] = info["name"]
288 argtypes = [p["type"]["c"] for p in info["params"]]
289 argnames = [p["name"] for p in info["params"]]
290 conv["ret"] = info["rv"]["c"]
291 conv["bareproto"] = ", ".join(argtypes)
292 conv["proto"] = ", ".join(
293 "%s %s" % pair for pair in zip(argtypes, argnames))
294 conv["args"] = ", ".join(argnames)
295 # The delegation scheme used here is needed because trying
296 # to pass pointers back and forward through ctypes is a
297 # recipe for having them truncated to 32 bits at the drop of
298 # a hat. This approach is less obvious but much safer.
299 print(dedent("""\
300 typedef %(ret)s preloadtype_%(name)s (%(bareproto)s);
301 preloadtype_%(name)s *ctypes_%(name)s = (void *) 0;
302 preloadtype_%(name)s *real_%(name)s = (void *) 0;
303 static volatile int delegate_%(name)s = 0;
304
305 extern void _gimock_init_%(name)s (preloadtype_%(name)s *f)
306 {
307 ctypes_%(name)s = f;
308 if (! real_%(name)s) {
309 /* Retry lookup in case the symbol wasn't
310 * resolvable until the program under test was
311 * loaded.
312 */
313 dlerror ();
314 real_%(name)s = dlsym (RTLD_NEXT, \"%(name)s\");
315 if (dlerror ()) _exit (1);
316 }
317 }
318 """) % conv, file=c)
319 if conv["ret"] == "void":
320 print(dedent("""\
321 void %(name)s (%(proto)s)
322 {
323 if (ctypes_%(name)s) {
324 delegate_%(name)s = 0;
325 (*ctypes_%(name)s) (%(args)s);
326 if (! delegate_%(name)s)
327 return;
328 }
329 (*real_%(name)s) (%(args)s);
330 }
331 """) % conv, file=c)
332 else:
333 print(dedent("""\
334 %(ret)s %(name)s (%(proto)s)
335 {
336 if (ctypes_%(name)s) {
337 %(ret)s ret;
338 delegate_%(name)s = 0;
339 ret = (*ctypes_%(name)s) (%(args)s);
340 if (! delegate_%(name)s)
341 return ret;
342 }
343 return (*real_%(name)s) (%(args)s);
344 }
345 """) % conv, file=c)
346 print(dedent("""\
347 extern void _gimock_delegate_%(name)s (void)
348 {
349 delegate_%(name)s = 1;
350 }
351 """) % conv, file=c)
352 print(dedent("""\
353 static void __attribute__ ((constructor))
354 gimockpreload_init (void)
355 {
356 dlerror ();
357 """), file=c)
358 for info, _ in rpreloads:
359 name = info["name"]
360 print(" real_%s = dlsym (RTLD_NEXT, \"%s\");" %
361 (name, name), file=c)
362 print(" if (dlerror ()) _exit (1);", file=c)
363 print("}", file=c)
364 if "GIMOCK_PRELOAD_DEBUG" in os.environ:
365 with open(c_path) as c:
366 print(c.read())
367 # TODO: Use libtool or similar rather than hardcoding gcc invocation.
368 lib_path = os.path.join(preloads_dir, "libgimockpreload.so")
369 cflags = subprocess.check_output([
370 "pkg-config", "--cflags", "glib-2.0", "gee-0.8"],
371 universal_newlines=True).rstrip("\n").split()
372 subprocess.check_call([
373 "gcc", "-O0", "-g", "-shared", "-fPIC", "-DPIC", "-I", "lib/click",
374 ] + cflags + [
375 "-Wl,-soname", "-Wl,libgimockpreload.so",
376 c_path, "-ldl", "-o", lib_path,
377 ])
378 return lib_path, rpreloads
379
380 # Use as:
381 # with self.run_in_subprocess("func", ...) as (enter, preloads):
382 # enter()
383 # # test case body; preloads["func"] will be a mock.MagicMock
384 # # instance
385 @contextlib.contextmanager
386 def run_in_subprocess(self, *patches):
387 preloads = {}
388 for patch in patches:
389 preloads[patch] = mock.MagicMock()
390 if preloads:
391 lib_path, rpreloads = self.make_preloads(preloads)
392 else:
393 lib_path, rpreloads = None, None
394
395 class ParentProcess(Exception):
396 pass
397
398 def helper(lib_path, rpreloads):
399 if "GIMOCK_SUBPROCESS" in os.environ:
400 del os.environ["LD_PRELOAD"]
401 preload_lib = ctypes.cdll.LoadLibrary(lib_path)
402 delegate_cfunctype = ctypes.CFUNCTYPE(None)
403 for info, func in rpreloads:
404 signature = [info["rv"]] + [
405 p["type"] for p in info["params"]]
406 signature = [self._ctypes_type(t) for t in signature]
407 cfunctype = ctypes.CFUNCTYPE(*signature)
408 init = getattr(
409 preload_lib, "_gimock_init_%s" % info["name"])
410 cfunc = cfunctype(func)
411 self._preload_func_refs.append(cfunc)
412 init(cfunc)
413 delegate = getattr(
414 preload_lib, "_gimock_delegate_%s" % info["name"])
415 self._delegate_funcs[info["name"]] = delegate_cfunctype(
416 delegate)
417 return
418 rfd, wfd = os.pipe()
419 # It would be cleaner to use subprocess.Popen(pass_fds=[wfd]), but
420 # that isn't available in Python 2.7.
421 if hasattr(os, "set_inheritable"):
422 os.set_inheritable(wfd, True)
423 else:
424 fcntl.fcntl(rfd, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
425 args = [
426 sys.executable, "-m", "unittest",
427 "%s.%s.%s" % (
428 self.__class__.__module__, self.__class__.__name__,
429 self._testMethodName)]
430 env = os.environ.copy()
431 env["GIMOCK_SUBPROCESS"] = str(wfd)
432 if lib_path is not None:
433 env["LD_PRELOAD"] = lib_path
434 subp = subprocess.Popen(args, close_fds=False, env=env)
435 os.close(wfd)
436 reader = os.fdopen(rfd, "rb")
437 subp.communicate()
438 exctype = pickle.load(reader)
439 if exctype is not None and issubclass(exctype, AssertionError):
440 raise AssertionError("Subprocess failed a test!")
441 elif exctype is not None or subp.returncode != 0:
442 raise Exception("Subprocess returned an error!")
443 reader.close()
444 raise ParentProcess()
445
446 try:
447 yield partial(helper, lib_path, rpreloads), preloads
448 if "GIMOCK_SUBPROCESS" in os.environ:
449 wfd = int(os.environ["GIMOCK_SUBPROCESS"])
450 writer = os.fdopen(wfd, "wb")
451 pickle.dump(None, writer)
452 writer.flush()
453 os._exit(0)
454 except ParentProcess:
455 pass
456 except Exception as e:
457 if "GIMOCK_SUBPROCESS" in os.environ:
458 wfd = int(os.environ["GIMOCK_SUBPROCESS"])
459 writer = os.fdopen(wfd, "wb")
460 # It would be better to use tblib to pickle the traceback so
461 # that we can re-raise it properly from the parent process.
462 # Until that's packaged and available to us, just print the
463 # traceback and send the exception type.
464 print()
465 traceback.print_exc()
466 pickle.dump(type(e), writer)
467 writer.flush()
468 os._exit(1)
469 else:
470 raise
471
472 def make_pointer(self, composite):
473 # Store a reference to a composite type and return a pointer to it,
474 # working around http://bugs.python.org/issue5710.
475 self._composite_refs.append(composite)
476 return ctypes.addressof(composite)
477
478 def make_string(self, s):
479 # As make_pointer, but for a string.
480 copied = ctypes.create_string_buffer(s.encode())
481 self._composite_refs.append(copied)
482 return ctypes.addressof(copied)
483
484 def convert_pointer(self, composite_type, address):
485 # Return a ctypes composite type instance at a given address.
486 return composite_type.from_address(address)
487
488 def convert_stat_pointer(self, name, address):
489 # As convert_pointer, but for a "struct stat *" or "struct stat64 *"
490 # depending on the wrapped function name.
491 stat_type = {"__xstat": Stat, "__xstat64": Stat64}
492 return self.convert_pointer(stat_type[name], address)
493
494 def delegate_to_original(self, name):
495 # Cause the wrapper function to delegate to the original version
496 # after the callback returns. (Note that the callback still needs
497 # to return something type-compatible with the declared result type,
498 # although the return value will otherwise be ignored.)
499 self._delegate_funcs[name]()
0500
=== added file 'click/tests/gimock_types.py'
--- click/tests/gimock_types.py 1970-01-01 00:00:00 +0000
+++ click/tests/gimock_types.py 2014-03-06 07:04:44 +0000
@@ -0,0 +1,89 @@
1# Copyright (C) 2014 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""A collection of variously hacky ctypes definitions for use with gimock."""
17
18import ctypes
19
20from click.tests.config import (
21 STAT_OFFSET_GID,
22 STAT_OFFSET_UID,
23 STAT64_OFFSET_GID,
24 STAT64_OFFSET_UID,
25 )
26
27
28class Passwd(ctypes.Structure):
29 _fields_ = [
30 ("pw_name", ctypes.c_char_p),
31 ("pw_passwd", ctypes.c_char_p),
32 ("pw_uid", ctypes.c_uint32),
33 ("pw_gid", ctypes.c_uint32),
34 ("pw_gecos", ctypes.c_char_p),
35 ("pw_dir", ctypes.c_char_p),
36 ("pw_shell", ctypes.c_char_p),
37 ]
38
39
40# TODO: This is pretty awful. The layout of "struct stat" is complicated
41# enough that we have to use offsetof() in configure to pick out the fields
42# we care about. Fortunately, we only care about a couple of fields, and
43# since this is an output parameter it doesn't matter if our structure is
44# too short (if we cared about this then we could use AC_CHECK_SIZEOF to
45# figure it out).
46class Stat(ctypes.Structure):
47 _pack_ = 1
48 _fields_ = []
49 _fields_.append(
50 ("pad0", ctypes.c_ubyte * min(STAT_OFFSET_UID, STAT_OFFSET_GID)))
51 if STAT_OFFSET_UID < STAT_OFFSET_GID:
52 _fields_.append(("st_uid", ctypes.c_uint32))
53 pad = (STAT_OFFSET_GID - STAT_OFFSET_UID -
54 ctypes.sizeof(ctypes.c_uint32))
55 assert pad >= 0
56 if pad > 0:
57 _fields_.append(("pad1", ctypes.c_ubyte * pad))
58 _fields_.append(("st_gid", ctypes.c_uint32))
59 else:
60 _fields_.append(("st_gid", ctypes.c_uint32))
61 pad = (STAT_OFFSET_UID - STAT_OFFSET_GID -
62 ctypes.sizeof(ctypes.c_uint32))
63 assert pad >= 0
64 if pad > 0:
65 _fields_.append(("pad1", ctypes.c_ubyte * pad))
66 _fields_.append(("st_uid", ctypes.c_uint32))
67
68
69class Stat64(ctypes.Structure):
70 _pack_ = 1
71 _fields_ = []
72 _fields_.append(
73 ("pad0", ctypes.c_ubyte * min(STAT64_OFFSET_UID, STAT64_OFFSET_GID)))
74 if STAT64_OFFSET_UID < STAT64_OFFSET_GID:
75 _fields_.append(("st_uid", ctypes.c_uint32))
76 pad = (STAT64_OFFSET_GID - STAT64_OFFSET_UID -
77 ctypes.sizeof(ctypes.c_uint32))
78 assert pad >= 0
79 if pad > 0:
80 _fields_.append(("pad1", ctypes.c_ubyte * pad))
81 _fields_.append(("st_gid", ctypes.c_uint32))
82 else:
83 _fields_.append(("st_gid", ctypes.c_uint32))
84 pad = (STAT64_OFFSET_UID - STAT64_OFFSET_GID -
85 ctypes.sizeof(ctypes.c_uint32))
86 assert pad >= 0
87 if pad > 0:
88 _fields_.append(("pad1", ctypes.c_ubyte * pad))
89 _fields_.append(("st_uid", ctypes.c_uint32))
090
=== modified file 'click/tests/helpers.py'
--- click/tests/helpers.py 2014-03-01 23:28:24 +0000
+++ click/tests/helpers.py 2014-03-06 07:04:44 +0000
@@ -39,10 +39,12 @@
39except ImportError:39except ImportError:
40 import mock40 import mock
4141
42from click import osextras42from gi.repository import Click, GLib
4343
4444from click.tests import gimock
45class TestCase(unittest.TestCase):45
46
47class TestCase(gimock.GIMockTestCase):
46 def setUp(self):48 def setUp(self):
47 super(TestCase, self).setUp()49 super(TestCase, self).setUp()
48 self.temp_dir = None50 self.temp_dir = None
@@ -74,6 +76,33 @@
74 if not hasattr(unittest.TestCase, 'assertRaisesRegex'):76 if not hasattr(unittest.TestCase, 'assertRaisesRegex'):
75 assertRaisesRegex = unittest.TestCase.assertRaisesRegexp77 assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
7678
79 def assertRaisesGError(self, domain_name, code, callableObj,
80 *args, **kwargs):
81 with self.assertRaises(GLib.GError) as cm:
82 callableObj(*args, **kwargs)
83 self.assertEqual(domain_name, cm.exception.domain)
84 self.assertEqual(code, cm.exception.code)
85
86 def assertRaisesFileError(self, code, callableObj, *args, **kwargs):
87 self.assertRaisesGError(
88 "g-file-error-quark", code, callableObj, *args, **kwargs)
89
90 def assertRaisesDatabaseError(self, code, callableObj, *args, **kwargs):
91 self.assertRaisesGError(
92 "click_database_error-quark", code, callableObj, *args, **kwargs)
93
94 def assertRaisesHooksError(self, code, callableObj, *args, **kwargs):
95 self.assertRaisesGError(
96 "click_hooks_error-quark", code, callableObj, *args, **kwargs)
97
98 def assertRaisesQueryError(self, code, callableObj, *args, **kwargs):
99 self.assertRaisesGError(
100 "click_query_error-quark", code, callableObj, *args, **kwargs)
101
102 def assertRaisesUserError(self, code, callableObj, *args, **kwargs):
103 self.assertRaisesGError(
104 "click_user_error-quark", code, callableObj, *args, **kwargs)
105
77106
78if not hasattr(mock, "call"):107if not hasattr(mock, "call"):
79 # mock 0.7.2, the version in Ubuntu 12.04 LTS, lacks mock.ANY and108 # mock 0.7.2, the version in Ubuntu 12.04 LTS, lacks mock.ANY and
@@ -228,14 +257,14 @@
228257
229@contextlib.contextmanager258@contextlib.contextmanager
230def mkfile(path, mode="w"):259def mkfile(path, mode="w"):
231 osextras.ensuredir(os.path.dirname(path))260 Click.ensuredir(os.path.dirname(path))
232 with open(path, mode) as f:261 with open(path, mode) as f:
233 yield f262 yield f
234263
235264
236@contextlib.contextmanager265@contextlib.contextmanager
237def mkfile_utf8(path, mode="w"):266def mkfile_utf8(path, mode="w"):
238 osextras.ensuredir(os.path.dirname(path))267 Click.ensuredir(os.path.dirname(path))
239 if sys.version < "3":268 if sys.version < "3":
240 import codecs269 import codecs
241 with codecs.open(path, mode, "UTF-8") as f:270 with codecs.open(path, mode, "UTF-8") as f:
242271
=== added file 'click/tests/preload.h'
--- click/tests/preload.h 1970-01-01 00:00:00 +0000
+++ click/tests/preload.h 2014-03-06 07:04:44 +0000
@@ -0,0 +1,110 @@
1#include <sys/stat.h>
2#include <sys/types.h>
3
4#include <glib.h>
5
6#include "click.h"
7
8/**
9 * chown:
10 *
11 * Attributes: (headers unistd.h)
12 */
13extern int chown (const char *file, uid_t owner, gid_t group);
14
15/* Workaround for g-ir-scanner not picking up the type properly: mode_t is
16 * uint32_t on all glibc platforms.
17 */
18/**
19 * mkdir:
20 * @mode: (type guint32)
21 *
22 * Attributes: (headers sys/stat.h,sys/types.h)
23 */
24extern int mkdir (const char *pathname, mode_t mode);
25
26/**
27 * getpwnam:
28 *
29 * Attributes: (headers sys/types.h,pwd.h)
30 * Returns: (transfer none):
31 */
32extern struct passwd *getpwnam (const char *name);
33
34/**
35 * under_under_xstat:
36 *
37 * Attributes: (headers sys/types.h,sys/stat.h,unistd.h)
38 */
39extern int under_under_xstat (int ver, const char *pathname, struct stat *buf);
40
41/**
42 * under_under_xstat64:
43 *
44 * Attributes: (headers sys/types.h,sys/stat.h,unistd.h)
45 */
46extern int under_under_xstat64 (int ver, const char *pathname, struct stat64 *buf);
47
48const gchar *g_get_user_name (void);
49
50/**
51 * g_spawn_sync:
52 * @argv: (array zero-terminated=1):
53 * @envp: (array zero-terminated=1):
54 * @flags: (type gint)
55 * @child_setup: (type gpointer)
56 * @standard_output: (out) (array zero-terminated=1) (element-type guint8):
57 * @standard_error: (out) (array zero-terminated=1) (element-type guint8):
58 * @exit_status: (out):
59 *
60 * Attributes: (headers glib.h)
61 */
62gboolean g_spawn_sync (const gchar *working_directory,
63 gchar **argv,
64 gchar **envp,
65 GSpawnFlags flags,
66 GSpawnChildSetupFunc child_setup,
67 gpointer user_data,
68 gchar **standard_output,
69 gchar **standard_error,
70 gint *exit_status,
71 GError **error);
72
73/**
74 * click_find_on_path:
75 *
76 * Attributes: (headers glib.h)
77 */
78gboolean click_find_on_path (const gchar *command);
79
80/**
81 * click_get_db_dir:
82 *
83 * Attributes: (headers glib.h)
84 */
85gchar *click_get_db_dir (void);
86
87/**
88 * click_get_hooks_dir:
89 *
90 * Attributes: (headers glib.h)
91 */
92gchar *click_get_hooks_dir (void);
93
94/**
95 * click_get_user_home:
96 *
97 * Attributes: (headers glib.h)
98 */
99gchar *click_get_user_home (const gchar *user_name);
100
101/**
102 * click_package_install_hooks:
103 * @db: (type gpointer)
104 *
105 * Attributes: (headers glib.h,click.h)
106 */
107void click_package_install_hooks (ClickDB *db, const gchar *package,
108 const gchar *old_version,
109 const gchar *new_version,
110 const gchar *user_name, GError **error);
0111
=== modified file 'click/tests/test_database.py'
--- click/tests/test_database.py 2013-12-10 14:33:19 +0000
+++ click/tests/test_database.py 2014-03-06 07:04:44 +0000
@@ -24,45 +24,46 @@
24 ]24 ]
2525
2626
27from functools import partial
28from itertools import takewhile
27import json29import json
28import os30import os
2931
30from click.database import ClickDB32from gi.repository import Click
31from click.tests.helpers import TestCase, mkfile, mock, touch33
3234from click.tests.gimock_types import Passwd
3335from click.tests.helpers import TestCase, mkfile, touch
34class MockPasswd:
35 def __init__(self, pw_uid, pw_gid):
36 self.pw_uid = pw_uid
37 self.pw_gid = pw_gid
38
39
40class MockStatResult:
41 original_stat = os.stat
42
43 def __init__(self, path, **override):
44 self.st = self.original_stat(path)
45 self.override = override
46
47 def __getattr__(self, name):
48 if name in self.override:
49 return self.override[name]
50 else:
51 return getattr(self.st, name)
5236
5337
54class TestClickSingleDB(TestCase):38class TestClickSingleDB(TestCase):
55 def setUp(self):39 def setUp(self):
56 super(TestClickSingleDB, self).setUp()40 super(TestClickSingleDB, self).setUp()
57 self.use_temp_dir()41 self.use_temp_dir()
58 self.master_db = ClickDB(extra_root=self.temp_dir, use_system=False)42 self.master_db = Click.DB()
59 self.db = self.master_db._db[-1]43 self.master_db.add(self.temp_dir)
44 self.db = self.master_db.get(self.master_db.props.size - 1)
45 self.spawn_calls = []
46
47 def g_spawn_sync_side_effect(self, status_map, working_directory, argv,
48 envp, flags, child_setup, user_data,
49 standard_output, standard_error, exit_status,
50 error):
51 self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv)))
52 if argv[0] in status_map:
53 exit_status[0] = status_map[argv[0]]
54 else:
55 self.delegate_to_original("g_spawn_sync")
56 return 0
57
58 def _installed_packages_tuplify(self, ip):
59 return [(p.props.package, p.props.version, p.props.path) for p in ip]
6060
61 def test_path(self):61 def test_path(self):
62 path = os.path.join(self.temp_dir, "a", "1.0")62 path = os.path.join(self.temp_dir, "a", "1.0")
63 os.makedirs(path)63 os.makedirs(path)
64 self.assertEqual(path, self.db.path("a", "1.0"))64 self.assertEqual(path, self.db.get_path("a", "1.0"))
65 self.assertRaises(KeyError, self.db.path, "a", "1.1")65 self.assertRaisesDatabaseError(
66 Click.DatabaseError.DOES_NOT_EXIST, self.db.get_path, "a", "1.1")
6667
67 def test_packages_current(self):68 def test_packages_current(self):
68 os.makedirs(os.path.join(self.temp_dir, "a", "1.0"))69 os.makedirs(os.path.join(self.temp_dir, "a", "1.0"))
@@ -76,7 +77,8 @@
76 self.assertEqual([77 self.assertEqual([
77 ("a", "1.1", a_current),78 ("a", "1.1", a_current),
78 ("b", "0.1", b_current),79 ("b", "0.1", b_current),
79 ], list(self.db.packages()))80 ], self._installed_packages_tuplify(
81 self.db.get_packages(all_versions=False)))
8082
81 def test_packages_all(self):83 def test_packages_all(self):
82 os.makedirs(os.path.join(self.temp_dir, "a", "1.0"))84 os.makedirs(os.path.join(self.temp_dir, "a", "1.0"))
@@ -90,120 +92,145 @@
90 ("a", "1.1", os.path.join(self.temp_dir, "a", "1.1")),92 ("a", "1.1", os.path.join(self.temp_dir, "a", "1.1")),
91 ("b", "0.1", os.path.join(self.temp_dir, "b", "0.1")),93 ("b", "0.1", os.path.join(self.temp_dir, "b", "0.1")),
92 ("c", "2.0", os.path.join(self.temp_dir, "c", "2.0")),94 ("c", "2.0", os.path.join(self.temp_dir, "c", "2.0")),
93 ], list(self.db.packages(all_versions=True)))95 ], self._installed_packages_tuplify(
9496 self.db.get_packages(all_versions=True)))
95 @mock.patch("subprocess.call")97
96 def test_app_running(self, mock_call):98 def test_app_running(self):
97 mock_call.return_value = 099 with self.run_in_subprocess("g_spawn_sync") as (enter, preloads):
98 self.assertTrue(self.db._app_running("foo", "bar", "1.0"))100 enter()
99 mock_call.assert_called_once_with(101 preloads["g_spawn_sync"].side_effect = partial(
100 ["upstart-app-pid", "foo_bar_1.0"], stdout=mock.ANY)102 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
101 mock_call.return_value = 1103 self.assertTrue(self.db.app_running("foo", "bar", "1.0"))
102 self.assertFalse(self.db._app_running("foo", "bar", "1.0"))104 self.assertEqual(
103105 [[b"upstart-app-pid", b"foo_bar_1.0"]], self.spawn_calls)
104 @mock.patch("click.osextras.find_on_path")106 preloads["g_spawn_sync"].side_effect = partial(
105 @mock.patch("subprocess.call")107 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
106 def test_any_app_running(self, mock_call, mock_find_on_path):108 self.assertFalse(self.db.app_running("foo", "bar", "1.0"))
107 manifest_path = os.path.join(109
108 self.temp_dir, "a", "1.0", ".click", "info", "a.manifest")110 def test_any_app_running(self):
109 with mkfile(manifest_path) as manifest:111 with self.run_in_subprocess(
110 json.dump({"hooks": {"a-app": {}}}, manifest)112 "click_find_on_path", "g_spawn_sync",
111 mock_call.return_value = 0113 ) as (enter, preloads):
112 mock_find_on_path.return_value = False114 enter()
113 self.assertFalse(self.db._any_app_running("a", "1.0"))115 manifest_path = os.path.join(
114 mock_find_on_path.return_value = True116 self.temp_dir, "a", "1.0", ".click", "info", "a.manifest")
115 self.assertTrue(self.db._any_app_running("a", "1.0"))117 with mkfile(manifest_path) as manifest:
116 mock_call.assert_called_once_with(118 json.dump({"hooks": {"a-app": {}}}, manifest)
117 ["upstart-app-pid", "a_a-app_1.0"], stdout=mock.ANY)119 preloads["g_spawn_sync"].side_effect = partial(
118 mock_call.return_value = 1120 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
119 self.assertFalse(self.db._any_app_running("a", "1.0"))121 preloads["click_find_on_path"].return_value = False
120122 self.assertFalse(self.db.any_app_running("a", "1.0"))
121 @mock.patch("click.osextras.find_on_path")123 preloads["click_find_on_path"].return_value = True
122 @mock.patch("subprocess.call")124 self.assertTrue(self.db.any_app_running("a", "1.0"))
123 def test_maybe_remove_registered(self, mock_call, mock_find_on_path):125 self.assertEqual(
124 version_path = os.path.join(self.temp_dir, "a", "1.0")126 [[b"upstart-app-pid", b"a_a-app_1.0"]], self.spawn_calls)
125 manifest_path = os.path.join(127 preloads["g_spawn_sync"].side_effect = partial(
126 version_path, ".click", "info", "a.manifest")128 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
127 with mkfile(manifest_path) as manifest:129 self.assertFalse(self.db.any_app_running("a", "1.0"))
128 json.dump({"hooks": {"a-app": {}}}, manifest)130
129 user_path = os.path.join(131 def test_maybe_remove_registered(self):
130 self.temp_dir, ".click", "users", "test-user", "a")132 with self.run_in_subprocess(
131 os.makedirs(os.path.dirname(user_path))133 "click_find_on_path", "g_spawn_sync",
132 os.symlink(version_path, user_path)134 ) as (enter, preloads):
133 mock_call.return_value = 0135 enter()
134 mock_find_on_path.return_value = True136 version_path = os.path.join(self.temp_dir, "a", "1.0")
135 self.db.maybe_remove("a", "1.0")137 manifest_path = os.path.join(
136 self.assertTrue(os.path.exists(version_path))138 version_path, ".click", "info", "a.manifest")
137 self.assertTrue(os.path.exists(user_path))139 with mkfile(manifest_path) as manifest:
138140 json.dump({"hooks": {"a-app": {}}}, manifest)
139 @mock.patch("click.osextras.find_on_path")141 user_path = os.path.join(
140 @mock.patch("subprocess.call")142 self.temp_dir, ".click", "users", "test-user", "a")
141 def test_maybe_remove_running(self, mock_call, mock_find_on_path):143 os.makedirs(os.path.dirname(user_path))
142 version_path = os.path.join(self.temp_dir, "a", "1.0")144 os.symlink(version_path, user_path)
143 manifest_path = os.path.join(145 preloads["g_spawn_sync"].side_effect = partial(
144 version_path, ".click", "info", "a.manifest")146 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
145 with mkfile(manifest_path) as manifest:147 preloads["click_find_on_path"].return_value = True
146 json.dump({"hooks": {"a-app": {}}}, manifest)148 self.db.maybe_remove("a", "1.0")
147 mock_call.return_value = 0149 self.assertTrue(os.path.exists(version_path))
148 mock_find_on_path.return_value = True150 self.assertTrue(os.path.exists(user_path))
149 self.db.maybe_remove("a", "1.0")151
150 gcinuse_path = os.path.join(152 def test_maybe_remove_running(self):
151 self.temp_dir, ".click", "users", "@gcinuse", "a")153 with self.run_in_subprocess(
152 self.assertTrue(os.path.islink(gcinuse_path))154 "click_find_on_path", "g_spawn_sync",
153 self.assertEqual(version_path, os.readlink(gcinuse_path))155 ) as (enter, preloads):
154 self.assertTrue(os.path.exists(version_path))156 enter()
155 self.db.maybe_remove("a", "1.0")157 version_path = os.path.join(self.temp_dir, "a", "1.0")
156 self.assertTrue(os.path.islink(gcinuse_path))158 manifest_path = os.path.join(
157 self.assertEqual(version_path, os.readlink(gcinuse_path))159 version_path, ".click", "info", "a.manifest")
158 self.assertTrue(os.path.exists(version_path))160 with mkfile(manifest_path) as manifest:
159161 json.dump({"hooks": {"a-app": {}}}, manifest)
160 @mock.patch("click.osextras.find_on_path")162 preloads["g_spawn_sync"].side_effect = partial(
161 @mock.patch("subprocess.call")163 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 0})
162 def test_maybe_remove_not_running(self, mock_call, mock_find_on_path):164 preloads["click_find_on_path"].return_value = True
163 version_path = os.path.join(self.temp_dir, "a", "1.0")165 self.db.maybe_remove("a", "1.0")
164 manifest_path = os.path.join(166 gcinuse_path = os.path.join(
165 version_path, ".click", "info", "a.manifest")167 self.temp_dir, ".click", "users", "@gcinuse", "a")
166 with mkfile(manifest_path) as manifest:168 self.assertTrue(os.path.islink(gcinuse_path))
167 json.dump({"hooks": {"a-app": {}}}, manifest)169 self.assertEqual(version_path, os.readlink(gcinuse_path))
168 current_path = os.path.join(self.temp_dir, "a", "current")170 self.assertTrue(os.path.exists(version_path))
169 os.symlink("1.0", current_path)171 self.db.maybe_remove("a", "1.0")
170 mock_call.return_value = 1172 self.assertTrue(os.path.islink(gcinuse_path))
171 mock_find_on_path.return_value = True173 self.assertEqual(version_path, os.readlink(gcinuse_path))
172 self.db.maybe_remove("a", "1.0")174 self.assertTrue(os.path.exists(version_path))
173 gcinuse_path = os.path.join(175
174 self.temp_dir, ".click", "users", "@gcinuse", "a")176 def test_maybe_remove_not_running(self):
175 self.assertFalse(os.path.islink(gcinuse_path))177 with self.run_in_subprocess(
176 self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a")))178 "click_find_on_path", "g_spawn_sync",
177179 ) as (enter, preloads):
178 @mock.patch("click.osextras.find_on_path")180 enter()
179 @mock.patch("subprocess.call")181 os.environ["TEST_QUIET"] = "1"
180 def test_gc(self, mock_call, mock_find_on_path):182 version_path = os.path.join(self.temp_dir, "a", "1.0")
181 a_path = os.path.join(self.temp_dir, "a", "1.0")183 manifest_path = os.path.join(
182 a_manifest_path = os.path.join(a_path, ".click", "info", "a.manifest")184 version_path, ".click", "info", "a.manifest")
183 with mkfile(a_manifest_path) as manifest:185 with mkfile(manifest_path) as manifest:
184 json.dump({"hooks": {"a-app": {}}}, manifest)186 json.dump({"hooks": {"a-app": {}}}, manifest)
185 b_path = os.path.join(self.temp_dir, "b", "1.0")187 current_path = os.path.join(self.temp_dir, "a", "current")
186 b_manifest_path = os.path.join(b_path, ".click", "info", "b.manifest")188 os.symlink("1.0", current_path)
187 with mkfile(b_manifest_path) as manifest:189 preloads["g_spawn_sync"].side_effect = partial(
188 json.dump({"hooks": {"b-app": {}}}, manifest)190 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
189 c_path = os.path.join(self.temp_dir, "c", "1.0")191 preloads["click_find_on_path"].return_value = True
190 c_manifest_path = os.path.join(c_path, ".click", "info", "c.manifest")192 self.db.maybe_remove("a", "1.0")
191 with mkfile(c_manifest_path) as manifest:193 gcinuse_path = os.path.join(
192 json.dump({"hooks": {"c-app": {}}}, manifest)194 self.temp_dir, ".click", "users", "@gcinuse", "a")
193 a_user_path = os.path.join(195 self.assertFalse(os.path.islink(gcinuse_path))
194 self.temp_dir, ".click", "users", "test-user", "a")196 self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a")))
195 os.makedirs(os.path.dirname(a_user_path))197
196 os.symlink(a_path, a_user_path)198 def test_gc(self):
197 b_gcinuse_path = os.path.join(199 with self.run_in_subprocess(
198 self.temp_dir, ".click", "users", "@gcinuse", "b")200 "click_find_on_path", "g_spawn_sync",
199 os.makedirs(os.path.dirname(b_gcinuse_path))201 ) as (enter, preloads):
200 os.symlink(b_path, b_gcinuse_path)202 enter()
201 mock_call.return_value = 1203 os.environ["TEST_QUIET"] = "1"
202 mock_find_on_path.return_value = True204 a_path = os.path.join(self.temp_dir, "a", "1.0")
203 self.db.gc(verbose=False)205 a_manifest_path = os.path.join(
204 self.assertTrue(os.path.exists(a_path))206 a_path, ".click", "info", "a.manifest")
205 self.assertFalse(os.path.exists(b_path))207 with mkfile(a_manifest_path) as manifest:
206 self.assertTrue(os.path.exists(c_path))208 json.dump({"hooks": {"a-app": {}}}, manifest)
209 b_path = os.path.join(self.temp_dir, "b", "1.0")
210 b_manifest_path = os.path.join(
211 b_path, ".click", "info", "b.manifest")
212 with mkfile(b_manifest_path) as manifest:
213 json.dump({"hooks": {"b-app": {}}}, manifest)
214 c_path = os.path.join(self.temp_dir, "c", "1.0")
215 c_manifest_path = os.path.join(
216 c_path, ".click", "info", "c.manifest")
217 with mkfile(c_manifest_path) as manifest:
218 json.dump({"hooks": {"c-app": {}}}, manifest)
219 a_user_path = os.path.join(
220 self.temp_dir, ".click", "users", "test-user", "a")
221 os.makedirs(os.path.dirname(a_user_path))
222 os.symlink(a_path, a_user_path)
223 b_gcinuse_path = os.path.join(
224 self.temp_dir, ".click", "users", "@gcinuse", "b")
225 os.makedirs(os.path.dirname(b_gcinuse_path))
226 os.symlink(b_path, b_gcinuse_path)
227 preloads["g_spawn_sync"].side_effect = partial(
228 self.g_spawn_sync_side_effect, {b"upstart-app-pid": 1 << 8})
229 preloads["click_find_on_path"].return_value = True
230 self.db.gc()
231 self.assertTrue(os.path.exists(a_path))
232 self.assertFalse(os.path.exists(b_path))
233 self.assertTrue(os.path.exists(c_path))
207234
208 def _make_ownership_test(self):235 def _make_ownership_test(self):
209 path = os.path.join(self.temp_dir, "a", "1.0")236 path = os.path.join(self.temp_dir, "a", "1.0")
@@ -215,60 +242,80 @@
215 os.symlink(path, user_path)242 os.symlink(path, user_path)
216 touch(os.path.join(self.temp_dir, ".click", "log"))243 touch(os.path.join(self.temp_dir, ".click", "log"))
217244
218 def test_clickpkg_paths(self):245 def _set_stat_side_effect(self, preloads, side_effect, limit):
219 self._make_ownership_test()246 limit = limit.encode()
220 self.assertCountEqual([247 preloads["__xstat"].side_effect = (
221 self.temp_dir,248 lambda ver, path, buf: side_effect(
222 os.path.join(self.temp_dir, ".click"),249 "__xstat", limit, ver, path, buf))
223 os.path.join(self.temp_dir, ".click", "log"),250 preloads["__xstat64"].side_effect = (
224 os.path.join(self.temp_dir, ".click", "users"),251 lambda ver, path, buf: side_effect(
225 os.path.join(self.temp_dir, "a"),252 "__xstat64", limit, ver, path, buf))
226 os.path.join(self.temp_dir, "a", "1.0"),253
227 os.path.join(self.temp_dir, "a", "1.0", ".click"),254 def test_ensure_ownership_quick_if_correct(self):
228 os.path.join(self.temp_dir, "a", "1.0", ".click", "info"),255 def stat_side_effect(name, limit, ver, path, buf):
229 os.path.join(256 st = self.convert_stat_pointer(name, buf)
230 self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"),257 if path == limit:
231 os.path.join(self.temp_dir, "a", "current"),258 st.st_uid = 1
232 ], list(self.db._clickpkg_paths()))259 st.st_gid = 1
233260 return 0
234 @mock.patch("pwd.getpwnam")261 else:
235 @mock.patch("os.chown")262 self.delegate_to_original(name)
236 def test_ensure_ownership_quick_if_correct(self, mock_chown,263 return -1
237 mock_getpwnam):264
238 mock_getpwnam.return_value = MockPasswd(pw_uid=1, pw_gid=1)265 with self.run_in_subprocess(
239 self._make_ownership_test()266 "chown", "getpwnam", "__xstat", "__xstat64",
240 with mock.patch("os.stat") as mock_stat:267 ) as (enter, preloads):
241 mock_stat.side_effect = (268 enter()
242 lambda path, *args, **kwargs: MockStatResult(269 preloads["getpwnam"].side_effect = (
243 path, st_uid=1, st_gid=1))270 lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1)))
244 self.db.ensure_ownership()271 self._set_stat_side_effect(
245 self.assertFalse(mock_chown.called)272 preloads, stat_side_effect, self.db.props.root)
246273
247 @mock.patch("pwd.getpwnam")274 self._make_ownership_test()
248 @mock.patch("os.chown")275 self.db.ensure_ownership()
249 def test_ensure_ownership(self, mock_chown, mock_getpwnam):276 self.assertFalse(preloads["chown"].called)
250 mock_getpwnam.return_value = MockPasswd(pw_uid=1, pw_gid=1)277
251 self._make_ownership_test()278 def test_ensure_ownership(self):
252 with mock.patch("os.stat") as mock_stat:279 def stat_side_effect(name, limit, ver, path, buf):
253 mock_stat.side_effect = (280 st = self.convert_stat_pointer(name, buf)
254 lambda path, *args, **kwargs: MockStatResult(281 if path == limit:
255 path, st_uid=2, st_gid=2))282 st.st_uid = 2
256 self.db.ensure_ownership()283 st.st_gid = 2
257 self.assertCountEqual([284 return 0
258 self.temp_dir,285 else:
259 os.path.join(self.temp_dir, ".click"),286 self.delegate_to_original(name)
260 os.path.join(self.temp_dir, ".click", "log"),287 return -1
261 os.path.join(self.temp_dir, ".click", "users"),288
262 os.path.join(self.temp_dir, "a"),289 with self.run_in_subprocess(
263 os.path.join(self.temp_dir, "a", "1.0"),290 "chown", "getpwnam", "__xstat", "__xstat64",
264 os.path.join(self.temp_dir, "a", "1.0", ".click"),291 ) as (enter, preloads):
265 os.path.join(self.temp_dir, "a", "1.0", ".click", "info"),292 enter()
266 os.path.join(293 preloads["getpwnam"].side_effect = (
267 self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"),294 lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1)))
268 os.path.join(self.temp_dir, "a", "current"),295 self._set_stat_side_effect(
269 ], [args[0][0] for args in mock_chown.call_args_list])296 preloads, stat_side_effect, self.db.props.root)
270 self.assertCountEqual(297
271 [(1, 1)], set(args[0][1:] for args in mock_chown.call_args_list))298 self._make_ownership_test()
299 self.db.ensure_ownership()
300 expected_paths = [
301 self.temp_dir,
302 os.path.join(self.temp_dir, ".click"),
303 os.path.join(self.temp_dir, ".click", "log"),
304 os.path.join(self.temp_dir, ".click", "users"),
305 os.path.join(self.temp_dir, "a"),
306 os.path.join(self.temp_dir, "a", "1.0"),
307 os.path.join(self.temp_dir, "a", "1.0", ".click"),
308 os.path.join(self.temp_dir, "a", "1.0", ".click", "info"),
309 os.path.join(
310 self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"),
311 os.path.join(self.temp_dir, "a", "current"),
312 ]
313 self.assertCountEqual(
314 [path.encode() for path in expected_paths],
315 [args[0][0] for args in preloads["chown"].call_args_list])
316 self.assertCountEqual(
317 [(1, 1)],
318 set(args[0][1:] for args in preloads["chown"].call_args_list))
272319
273320
274class TestClickDB(TestCase):321class TestClickDB(TestCase):
@@ -276,6 +323,11 @@
276 super(TestClickDB, self).setUp()323 super(TestClickDB, self).setUp()
277 self.use_temp_dir()324 self.use_temp_dir()
278325
326 def _installed_packages_tuplify(self, ip):
327 return [
328 (p.props.package, p.props.version, p.props.path, p.props.writeable)
329 for p in ip]
330
279 def test_read_configuration(self):331 def test_read_configuration(self):
280 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:332 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
281 print("[Click Database]", file=a)333 print("[Click Database]", file=a)
@@ -283,23 +335,27 @@
283 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:335 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
284 print("[Click Database]", file=b)336 print("[Click Database]", file=b)
285 print("root = /b", file=b)337 print("root = /b", file=b)
286 db = ClickDB(extra_root="/c", override_db_dir=self.temp_dir)338 db = Click.DB()
287 self.assertEqual(3, len(db))339 db.read(db_dir=self.temp_dir)
288 self.assertEqual(["/a", "/b", "/c"], [d.root for d in db])340 db.add("/c")
341 self.assertEqual(3, db.props.size)
342 self.assertEqual(
343 ["/a", "/b", "/c"],
344 [db.get(i).props.root for i in range(db.props.size)])
289345
290 def test_no_use_system(self):346 def test_no_read(self):
291 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:347 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
292 print("[Click Database]", file=a)348 print("[Click Database]", file=a)
293 print("root = /a", file=a)349 print("root = /a", file=a)
294 db = ClickDB(use_system=False, override_db_dir=self.temp_dir)350 db = Click.DB()
295 self.assertEqual(0, len(db))351 self.assertEqual(0, db.props.size)
296352
297 def test_add(self):353 def test_add(self):
298 db = ClickDB(use_system=False)354 db = Click.DB()
299 self.assertEqual(0, len(db))355 self.assertEqual(0, db.props.size)
300 db.add("/new/root")356 db.add("/new/root")
301 self.assertEqual(1, len(db))357 self.assertEqual(1, db.props.size)
302 self.assertEqual(["/new/root"], [d.root for d in db])358 self.assertEqual("/new/root", db.get(0).props.root)
303359
304 def test_overlay(self):360 def test_overlay(self):
305 with open(os.path.join(self.temp_dir, "00_custom.conf"), "w") as f:361 with open(os.path.join(self.temp_dir, "00_custom.conf"), "w") as f:
@@ -308,8 +364,9 @@
308 with open(os.path.join(self.temp_dir, "99_default.conf"), "w") as f:364 with open(os.path.join(self.temp_dir, "99_default.conf"), "w") as f:
309 print("[Click Database]", file=f)365 print("[Click Database]", file=f)
310 print("root = /opt/click.ubuntu.com", file=f)366 print("root = /opt/click.ubuntu.com", file=f)
311 db = ClickDB(override_db_dir=self.temp_dir)367 db = Click.DB()
312 self.assertEqual("/opt/click.ubuntu.com", db.overlay)368 db.read(db_dir=self.temp_dir)
369 self.assertEqual("/opt/click.ubuntu.com", db.props.overlay)
313370
314 def test_path(self):371 def test_path(self):
315 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:372 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
@@ -318,21 +375,24 @@
318 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:375 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
319 print("[Click Database]", file=b)376 print("[Click Database]", file=b)
320 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)377 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)
321 db = ClickDB(override_db_dir=self.temp_dir)378 db = Click.DB()
322 self.assertRaises(KeyError, db.path, "pkg", "1.0")379 db.read(db_dir=self.temp_dir)
380 self.assertRaisesDatabaseError(
381 Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.0")
323 os.makedirs(os.path.join(self.temp_dir, "a", "pkg", "1.0"))382 os.makedirs(os.path.join(self.temp_dir, "a", "pkg", "1.0"))
324 self.assertEqual(383 self.assertEqual(
325 os.path.join(self.temp_dir, "a", "pkg", "1.0"),384 os.path.join(self.temp_dir, "a", "pkg", "1.0"),
326 db.path("pkg", "1.0"))385 db.get_path("pkg", "1.0"))
327 self.assertRaises(KeyError, db.path, "pkg", "1.1")386 self.assertRaisesDatabaseError(
387 Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.1")
328 os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.0"))388 os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.0"))
329 self.assertEqual(389 self.assertEqual(
330 os.path.join(self.temp_dir, "b", "pkg", "1.0"),390 os.path.join(self.temp_dir, "b", "pkg", "1.0"),
331 db.path("pkg", "1.0"))391 db.get_path("pkg", "1.0"))
332 os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.1"))392 os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.1"))
333 self.assertEqual(393 self.assertEqual(
334 os.path.join(self.temp_dir, "b", "pkg", "1.1"),394 os.path.join(self.temp_dir, "b", "pkg", "1.1"),
335 db.path("pkg", "1.1"))395 db.get_path("pkg", "1.1"))
336396
337 def test_packages_current(self):397 def test_packages_current(self):
338 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:398 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
@@ -341,8 +401,9 @@
341 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:401 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
342 print("[Click Database]", file=b)402 print("[Click Database]", file=b)
343 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)403 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)
344 db = ClickDB(override_db_dir=self.temp_dir)404 db = Click.DB()
345 self.assertEqual([], list(db.packages()))405 db.read(db_dir=self.temp_dir)
406 self.assertEqual([], list(db.get_packages(all_versions=False)))
346 os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0"))407 os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0"))
347 os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current"))408 os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current"))
348 os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1"))409 os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1"))
@@ -354,7 +415,8 @@
354 self.assertEqual([415 self.assertEqual([
355 ("pkg1", "1.1", pkg1_current, True),416 ("pkg1", "1.1", pkg1_current, True),
356 ("pkg2", "0.1", pkg2_current, True),417 ("pkg2", "0.1", pkg2_current, True),
357 ], list(db.packages()))418 ], self._installed_packages_tuplify(
419 db.get_packages(all_versions=False)))
358420
359 def test_packages_all(self):421 def test_packages_all(self):
360 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:422 with open(os.path.join(self.temp_dir, "a.conf"), "w") as a:
@@ -363,8 +425,9 @@
363 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:425 with open(os.path.join(self.temp_dir, "b.conf"), "w") as b:
364 print("[Click Database]", file=b)426 print("[Click Database]", file=b)
365 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)427 print("root = %s" % os.path.join(self.temp_dir, "b"), file=b)
366 db = ClickDB(override_db_dir=self.temp_dir)428 db = Click.DB()
367 self.assertEqual([], list(db.packages()))429 db.read(db_dir=self.temp_dir)
430 self.assertEqual([], list(db.get_packages(all_versions=False)))
368 os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0"))431 os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0"))
369 os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current"))432 os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current"))
370 os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1"))433 os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1"))
@@ -378,4 +441,5 @@
378 True),441 True),
379 ("pkg1", "1.0", os.path.join(self.temp_dir, "a", "pkg1", "1.0"),442 ("pkg1", "1.0", os.path.join(self.temp_dir, "a", "pkg1", "1.0"),
380 False),443 False),
381 ], list(db.packages(all_versions=True)))444 ], self._installed_packages_tuplify(
445 db.get_packages(all_versions=True)))
382446
=== modified file 'click/tests/test_hooks.py'
--- click/tests/test_hooks.py 2014-02-19 15:31:55 +0000
+++ click/tests/test_hooks.py 2014-03-06 07:04:44 +0000
@@ -27,824 +27,957 @@
27 ]27 ]
2828
2929
30import contextlib30from functools import partial
31from itertools import takewhile
31import json32import json
32import os33import os
33from textwrap import dedent34from textwrap import dedent
3435
35from click import hooks36from gi.repository import Click, GLib
36from click.database import ClickDB37
37from click.hooks import (38from click.tests.gimock_types import Passwd
38 ClickHook,39from click.tests.helpers import TestCase, mkfile, mkfile_utf8
39 ClickPatternFormatter,
40 package_install_hooks,
41 package_remove_hooks,
42 )
43from click.user import ClickUser
44from click.tests.helpers import TestCase, mkfile, mkfile_utf8, mock
45
46
47@contextlib.contextmanager
48def temp_hooks_dir(new_dir):
49 old_dir = hooks.hooks_dir
50 try:
51 hooks.hooks_dir = new_dir
52 yield
53 finally:
54 hooks.hooks_dir = old_dir
5540
5641
57class TestClickPatternFormatter(TestCase):42class TestClickPatternFormatter(TestCase):
58 def setUp(self):43 def _make_variant(self, **kwargs):
59 super(TestClickPatternFormatter, self).setUp()44 # pygobject's Variant creator can't handle maybe types, so we have
60 self.formatter = ClickPatternFormatter()45 # to do this by hand.
46 builder = GLib.VariantBuilder.new(GLib.VariantType.new("a{sms}"))
47 for key, value in kwargs.items():
48 entry = GLib.VariantBuilder.new(GLib.VariantType.new("{sms}"))
49 entry.add_value(GLib.Variant.new_string(key))
50 entry.add_value(GLib.Variant.new_maybe(
51 GLib.VariantType.new("s"),
52 None if value is None else GLib.Variant.new_string(value)))
53 builder.add_value(entry.end())
54 return builder.end()
6155
62 def test_expands_provided_keys(self):56 def test_expands_provided_keys(self):
63 self.assertEqual(57 self.assertEqual(
64 "foo.bar", self.formatter.format("foo.${key}", key="bar"))58 "foo.bar",
59 Click.pattern_format("foo.${key}", self._make_variant(key="bar")))
65 self.assertEqual(60 self.assertEqual(
66 "foo.barbaz",61 "foo.barbaz",
67 self.formatter.format(62 Click.pattern_format(
68 "foo.${key1}${key2}", key1="bar", key2="baz"))63 "foo.${key1}${key2}",
64 self._make_variant(key1="bar", key2="baz")))
6965
70 def test_expands_missing_keys_to_empty_string(self):66 def test_expands_missing_keys_to_empty_string(self):
71 self.assertEqual("xy", self.formatter.format("x${key}y"))67 self.assertEqual(
68 "xy", Click.pattern_format("x${key}y", self._make_variant()))
7269
73 def test_preserves_unmatched_dollar(self):70 def test_preserves_unmatched_dollar(self):
74 self.assertEqual("$", self.formatter.format("$"))71 self.assertEqual("$", Click.pattern_format("$", self._make_variant()))
75 self.assertEqual("$ {foo}", self.formatter.format("$ {foo}"))72 self.assertEqual(
76 self.assertEqual("x${y", self.formatter.format("${key}${y", key="x"))73 "$ {foo}", Click.pattern_format("$ {foo}", self._make_variant()))
74 self.assertEqual(
75 "x${y",
76 Click.pattern_format("${key}${y", self._make_variant(key="x")))
7777
78 def test_double_dollar(self):78 def test_double_dollar(self):
79 self.assertEqual("$", self.formatter.format("$$"))79 self.assertEqual("$", Click.pattern_format("$$", self._make_variant()))
80 self.assertEqual("${foo}", self.formatter.format("$${foo}"))80 self.assertEqual(
81 self.assertEqual("x$y", self.formatter.format("x$$${key}", key="y"))81 "${foo}", Click.pattern_format("$${foo}", self._make_variant()))
82 self.assertEqual(
83 "x$y",
84 Click.pattern_format("x$$${key}", self._make_variant(key="y")))
8285
83 def test_possible_expansion(self):86 def test_possible_expansion(self):
84 self.assertEqual(87 self.assertEqual(
85 {"id": "abc"},88 {"id": "abc"},
86 self.formatter.possible_expansion(89 Click.pattern_possible_expansion(
87 "x_abc_1", "x_${id}_${num}", num="1"))90 "x_abc_1", "x_${id}_${num}",
91 self._make_variant(num="1")).unpack())
88 self.assertIsNone(92 self.assertIsNone(
89 self.formatter.possible_expansion(93 Click.pattern_possible_expansion(
90 "x_abc_1", "x_${id}_${num}", num="2"))94 "x_abc_1", "x_${id}_${num}", self._make_variant(num="2")))
9195
9296
93class TestClickHookBase(TestCase):97class TestClickHookBase(TestCase):
94 def setUp(self):98 def setUp(self):
95 super(TestClickHookBase, self).setUp()99 super(TestClickHookBase, self).setUp()
96 self.use_temp_dir()100 self.use_temp_dir()
97 self.db = ClickDB(self.temp_dir)101 self.db = Click.DB()
102 self.db.add(self.temp_dir)
103 self.spawn_calls = []
104
105 def _setup_hooks_dir(self, preloads, hooks_dir=None):
106 if hooks_dir is None:
107 hooks_dir = self.temp_dir
108 preloads["click_get_hooks_dir"].side_effect = (
109 lambda: self.make_string(hooks_dir))
110
111 def g_spawn_sync_side_effect(self, status_map, working_directory, argv,
112 envp, flags, child_setup, user_data,
113 standard_output, standard_error, exit_status,
114 error):
115 self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv)))
116 if argv[0] in status_map:
117 exit_status[0] = status_map[argv[0]]
118 else:
119 self.delegate_to_original("g_spawn_sync")
120 return 0
98121
99122
100class TestClickHookSystemLevel(TestClickHookBase):123class TestClickHookSystemLevel(TestClickHookBase):
101 def test_open(self):124 def test_open(self):
102 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:125 with self.run_in_subprocess(
103 print(dedent("""\126 "click_get_hooks_dir") as (enter, preloads):
104 Pattern: /usr/share/test/${id}.test127 enter()
105 # Comment128 self._setup_hooks_dir(preloads)
106 Exec: test-update129 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
107 User: root130 print(dedent("""\
108 """), file=f)131 Pattern: /usr/share/test/${id}.test
109 with temp_hooks_dir(self.temp_dir):132 # Comment
110 hook = ClickHook.open(self.db, "test")133 Exec: test-update
111 self.assertCountEqual(["Pattern", "Exec", "User"], hook.keys())134 User: root
112 self.assertEqual("/usr/share/test/${id}.test", hook["pattern"])135 """), file=f)
113 self.assertEqual("test-update", hook["exec"])136 hook = Click.Hook.open(self.db, "test")
114 self.assertFalse(hook.user_level)137 self.assertCountEqual(
138 ["pattern", "exec", "user"], hook.get_fields())
139 self.assertEqual(
140 "/usr/share/test/${id}.test", hook.get_field("pattern"))
141 self.assertEqual("test-update", hook.get_field("exec"))
142 self.assertFalse(hook.props.is_user_level)
115143
116 def test_hook_name_absent(self):144 def test_hook_name_absent(self):
117 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:145 with self.run_in_subprocess(
118 print("Pattern: /usr/share/test/${id}.test", file=f)146 "click_get_hooks_dir") as (enter, preloads):
119 with temp_hooks_dir(self.temp_dir):147 enter()
120 hook = ClickHook.open(self.db, "test")148 self._setup_hooks_dir(preloads)
121 self.assertEqual("test", hook.hook_name)149 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
150 print("Pattern: /usr/share/test/${id}.test", file=f)
151 hook = Click.Hook.open(self.db, "test")
152 self.assertEqual("test", hook.get_hook_name())
122153
123 def test_hook_name_present(self):154 def test_hook_name_present(self):
124 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:155 with self.run_in_subprocess(
125 print("Pattern: /usr/share/test/${id}.test", file=f)156 "click_get_hooks_dir") as (enter, preloads):
126 print("Hook-Name: other", file=f)157 enter()
127 with temp_hooks_dir(self.temp_dir):158 self._setup_hooks_dir(preloads)
128 hook = ClickHook.open(self.db, "test")159 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
129 self.assertEqual("other", hook.hook_name)160 print("Pattern: /usr/share/test/${id}.test", file=f)
161 print("Hook-Name: other", file=f)
162 hook = Click.Hook.open(self.db, "test")
163 self.assertEqual("other", hook.get_hook_name())
130164
131 def test_invalid_app_id(self):165 def test_invalid_app_id(self):
132 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:166 with self.run_in_subprocess(
133 print(dedent("""\167 "click_get_hooks_dir") as (enter, preloads):
134 Pattern: /usr/share/test/${id}.test168 enter()
135 # Comment169 self._setup_hooks_dir(preloads)
136 Exec: test-update170 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
137 User: root171 print(dedent("""\
138 """), file=f)172 Pattern: /usr/share/test/${id}.test
139 with temp_hooks_dir(self.temp_dir):173 # Comment
140 hook = ClickHook.open(self.db, "test")174 Exec: test-update
141 self.assertRaises(175 User: root
142 ValueError, hook.app_id, "package", "0.1", "app_name")176 """), file=f)
143 self.assertRaises(177 hook = Click.Hook.open(self.db, "test")
144 ValueError, hook.app_id, "package", "0.1", "app/name")178 self.assertRaisesHooksError(
179 Click.HooksError.BAD_APP_NAME, hook.get_app_id,
180 "package", "0.1", "app_name")
181 self.assertRaisesHooksError(
182 Click.HooksError.BAD_APP_NAME, hook.get_app_id,
183 "package", "0.1", "app/name")
145184
146 def test_short_id_invalid(self):185 def test_short_id_invalid(self):
147 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:186 with self.run_in_subprocess(
148 print("Pattern: /usr/share/test/${short-id}.test", file=f)187 "click_get_hooks_dir") as (enter, preloads):
149 with temp_hooks_dir(self.temp_dir):188 enter()
150 hook = ClickHook.open(self.db, "test")189 self._setup_hooks_dir(preloads)
151 # It would perhaps be better if unrecognised $-expansions raised190 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
152 # KeyError, but they don't right now.191 print("Pattern: /usr/share/test/${short-id}.test", file=f)
153 self.assertEqual(192 hook = Click.Hook.open(self.db, "test")
154 "/usr/share/test/.test",193 # It would perhaps be better if unrecognised $-expansions raised
155 hook.pattern("package", "0.1", "app-name"))194 # KeyError, but they don't right now.
195 self.assertEqual(
196 "/usr/share/test/.test",
197 hook.get_pattern("package", "0.1", "app-name"))
156198
157 def test_short_id_valid_with_single_version(self):199 def test_short_id_valid_with_single_version(self):
158 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:200 with self.run_in_subprocess(
159 print("Pattern: /usr/share/test/${short-id}.test", file=f)201 "click_get_hooks_dir") as (enter, preloads):
160 print("Single-Version: yes", file=f)202 enter()
161 with temp_hooks_dir(self.temp_dir):203 self._setup_hooks_dir(preloads)
162 hook = ClickHook.open(self.db, "test")204 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
163 self.assertEqual(205 print("Pattern: /usr/share/test/${short-id}.test", file=f)
164 "/usr/share/test/package_app-name.test",206 print("Single-Version: yes", file=f)
165 hook.pattern("package", "0.1", "app-name"))207 hook = Click.Hook.open(self.db, "test")
166208 self.assertEqual(
167 @mock.patch("subprocess.check_call")209 "/usr/share/test/package_app-name.test",
168 def test_run_commands(self, mock_check_call):210 hook.get_pattern("package", "0.1", "app-name"))
169 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:211
170 print("Exec: test-update", file=f)212 def test_run_commands(self):
171 print("User: root", file=f)213 with self.run_in_subprocess(
172 with temp_hooks_dir(self.temp_dir):214 "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads):
173 hook = ClickHook.open(self.db, "test")215 enter()
174 self.assertEqual("root", hook._run_commands_user(user=None))216 self._setup_hooks_dir(preloads)
175 hook._run_commands(user=None)217 preloads["g_spawn_sync"].side_effect = partial(
176 mock_check_call.assert_called_once_with(218 self.g_spawn_sync_side_effect, {b"/bin/sh": 0})
177 "test-update", preexec_fn=mock.ANY, shell=True)219 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
178220 print("Exec: test-update", file=f)
179 def test_previous_entries(self):221 print("User: root", file=f)
180 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:222 hook = Click.Hook.open(self.db, "test")
181 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)223 self.assertEqual(
182 link_one = os.path.join(224 "root", hook.get_run_commands_user(user_name=None))
183 self.temp_dir, "org.example.package_test-app_1.0.test")225 hook.run_commands(user_name=None)
184 link_two = os.path.join(226 self.assertEqual(
185 self.temp_dir, "org.example.package_test-app_2.0.test")227 [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls)
186 os.symlink("dummy", link_one)
187 os.symlink("dummy", link_two)
188 os.symlink("dummy", os.path.join(self.temp_dir, "malformed"))
189 with temp_hooks_dir(self.temp_dir):
190 hook = ClickHook.open(self.db, "test")
191 self.assertCountEqual([
192 (link_one, "org.example.package", "1.0", "test-app"),
193 (link_two, "org.example.package", "2.0", "test-app"),
194 ], list(hook._previous_entries()))
195228
196 def test_install_package(self):229 def test_install_package(self):
197 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:230 with self.run_in_subprocess(
198 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)231 "click_get_hooks_dir") as (enter, preloads):
199 os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0"))232 enter()
200 with temp_hooks_dir(self.temp_dir):233 self._setup_hooks_dir(preloads)
201 hook = ClickHook.open(self.db, "test")234 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
202 hook.install_package(235 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
203 "org.example.package", "1.0", "test-app", "foo/bar")236 os.makedirs(
204 symlink_path = os.path.join(237 os.path.join(self.temp_dir, "org.example.package", "1.0"))
205 self.temp_dir, "org.example.package_test-app_1.0.test")238 hook = Click.Hook.open(self.db, "test")
206 target_path = os.path.join(239 hook.install_package(
207 self.temp_dir, "org.example.package", "1.0", "foo", "bar")240 "org.example.package", "1.0", "test-app", "foo/bar")
208 self.assertTrue(os.path.islink(symlink_path))241 symlink_path = os.path.join(
209 self.assertEqual(target_path, os.readlink(symlink_path))242 self.temp_dir, "org.example.package_test-app_1.0.test")
243 target_path = os.path.join(
244 self.temp_dir, "org.example.package", "1.0", "foo", "bar")
245 self.assertTrue(os.path.islink(symlink_path))
246 self.assertEqual(target_path, os.readlink(symlink_path))
210247
211 def test_install_package_trailing_slash(self):248 def test_install_package_trailing_slash(self):
212 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:249 with self.run_in_subprocess(
213 print("Pattern: %s/${id}/" % self.temp_dir, file=f)250 "click_get_hooks_dir") as (enter, preloads):
214 os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0"))251 enter()
215 with temp_hooks_dir(self.temp_dir):252 self._setup_hooks_dir(preloads)
216 hook = ClickHook.open(self.db, "test")253 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
217 hook.install_package("org.example.package", "1.0", "test-app", "foo")254 print("Pattern: %s/${id}/" % self.temp_dir, file=f)
218 symlink_path = os.path.join(255 os.makedirs(
219 self.temp_dir, "org.example.package_test-app_1.0")256 os.path.join(self.temp_dir, "org.example.package", "1.0"))
220 target_path = os.path.join(257 hook = Click.Hook.open(self.db, "test")
221 self.temp_dir, "org.example.package", "1.0", "foo")258 hook.install_package(
222 self.assertTrue(os.path.islink(symlink_path))259 "org.example.package", "1.0", "test-app", "foo")
223 self.assertEqual(target_path, os.readlink(symlink_path))260 symlink_path = os.path.join(
261 self.temp_dir, "org.example.package_test-app_1.0")
262 target_path = os.path.join(
263 self.temp_dir, "org.example.package", "1.0", "foo")
264 self.assertTrue(os.path.islink(symlink_path))
265 self.assertEqual(target_path, os.readlink(symlink_path))
224266
225 def test_upgrade(self):267 def test_upgrade(self):
226 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:268 with self.run_in_subprocess(
227 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)269 "click_get_hooks_dir") as (enter, preloads):
228 symlink_path = os.path.join(270 enter()
229 self.temp_dir, "org.example.package_test-app_1.0.test")271 self._setup_hooks_dir(preloads)
230 os.symlink("old-target", symlink_path)272 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
231 os.makedirs(os.path.join(self.temp_dir, "org.example.package", "1.0"))273 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
232 with temp_hooks_dir(self.temp_dir):274 symlink_path = os.path.join(
233 hook = ClickHook.open(self.db, "test")275 self.temp_dir, "org.example.package_test-app_1.0.test")
234 hook.install_package(276 os.symlink("old-target", symlink_path)
235 "org.example.package", "1.0", "test-app", "foo/bar")277 os.makedirs(
236 target_path = os.path.join(278 os.path.join(self.temp_dir, "org.example.package", "1.0"))
237 self.temp_dir, "org.example.package", "1.0", "foo", "bar")279 hook = Click.Hook.open(self.db, "test")
238 self.assertTrue(os.path.islink(symlink_path))280 hook.install_package(
239 self.assertEqual(target_path, os.readlink(symlink_path))281 "org.example.package", "1.0", "test-app", "foo/bar")
282 target_path = os.path.join(
283 self.temp_dir, "org.example.package", "1.0", "foo", "bar")
284 self.assertTrue(os.path.islink(symlink_path))
285 self.assertEqual(target_path, os.readlink(symlink_path))
240286
241 def test_remove_package(self):287 def test_remove_package(self):
242 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:288 with self.run_in_subprocess(
243 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)289 "click_get_hooks_dir") as (enter, preloads):
244 symlink_path = os.path.join(290 enter()
245 self.temp_dir, "org.example.package_test-app_1.0.test")291 self._setup_hooks_dir(preloads)
246 os.symlink("old-target", symlink_path)292 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
247 with temp_hooks_dir(self.temp_dir):293 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
248 hook = ClickHook.open(self.db, "test")294 symlink_path = os.path.join(
249 hook.remove_package("org.example.package", "1.0", "test-app")295 self.temp_dir, "org.example.package_test-app_1.0.test")
250 self.assertFalse(os.path.exists(symlink_path))296 os.symlink("old-target", symlink_path)
297 hook = Click.Hook.open(self.db, "test")
298 hook.remove_package("org.example.package", "1.0", "test-app")
299 self.assertFalse(os.path.exists(symlink_path))
251300
252 def test_install(self):301 def test_install(self):
253 with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:302 with self.run_in_subprocess(
254 print("Pattern: %s/${id}.new" % self.temp_dir, file=f)303 "click_get_hooks_dir") as (enter, preloads):
255 with mkfile_utf8(os.path.join(304 enter()
256 self.temp_dir, "test-1", "1.0", ".click", "info",305 self._setup_hooks_dir(
257 "test-1.manifest")) as f:306 preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
258 json.dump({307 with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:
259 "maintainer":308 print("Pattern: %s/${id}.new" % self.temp_dir, file=f)
260 b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),309 with mkfile_utf8(os.path.join(
261 "hooks": {"test1-app": {"new": "target-1"}},310 self.temp_dir, "test-1", "1.0", ".click", "info",
262 }, f, ensure_ascii=False)311 "test-1.manifest")) as f:
263 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))312 json.dump({
264 with mkfile_utf8(os.path.join(313 "maintainer":
265 self.temp_dir, "test-2", "2.0", ".click", "info",314 b"Unic\xc3\xb3de <unicode@example.org>".decode(
266 "test-2.manifest")) as f:315 "UTF-8"),
267 json.dump({316 "hooks": {"test1-app": {"new": "target-1"}},
268 "maintainer":317 }, f, ensure_ascii=False)
269 b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),318 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
270 "hooks": {"test1-app": {"new": "target-2"}},319 with mkfile_utf8(os.path.join(
271 }, f, ensure_ascii=False)320 self.temp_dir, "test-2", "2.0", ".click", "info",
272 os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))321 "test-2.manifest")) as f:
273 with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):322 json.dump({
274 hook = ClickHook.open(self.db, "new")323 "maintainer":
275 hook.install()324 b"Unic\xc3\xb3de <unicode@example.org>".decode(
276 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")325 "UTF-8"),
277 self.assertTrue(os.path.lexists(path_1))326 "hooks": {"test1-app": {"new": "target-2"}},
278 self.assertEqual(327 }, f, ensure_ascii=False)
279 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),328 os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))
280 os.readlink(path_1))329 hook = Click.Hook.open(self.db, "new")
281 path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")330 hook.install()
282 self.assertTrue(os.path.lexists(path_2))331 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")
283 self.assertEqual(332 self.assertTrue(os.path.lexists(path_1))
284 os.path.join(self.temp_dir, "test-2", "2.0", "target-2"),333 self.assertEqual(
285 os.readlink(path_2))334 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
335 os.readlink(path_1))
336 path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")
337 self.assertTrue(os.path.lexists(path_2))
338 self.assertEqual(
339 os.path.join(self.temp_dir, "test-2", "2.0", "target-2"),
340 os.readlink(path_2))
286341
287 def test_remove(self):342 def test_remove(self):
288 with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:343 with self.run_in_subprocess(
289 print("Pattern: %s/${id}.old" % self.temp_dir, file=f)344 "click_get_hooks_dir") as (enter, preloads):
290 with mkfile(os.path.join(345 enter()
291 self.temp_dir, "test-1", "1.0", ".click", "info",346 self._setup_hooks_dir(
292 "test-1.manifest")) as f:347 preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
293 json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)348 with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:
294 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))349 print("Pattern: %s/${id}.old" % self.temp_dir, file=f)
295 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")350 with mkfile(os.path.join(
296 os.symlink(351 self.temp_dir, "test-1", "1.0", ".click", "info",
297 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1)352 "test-1.manifest")) as f:
298 with mkfile(os.path.join(353 json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)
299 self.temp_dir, "test-2", "2.0", ".click", "info",354 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
300 "test-2.manifest")) as f:355 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")
301 json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)356 os.symlink(
302 os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))357 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
303 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")358 path_1)
304 os.symlink(359 with mkfile(os.path.join(
305 os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), path_2)360 self.temp_dir, "test-2", "2.0", ".click", "info",
306 with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):361 "test-2.manifest")) as f:
307 hook = ClickHook.open(self.db, "old")362 json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)
308 hook.remove()363 os.symlink("2.0", os.path.join(self.temp_dir, "test-2", "current"))
309 self.assertFalse(os.path.exists(path_1))364 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")
310 self.assertFalse(os.path.exists(path_2))365 os.symlink(
366 os.path.join(self.temp_dir, "test-2", "2.0", "target-2"),
367 path_2)
368 hook = Click.Hook.open(self.db, "old")
369 hook.remove()
370 self.assertFalse(os.path.exists(path_1))
371 self.assertFalse(os.path.exists(path_2))
311372
312 def test_sync(self):373 def test_sync(self):
313 with mkfile(os.path.join(self.temp_dir, "hooks", "test.hook")) as f:374 with self.run_in_subprocess(
314 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)375 "click_get_hooks_dir") as (enter, preloads):
315 with mkfile(os.path.join(376 enter()
316 self.temp_dir, "test-1", "1.0", ".click", "info",377 self._setup_hooks_dir(
317 "test-1.manifest")) as f:378 preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
318 json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)379 with mkfile(os.path.join(
319 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))380 self.temp_dir, "hooks", "test.hook")) as f:
320 with mkfile(os.path.join(381 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
321 self.temp_dir, "test-2", "1.1", ".click", "info",382 with mkfile(os.path.join(
322 "test-2.manifest")) as f:383 self.temp_dir, "test-1", "1.0", ".click", "info",
323 json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)384 "test-1.manifest")) as f:
324 os.symlink("1.1", os.path.join(self.temp_dir, "test-2", "current"))385 json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)
325 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")386 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
326 os.symlink(387 with mkfile(os.path.join(
327 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1)388 self.temp_dir, "test-2", "1.1", ".click", "info",
328 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")389 "test-2.manifest")) as f:
329 path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")390 json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)
330 os.symlink(391 os.symlink("1.1", os.path.join(self.temp_dir, "test-2", "current"))
331 os.path.join(self.temp_dir, "test-3", "1.0", "target-3"), path_3)392 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")
332 with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):393 os.symlink(
333 hook = ClickHook.open(self.db, "test")394 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
334 hook.sync()395 path_1)
335 self.assertTrue(os.path.lexists(path_1))396 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")
336 self.assertEqual(397 path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")
337 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),398 os.symlink(
338 os.readlink(path_1))399 os.path.join(self.temp_dir, "test-3", "1.0", "target-3"),
339 self.assertTrue(os.path.lexists(path_2))400 path_3)
340 self.assertEqual(401 hook = Click.Hook.open(self.db, "test")
341 os.path.join(self.temp_dir, "test-2", "1.1", "target-2"),402 hook.sync()
342 os.readlink(path_2))403 self.assertTrue(os.path.lexists(path_1))
343 self.assertFalse(os.path.lexists(path_3))404 self.assertEqual(
405 os.path.join(self.temp_dir, "test-1", "1.0", "target-1"),
406 os.readlink(path_1))
407 self.assertTrue(os.path.lexists(path_2))
408 self.assertEqual(
409 os.path.join(self.temp_dir, "test-2", "1.1", "target-2"),
410 os.readlink(path_2))
411 self.assertFalse(os.path.lexists(path_3))
344412
345413
346class TestClickHookUserLevel(TestClickHookBase):414class TestClickHookUserLevel(TestClickHookBase):
347 def test_open(self):415 def test_open(self):
348 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:416 with self.run_in_subprocess(
349 print(dedent("""\417 "click_get_hooks_dir") as (enter, preloads):
350 User-Level: yes418 enter()
351 Pattern: ${home}/.local/share/test/${id}.test419 self._setup_hooks_dir(preloads)
352 # Comment420 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
353 Exec: test-update421 print(dedent("""\
354 """), file=f)422 User-Level: yes
355 with temp_hooks_dir(self.temp_dir):423 Pattern: ${home}/.local/share/test/${id}.test
356 hook = ClickHook.open(self.db, "test")424 # Comment
357 self.assertCountEqual(["User-Level", "Pattern", "Exec"], hook.keys())425 Exec: test-update
358 self.assertEqual(426 """), file=f)
359 "${home}/.local/share/test/${id}.test", hook["pattern"])427 hook = Click.Hook.open(self.db, "test")
360 self.assertEqual("test-update", hook["exec"])428 self.assertCountEqual(
361 self.assertTrue(hook.user_level)429 ["user-level", "pattern", "exec"], hook.get_fields())
430 self.assertEqual(
431 "${home}/.local/share/test/${id}.test",
432 hook.get_field("pattern"))
433 self.assertEqual("test-update", hook.get_field("exec"))
434 self.assertTrue(hook.props.is_user_level)
362435
363 def test_hook_name_absent(self):436 def test_hook_name_absent(self):
364 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:437 with self.run_in_subprocess(
365 print("User-Level: yes", file=f)438 "click_get_hooks_dir") as (enter, preloads):
366 print("Pattern: ${home}/.local/share/test/${id}.test", file=f)439 enter()
367 with temp_hooks_dir(self.temp_dir):440 self._setup_hooks_dir(preloads)
368 hook = ClickHook.open(self.db, "test")441 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
369 self.assertEqual("test", hook.hook_name)442 print("User-Level: yes", file=f)
443 print("Pattern: ${home}/.local/share/test/${id}.test", file=f)
444 hook = Click.Hook.open(self.db, "test")
445 self.assertEqual("test", hook.get_hook_name())
370446
371 def test_hook_name_present(self):447 def test_hook_name_present(self):
372 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:448 with self.run_in_subprocess(
373 print("User-Level: yes", file=f)449 "click_get_hooks_dir") as (enter, preloads):
374 print("Pattern: ${home}/.local/share/test/${id}.test", file=f)450 enter()
375 print("Hook-Name: other", file=f)451 self._setup_hooks_dir(preloads)
376 with temp_hooks_dir(self.temp_dir):452 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
377 hook = ClickHook.open(self.db, "test")453 print("User-Level: yes", file=f)
378 self.assertEqual("other", hook.hook_name)454 print("Pattern: ${home}/.local/share/test/${id}.test", file=f)
455 print("Hook-Name: other", file=f)
456 hook = Click.Hook.open(self.db, "test")
457 self.assertEqual("other", hook.get_hook_name())
379458
380 def test_invalid_app_id(self):459 def test_invalid_app_id(self):
381 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:460 with self.run_in_subprocess(
382 print(dedent("""\461 "click_get_hooks_dir") as (enter, preloads):
383 User-Level: yes462 enter()
384 Pattern: ${home}/.local/share/test/${id}.test463 self._setup_hooks_dir(preloads)
385 # Comment464 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
386 Exec: test-update465 print(dedent("""\
387 """), file=f)466 User-Level: yes
388 with temp_hooks_dir(self.temp_dir):467 Pattern: ${home}/.local/share/test/${id}.test
389 hook = ClickHook.open(self.db, "test")468 # Comment
390 self.assertRaises(469 Exec: test-update
391 ValueError, hook.app_id, "package", "0.1", "app_name")470 """), file=f)
392 self.assertRaises(471 hook = Click.Hook.open(self.db, "test")
393 ValueError, hook.app_id, "package", "0.1", "app/name")472 self.assertRaisesHooksError(
394473 Click.HooksError.BAD_APP_NAME, hook.get_app_id,
395 @mock.patch("pwd.getpwnam")474 "package", "0.1", "app_name")
396 def test_short_id_valid(self, mock_getpwnam):475 self.assertRaisesHooksError(
397 class MockPasswd:476 Click.HooksError.BAD_APP_NAME, hook.get_app_id,
398 def __init__(self, pw_dir):477 "package", "0.1", "app/name")
399 self.pw_dir = pw_dir478
400479 def test_short_id_valid(self):
401 mock_getpwnam.return_value = MockPasswd(pw_dir="/mock")480 with self.run_in_subprocess(
402 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:481 "click_get_hooks_dir", "getpwnam") as (enter, preloads):
403 print("User-Level: yes", file=f)482 enter()
404 print(483 self._setup_hooks_dir(preloads)
405 "Pattern: ${home}/.local/share/test/${short-id}.test", file=f)484 preloads["getpwnam"].side_effect = (
406 with temp_hooks_dir(self.temp_dir):485 lambda name: self.make_pointer(Passwd(pw_dir=b"/mock")))
407 hook = ClickHook.open(self.db, "test")486 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
408 self.assertEqual(487 print("User-Level: yes", file=f)
409 "/mock/.local/share/test/package_app-name.test",488 print(
410 hook.pattern("package", "0.1", "app-name", user="mock"))489 "Pattern: ${home}/.local/share/test/${short-id}.test",
411490 file=f)
412 @mock.patch("subprocess.check_call")491 hook = Click.Hook.open(self.db, "test")
413 def test_run_commands(self, mock_check_call):492 self.assertEqual(
414 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:493 "/mock/.local/share/test/package_app-name.test",
415 print("User-Level: yes", file=f)494 hook.get_pattern(
416 print("Exec: test-update", file=f)495 "package", "0.1", "app-name", user_name="mock"))
417 with temp_hooks_dir(self.temp_dir):496
418 hook = ClickHook.open(self.db, "test")497 def test_run_commands(self):
419 self.assertEqual(498 with self.run_in_subprocess(
420 "test-user", hook._run_commands_user(user="test-user"))499 "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads):
421 hook._run_commands(user="test-user")500 enter()
422 mock_check_call.assert_called_once_with(501 self._setup_hooks_dir(preloads)
423 "test-update", preexec_fn=mock.ANY, shell=True)502 preloads["g_spawn_sync"].side_effect = partial(
424503 self.g_spawn_sync_side_effect, {b"/bin/sh": 0})
425 @mock.patch("click.hooks.ClickHook._user_home")504 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
426 def test_previous_entries(self, mock_user_home):505 print("User-Level: yes", file=f)
427 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:506 print("Exec: test-update", file=f)
428 print("User-Level: yes", file=f)507 hook = Click.Hook.open(self.db, "test")
429 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)508 self.assertEqual(
430 link_one = os.path.join(509 "test-user", hook.get_run_commands_user(user_name="test-user"))
431 self.temp_dir, "org.example.package_test-app_1.0.test")510 hook.run_commands(user_name="test-user")
432 link_two = os.path.join(511 self.assertEqual(
433 self.temp_dir, "org.example.package_test-app_2.0.test")512 [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls)
434 os.symlink("dummy", link_one)513
435 os.symlink("dummy", link_two)514 def test_install_package(self):
436 os.symlink("dummy", os.path.join(self.temp_dir, "malformed"))515 with self.run_in_subprocess(
437 with temp_hooks_dir(self.temp_dir):516 "click_get_hooks_dir", "click_get_user_home",
438 hook = ClickHook.open(self.db, "test")517 ) as (enter, preloads):
439 self.assertCountEqual([518 enter()
440 (link_one, "org.example.package", "1.0", "test-app"),519 self._setup_hooks_dir(preloads)
441 (link_two, "org.example.package", "2.0", "test-app"),520 preloads["click_get_user_home"].return_value = "/home/test-user"
442 ], list(hook._previous_entries(user="test-user")))
443
444 @mock.patch("click.hooks.ClickHook._user_home")
445 def test_install_package(self, mock_user_home):
446 mock_user_home.return_value = "/home/test-user"
447 with temp_hooks_dir(self.temp_dir):
448 os.makedirs(os.path.join(521 os.makedirs(os.path.join(
449 self.temp_dir, "org.example.package", "1.0"))522 self.temp_dir, "org.example.package", "1.0"))
450 user_db = ClickUser(self.db, user="test-user")523 user_db = Click.User.for_user(self.db, "test-user")
451 user_db.set_version("org.example.package", "1.0")524 user_db.set_version("org.example.package", "1.0")
452 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:525 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
453 print("User-Level: yes", file=f)526 print("User-Level: yes", file=f)
454 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)527 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
455 hook = ClickHook.open(self.db, "test")528 hook = Click.Hook.open(self.db, "test")
456 hook.install_package(529 hook.install_package(
457 "org.example.package", "1.0", "test-app", "foo/bar",530 "org.example.package", "1.0", "test-app", "foo/bar",
458 user="test-user")531 user_name="test-user")
459 symlink_path = os.path.join(532 symlink_path = os.path.join(
460 self.temp_dir, "org.example.package_test-app_1.0.test")533 self.temp_dir, "org.example.package_test-app_1.0.test")
461 target_path = os.path.join(534 target_path = os.path.join(
462 self.temp_dir, ".click", "users", "test-user",535 self.temp_dir, ".click", "users", "test-user",
463 "org.example.package", "foo", "bar")536 "org.example.package", "foo", "bar")
464 self.assertTrue(os.path.islink(symlink_path))537 self.assertTrue(os.path.islink(symlink_path))
465 self.assertEqual(target_path, os.readlink(symlink_path))538 self.assertEqual(target_path, os.readlink(symlink_path))
466539
467 @mock.patch("click.hooks.ClickHook._user_home")540 def test_install_package_trailing_slash(self):
468 def test_install_package_trailing_slash(self, mock_user_home):541 with self.run_in_subprocess(
469 mock_user_home.return_value = "/home/test-user"542 "click_get_hooks_dir", "click_get_user_home",
470 with temp_hooks_dir(self.temp_dir):543 ) as (enter, preloads):
544 enter()
545 self._setup_hooks_dir(preloads)
546 preloads["click_get_user_home"].return_value = "/home/test-user"
471 os.makedirs(os.path.join(547 os.makedirs(os.path.join(
472 self.temp_dir, "org.example.package", "1.0"))548 self.temp_dir, "org.example.package", "1.0"))
473 user_db = ClickUser(self.db, user="test-user")549 user_db = Click.User.for_user(self.db, "test-user")
474 user_db.set_version("org.example.package", "1.0")550 user_db.set_version("org.example.package", "1.0")
475 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:551 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
476 print("User-Level: yes", file=f)552 print("User-Level: yes", file=f)
477 print("Pattern: %s/${id}/" % self.temp_dir, file=f)553 print("Pattern: %s/${id}/" % self.temp_dir, file=f)
478 hook = ClickHook.open(self.db, "test")554 hook = Click.Hook.open(self.db, "test")
479 hook.install_package(555 hook.install_package(
480 "org.example.package", "1.0", "test-app", "foo", user="test-user")556 "org.example.package", "1.0", "test-app", "foo",
481 symlink_path = os.path.join(557 user_name="test-user")
482 self.temp_dir, "org.example.package_test-app_1.0")558 symlink_path = os.path.join(
483 target_path = os.path.join(559 self.temp_dir, "org.example.package_test-app_1.0")
484 self.temp_dir, ".click", "users", "test-user",560 target_path = os.path.join(
485 "org.example.package", "foo")561 self.temp_dir, ".click", "users", "test-user",
486 self.assertTrue(os.path.islink(symlink_path))562 "org.example.package", "foo")
487 self.assertEqual(target_path, os.readlink(symlink_path))563 self.assertTrue(os.path.islink(symlink_path))
564 self.assertEqual(target_path, os.readlink(symlink_path))
488565
489 @mock.patch("click.hooks.ClickHook._user_home")566 def test_install_package_removes_previous(self):
490 def test_install_package_removes_previous(self, mock_user_home):567 with self.run_in_subprocess(
491 mock_user_home.return_value = "/home/test-user"568 "click_get_hooks_dir", "click_get_user_home",
492 with temp_hooks_dir(self.temp_dir):569 ) as (enter, preloads):
570 enter()
571 self._setup_hooks_dir(preloads)
572 preloads["click_get_user_home"].return_value = "/home/test-user"
493 os.makedirs(os.path.join(573 os.makedirs(os.path.join(
494 self.temp_dir, "org.example.package", "1.0"))574 self.temp_dir, "org.example.package", "1.0"))
495 os.makedirs(os.path.join(575 os.makedirs(os.path.join(
496 self.temp_dir, "org.example.package", "1.1"))576 self.temp_dir, "org.example.package", "1.1"))
497 user_db = ClickUser(self.db, user="test-user")577 user_db = Click.User.for_user(self.db, "test-user")
498 user_db.set_version("org.example.package", "1.0")578 user_db.set_version("org.example.package", "1.0")
499 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:579 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
500 print("User-Level: yes", file=f)580 print("User-Level: yes", file=f)
501 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)581 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
502 hook = ClickHook.open(self.db, "test")582 hook = Click.Hook.open(self.db, "test")
503 hook.install_package(583 hook.install_package(
504 "org.example.package", "1.0", "test-app", "foo/bar",584 "org.example.package", "1.0", "test-app", "foo/bar",
505 user="test-user")585 user_name="test-user")
506 hook.install_package(586 hook.install_package(
507 "org.example.package", "1.1", "test-app", "foo/bar",587 "org.example.package", "1.1", "test-app", "foo/bar",
508 user="test-user")588 user_name="test-user")
509 old_symlink_path = os.path.join(589 old_symlink_path = os.path.join(
510 self.temp_dir, "org.example.package_test-app_1.0.test")590 self.temp_dir, "org.example.package_test-app_1.0.test")
511 symlink_path = os.path.join(591 symlink_path = os.path.join(
512 self.temp_dir, "org.example.package_test-app_1.1.test")592 self.temp_dir, "org.example.package_test-app_1.1.test")
513 self.assertFalse(os.path.islink(old_symlink_path))593 self.assertFalse(os.path.islink(old_symlink_path))
514 self.assertTrue(os.path.islink(symlink_path))594 self.assertTrue(os.path.islink(symlink_path))
515 target_path = os.path.join(595 target_path = os.path.join(
516 self.temp_dir, ".click", "users", "test-user",596 self.temp_dir, ".click", "users", "test-user",
517 "org.example.package", "foo", "bar")597 "org.example.package", "foo", "bar")
518 self.assertEqual(target_path, os.readlink(symlink_path))598 self.assertEqual(target_path, os.readlink(symlink_path))
519599
520 @mock.patch("click.hooks.ClickHook._user_home")600 def test_upgrade(self):
521 def test_upgrade(self, mock_user_home):601 with self.run_in_subprocess(
522 mock_user_home.return_value = "/home/test-user"602 "click_get_hooks_dir", "click_get_user_home",
523 symlink_path = os.path.join(603 ) as (enter, preloads):
524 self.temp_dir, "org.example.package_test-app_1.0.test")604 enter()
525 os.symlink("old-target", symlink_path)605 self._setup_hooks_dir(preloads)
526 with temp_hooks_dir(self.temp_dir):606 preloads["click_get_user_home"].return_value = "/home/test-user"
607 symlink_path = os.path.join(
608 self.temp_dir, "org.example.package_test-app_1.0.test")
609 os.symlink("old-target", symlink_path)
527 os.makedirs(os.path.join(610 os.makedirs(os.path.join(
528 self.temp_dir, "org.example.package", "1.0"))611 self.temp_dir, "org.example.package", "1.0"))
529 user_db = ClickUser(self.db, user="test-user")612 user_db = Click.User.for_user(self.db, "test-user")
530 user_db.set_version("org.example.package", "1.0")613 user_db.set_version("org.example.package", "1.0")
531 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:614 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
532 print("User-Level: yes", file=f)615 print("User-Level: yes", file=f)
533 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)616 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
534 hook = ClickHook.open(self.db, "test")617 hook = Click.Hook.open(self.db, "test")
535 hook.install_package(618 hook.install_package(
536 "org.example.package", "1.0", "test-app", "foo/bar",619 "org.example.package", "1.0", "test-app", "foo/bar",
537 user="test-user")620 user_name="test-user")
538 target_path = os.path.join(621 target_path = os.path.join(
539 self.temp_dir, ".click", "users", "test-user",622 self.temp_dir, ".click", "users", "test-user",
540 "org.example.package", "foo", "bar")623 "org.example.package", "foo", "bar")
541 self.assertTrue(os.path.islink(symlink_path))624 self.assertTrue(os.path.islink(symlink_path))
542 self.assertEqual(target_path, os.readlink(symlink_path))625 self.assertEqual(target_path, os.readlink(symlink_path))
543626
544 @mock.patch("click.hooks.ClickHook._user_home")627 def test_remove_package(self):
545 def test_remove_package(self, mock_user_home):628 with self.run_in_subprocess(
546 mock_user_home.return_value = "/home/test-user"629 "click_get_hooks_dir", "click_get_user_home",
547 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:630 ) as (enter, preloads):
548 print("User-Level: yes", file=f)631 enter()
549 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)632 self._setup_hooks_dir(preloads)
550 symlink_path = os.path.join(633 preloads["click_get_user_home"].return_value = "/home/test-user"
551 self.temp_dir, "org.example.package_test-app_1.0.test")634 with mkfile(os.path.join(self.temp_dir, "test.hook")) as f:
552 os.symlink("old-target", symlink_path)635 print("User-Level: yes", file=f)
553 with temp_hooks_dir(self.temp_dir):636 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
554 hook = ClickHook.open(self.db, "test")637 symlink_path = os.path.join(
555 hook.remove_package(638 self.temp_dir, "org.example.package_test-app_1.0.test")
556 "org.example.package", "1.0", "test-app", user="test-user")639 os.symlink("old-target", symlink_path)
557 self.assertFalse(os.path.exists(symlink_path))640 hook = Click.Hook.open(self.db, "test")
558641 hook.remove_package(
559 @mock.patch("click.hooks.ClickHook._user_home")642 "org.example.package", "1.0", "test-app",
560 def test_install(self, mock_user_home):643 user_name="test-user")
561 mock_user_home.return_value = "/home/test-user"644 self.assertFalse(os.path.exists(symlink_path))
562 with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:645
563 print("User-Level: yes", file=f)646 def test_install(self):
564 print("Pattern: %s/${id}.new" % self.temp_dir, file=f)647 with self.run_in_subprocess(
565 user_db = ClickUser(self.db, user="test-user")648 "click_get_hooks_dir", "click_get_user_home",
566 with mkfile_utf8(os.path.join(649 ) as (enter, preloads):
567 self.temp_dir, "test-1", "1.0", ".click", "info",650 enter()
568 "test-1.manifest")) as f:651 self._setup_hooks_dir(preloads)
569 json.dump({652 preloads["click_get_user_home"].return_value = "/home/test-user"
570 "maintainer":653 with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f:
571 b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),654 print("User-Level: yes", file=f)
572 "hooks": {"test1-app": {"new": "target-1"}},655 print("Pattern: %s/${id}.new" % self.temp_dir, file=f)
573 }, f, ensure_ascii=False)656 user_db = Click.User.for_user(self.db, "test-user")
574 user_db.set_version("test-1", "1.0")657 with mkfile_utf8(os.path.join(
575 with mkfile_utf8(os.path.join(658 self.temp_dir, "test-1", "1.0", ".click", "info",
576 self.temp_dir, "test-2", "2.0", ".click", "info",659 "test-1.manifest")) as f:
577 "test-2.manifest")) as f:660 json.dump({
578 json.dump({661 "maintainer":
579 "maintainer":662 b"Unic\xc3\xb3de <unicode@example.org>".decode(
580 b"Unic\xc3\xb3de <unicode@example.org>".decode("UTF-8"),663 "UTF-8"),
581 "hooks": {"test1-app": {"new": "target-2"}},664 "hooks": {"test1-app": {"new": "target-1"}},
582 }, f, ensure_ascii=False)665 }, f, ensure_ascii=False)
583 user_db.set_version("test-2", "2.0")666 user_db.set_version("test-1", "1.0")
584 with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):667 with mkfile_utf8(os.path.join(
585 hook = ClickHook.open(self.db, "new")668 self.temp_dir, "test-2", "2.0", ".click", "info",
586 hook.install()669 "test-2.manifest")) as f:
587 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")670 json.dump({
588 self.assertTrue(os.path.lexists(path_1))671 "maintainer":
589 self.assertEqual(672 b"Unic\xc3\xb3de <unicode@example.org>".decode(
590 os.path.join(673 "UTF-8"),
591 self.temp_dir, ".click", "users", "test-user", "test-1",674 "hooks": {"test1-app": {"new": "target-2"}},
592 "target-1"),675 }, f, ensure_ascii=False)
593 os.readlink(path_1))676 user_db.set_version("test-2", "2.0")
594 path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")677 self._setup_hooks_dir(
595 self.assertTrue(os.path.lexists(path_2))678 preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
596 self.assertEqual(679 hook = Click.Hook.open(self.db, "new")
597 os.path.join(680 hook.install()
598 self.temp_dir, ".click", "users", "test-user", "test-2",681 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new")
599 "target-2"),682 self.assertTrue(os.path.lexists(path_1))
600 os.readlink(path_2))683 self.assertEqual(
601684 os.path.join(
602 os.unlink(path_1)685 self.temp_dir, ".click", "users", "test-user", "test-1",
603 os.unlink(path_2)686 "target-1"),
604 hook.install(user="another-user")687 os.readlink(path_1))
605 self.assertFalse(os.path.lexists(path_1))688 path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new")
606 self.assertFalse(os.path.lexists(path_2))689 self.assertTrue(os.path.lexists(path_2))
607690 self.assertEqual(
608 hook.install(user="test-user")691 os.path.join(
609 self.assertTrue(os.path.lexists(path_1))692 self.temp_dir, ".click", "users", "test-user", "test-2",
610 self.assertEqual(693 "target-2"),
611 os.path.join(694 os.readlink(path_2))
612 self.temp_dir, ".click", "users", "test-user", "test-1",695
613 "target-1"),696 os.unlink(path_1)
614 os.readlink(path_1))697 os.unlink(path_2)
615 self.assertTrue(os.path.lexists(path_2))698 hook.install(user_name="another-user")
616 self.assertEqual(699 self.assertFalse(os.path.lexists(path_1))
617 os.path.join(700 self.assertFalse(os.path.lexists(path_2))
618 self.temp_dir, ".click", "users", "test-user", "test-2",701
619 "target-2"),702 hook.install(user_name="test-user")
620 os.readlink(path_2))703 self.assertTrue(os.path.lexists(path_1))
621704 self.assertEqual(
622 @mock.patch("click.hooks.ClickHook._user_home")705 os.path.join(
623 def test_remove(self, mock_user_home):706 self.temp_dir, ".click", "users", "test-user", "test-1",
624 mock_user_home.return_value = "/home/test-user"707 "target-1"),
625 with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:708 os.readlink(path_1))
626 print("User-Level: yes", file=f)709 self.assertTrue(os.path.lexists(path_2))
627 print("Pattern: %s/${id}.old" % self.temp_dir, file=f)710 self.assertEqual(
628 user_db = ClickUser(self.db, user="test-user")711 os.path.join(
629 with mkfile(os.path.join(712 self.temp_dir, ".click", "users", "test-user", "test-2",
630 self.temp_dir, "test-1", "1.0", ".click", "info",713 "target-2"),
631 "test-1.manifest")) as f:714 os.readlink(path_2))
632 json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)715
633 user_db.set_version("test-1", "1.0")716 def test_remove(self):
634 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))717 with self.run_in_subprocess(
635 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")718 "click_get_hooks_dir", "click_get_user_home",
636 os.symlink(os.path.join(user_db.path("test-1"), "target-1"), path_1)719 ) as (enter, preloads):
637 with mkfile(os.path.join(720 enter()
638 self.temp_dir, "test-2", "2.0", ".click", "info",721 self._setup_hooks_dir(preloads)
639 "test-2.manifest")) as f:722 preloads["click_get_user_home"].return_value = "/home/test-user"
640 json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)723 with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f:
641 user_db.set_version("test-2", "2.0")724 print("User-Level: yes", file=f)
642 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")725 print("Pattern: %s/${id}.old" % self.temp_dir, file=f)
643 os.symlink(os.path.join(user_db.path("test-2"), "target-2"), path_2)726 user_db = Click.User.for_user(self.db, "test-user")
644 with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):727 with mkfile(os.path.join(
645 hook = ClickHook.open(self.db, "old")728 self.temp_dir, "test-1", "1.0", ".click", "info",
646 hook.remove()729 "test-1.manifest")) as f:
647 self.assertFalse(os.path.exists(path_1))730 json.dump({"hooks": {"test1-app": {"old": "target-1"}}}, f)
648 self.assertFalse(os.path.exists(path_2))731 user_db.set_version("test-1", "1.0")
649732 os.symlink("1.0", os.path.join(self.temp_dir, "test-1", "current"))
650 @mock.patch("click.hooks.ClickHook._user_home")733 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old")
651 def test_sync(self, mock_user_home):734 os.symlink(
652 mock_user_home.return_value = "/home/test-user"735 os.path.join(user_db.get_path("test-1"), "target-1"), path_1)
653 with mkfile(os.path.join(self.temp_dir, "hooks", "test.hook")) as f:736 with mkfile(os.path.join(
654 print("User-Level: yes", file=f)737 self.temp_dir, "test-2", "2.0", ".click", "info",
655 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)738 "test-2.manifest")) as f:
656 user_db = ClickUser(self.db, user="test-user")739 json.dump({"hooks": {"test2-app": {"old": "target-2"}}}, f)
657 with mkfile(os.path.join(740 user_db.set_version("test-2", "2.0")
658 self.temp_dir, "test-1", "1.0", ".click", "info",741 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old")
659 "test-1.manifest")) as f:742 os.symlink(
660 json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)743 os.path.join(user_db.get_path("test-2"), "target-2"), path_2)
661 user_db.set_version("test-1", "1.0")744 self._setup_hooks_dir(
662 with mkfile(os.path.join(745 preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
663 self.temp_dir, "test-2", "1.1", ".click", "info",746 hook = Click.Hook.open(self.db, "old")
664 "test-2.manifest")) as f:747 hook.remove()
665 json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)748 self.assertFalse(os.path.exists(path_1))
666 user_db.set_version("test-2", "1.1")749 self.assertFalse(os.path.exists(path_2))
667 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")750
668 os.symlink(751 def test_sync(self):
669 os.path.join(752 with self.run_in_subprocess(
670 self.temp_dir, ".click", "users", "test-user", "test-1",753 "click_get_hooks_dir", "click_get_user_home",
671 "target-1"),754 ) as (enter, preloads):
672 path_1)755 enter()
673 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")756 preloads["click_get_user_home"].return_value = "/home/test-user"
674 path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")757 self._setup_hooks_dir(preloads)
675 os.symlink(758 with mkfile(
676 os.path.join(759 os.path.join(self.temp_dir, "hooks", "test.hook")) as f:
677 self.temp_dir, ".click", "users", "test-user", "test-3",760 print("User-Level: yes", file=f)
678 "target-3"),761 print("Pattern: %s/${id}.test" % self.temp_dir, file=f)
679 path_3)762 user_db = Click.User.for_user(self.db, "test-user")
680 with temp_hooks_dir(os.path.join(self.temp_dir, "hooks")):763 with mkfile(os.path.join(
681 hook = ClickHook.open(self.db, "test")764 self.temp_dir, "test-1", "1.0", ".click", "info",
682 hook.sync(user="test-user")765 "test-1.manifest")) as f:
683 self.assertTrue(os.path.lexists(path_1))766 json.dump({"hooks": {"test1-app": {"test": "target-1"}}}, f)
684 self.assertEqual(767 user_db.set_version("test-1", "1.0")
685 os.path.join(768 with mkfile(os.path.join(
686 self.temp_dir, ".click", "users", "test-user", "test-1",769 self.temp_dir, "test-2", "1.1", ".click", "info",
687 "target-1"),770 "test-2.manifest")) as f:
688 os.readlink(path_1))771 json.dump({"hooks": {"test2-app": {"test": "target-2"}}}, f)
689 self.assertTrue(os.path.lexists(path_2))772 user_db.set_version("test-2", "1.1")
690 self.assertEqual(773 path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test")
691 os.path.join(774 os.symlink(
692 self.temp_dir, ".click", "users", "test-user", "test-2",775 os.path.join(
693 "target-2"),776 self.temp_dir, ".click", "users", "test-user", "test-1",
694 os.readlink(path_2))777 "target-1"),
695 self.assertFalse(os.path.lexists(path_3))778 path_1)
779 path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test")
780 path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test")
781 os.symlink(
782 os.path.join(
783 self.temp_dir, ".click", "users", "test-user", "test-3",
784 "target-3"),
785 path_3)
786 self._setup_hooks_dir(
787 preloads, hooks_dir=os.path.join(self.temp_dir, "hooks"))
788 hook = Click.Hook.open(self.db, "test")
789 hook.sync(user_name="test-user")
790 self.assertTrue(os.path.lexists(path_1))
791 self.assertEqual(
792 os.path.join(
793 self.temp_dir, ".click", "users", "test-user", "test-1",
794 "target-1"),
795 os.readlink(path_1))
796 self.assertTrue(os.path.lexists(path_2))
797 self.assertEqual(
798 os.path.join(
799 self.temp_dir, ".click", "users", "test-user", "test-2",
800 "target-2"),
801 os.readlink(path_2))
802 self.assertFalse(os.path.lexists(path_3))
696803
697804
698class TestPackageInstallHooks(TestClickHookBase):805class TestPackageInstallHooks(TestClickHookBase):
699 def test_removes_old_hooks(self):806 def test_removes_old_hooks(self):
700 hooks_dir = os.path.join(self.temp_dir, "hooks")807 with self.run_in_subprocess(
701 with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:808 "click_get_hooks_dir") as (enter, preloads):
702 print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)809 enter()
703 print("Single-Version: yes", file=f)810 hooks_dir = os.path.join(self.temp_dir, "hooks")
704 with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:811 self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
705 print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f)812 with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:
706 print("Single-Version: yes", file=f)813 print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)
707 print("Hook-Name: yelp", file=f)814 print("Single-Version: yes", file=f)
708 with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:815 with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:
709 print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f)816 print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir,
710 print("Single-Version: yes", file=f)817 file=f)
711 print("Hook-Name: yelp", file=f)818 print("Single-Version: yes", file=f)
712 os.mkdir(os.path.join(self.temp_dir, "unity"))819 print("Hook-Name: yelp", file=f)
713 unity_path = os.path.join(self.temp_dir, "unity", "test_app_1.0.scope")820 with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:
714 os.symlink("dummy", unity_path)821 print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir,
715 os.mkdir(os.path.join(self.temp_dir, "yelp"))822 file=f)
716 yelp_docs_path = os.path.join(823 print("Single-Version: yes", file=f)
717 self.temp_dir, "yelp", "docs-test_app_1.0.txt")824 print("Hook-Name: yelp", file=f)
718 os.symlink("dummy", yelp_docs_path)825 os.mkdir(os.path.join(self.temp_dir, "unity"))
719 yelp_other_path = os.path.join(826 unity_path = os.path.join(
720 self.temp_dir, "yelp", "other-test_app_1.0.txt")827 self.temp_dir, "unity", "test_app_1.0.scope")
721 os.symlink("dummy", yelp_other_path)828 os.symlink("dummy", unity_path)
722 package_dir = os.path.join(self.temp_dir, "test")829 os.mkdir(os.path.join(self.temp_dir, "yelp"))
723 with mkfile(os.path.join(830 yelp_docs_path = os.path.join(
724 package_dir, "1.0", ".click", "info", "test.manifest")) as f:831 self.temp_dir, "yelp", "docs-test_app_1.0.txt")
725 json.dump(832 os.symlink("dummy", yelp_docs_path)
726 {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}},833 yelp_other_path = os.path.join(
727 f)834 self.temp_dir, "yelp", "other-test_app_1.0.txt")
728 with mkfile(os.path.join(835 os.symlink("dummy", yelp_other_path)
729 package_dir, "1.1", ".click", "info", "test.manifest")) as f:836 package_dir = os.path.join(self.temp_dir, "test")
730 json.dump({}, f)837 with mkfile(os.path.join(
731 with temp_hooks_dir(hooks_dir):838 package_dir, "1.0", ".click", "info",
732 package_install_hooks(self.db, "test", "1.0", "1.1")839 "test.manifest")) as f:
733 self.assertFalse(os.path.lexists(unity_path))840 json.dump(
734 self.assertFalse(os.path.lexists(yelp_docs_path))841 {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}},
735 self.assertFalse(os.path.lexists(yelp_other_path))842 f)
843 with mkfile(os.path.join(
844 package_dir, "1.1", ".click", "info",
845 "test.manifest")) as f:
846 json.dump({}, f)
847 Click.package_install_hooks(self.db, "test", "1.0", "1.1")
848 self.assertFalse(os.path.lexists(unity_path))
849 self.assertFalse(os.path.lexists(yelp_docs_path))
850 self.assertFalse(os.path.lexists(yelp_other_path))
736851
737 def test_installs_new_hooks(self):852 def test_installs_new_hooks(self):
738 hooks_dir = os.path.join(self.temp_dir, "hooks")853 with self.run_in_subprocess(
739 with mkfile(os.path.join(hooks_dir, "a.hook")) as f:854 "click_get_hooks_dir") as (enter, preloads):
740 print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)855 enter()
741 with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:856 hooks_dir = os.path.join(self.temp_dir, "hooks")
742 print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)857 self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
743 print("Hook-Name: b", file=f)858 with mkfile(os.path.join(hooks_dir, "a.hook")) as f:
744 with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:859 print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)
745 print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)860 with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:
746 print("Hook-Name: b", file=f)861 print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)
747 os.mkdir(os.path.join(self.temp_dir, "a"))862 print("Hook-Name: b", file=f)
748 os.mkdir(os.path.join(self.temp_dir, "b"))863 with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:
749 package_dir = os.path.join(self.temp_dir, "test")864 print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)
750 with mkfile(os.path.join(865 print("Hook-Name: b", file=f)
751 package_dir, "1.0", ".click", "info", "test.manifest")) as f:866 os.mkdir(os.path.join(self.temp_dir, "a"))
752 json.dump({"hooks": {}}, f)867 os.mkdir(os.path.join(self.temp_dir, "b"))
753 with mkfile(os.path.join(868 package_dir = os.path.join(self.temp_dir, "test")
754 package_dir, "1.1", ".click", "info", "test.manifest")) as f:869 with mkfile(os.path.join(
755 json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)870 package_dir, "1.0", ".click", "info",
756 with temp_hooks_dir(hooks_dir):871 "test.manifest")) as f:
757 package_install_hooks(self.db, "test", "1.0", "1.1")872 json.dump({"hooks": {}}, f)
758 self.assertTrue(os.path.lexists(873 with mkfile(os.path.join(
759 os.path.join(self.temp_dir, "a", "test_app_1.1.a")))874 package_dir, "1.1", ".click", "info",
760 self.assertTrue(os.path.lexists(875 "test.manifest")) as f:
761 os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))876 json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)
762 self.assertTrue(os.path.lexists(877 Click.package_install_hooks(self.db, "test", "1.0", "1.1")
763 os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))878 self.assertTrue(os.path.lexists(
879 os.path.join(self.temp_dir, "a", "test_app_1.1.a")))
880 self.assertTrue(os.path.lexists(
881 os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))
882 self.assertTrue(os.path.lexists(
883 os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))
764884
765 def test_upgrades_existing_hooks(self):885 def test_upgrades_existing_hooks(self):
766 hooks_dir = os.path.join(self.temp_dir, "hooks")886 with self.run_in_subprocess(
767 with mkfile(os.path.join(hooks_dir, "a.hook")) as f:887 "click_get_hooks_dir") as (enter, preloads):
768 print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)888 enter()
769 print("Single-Version: yes", file=f)889 hooks_dir = os.path.join(self.temp_dir, "hooks")
770 with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:890 self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
771 print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)891 with mkfile(os.path.join(hooks_dir, "a.hook")) as f:
772 print("Single-Version: yes", file=f)892 print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f)
773 print("Hook-Name: b", file=f)893 print("Single-Version: yes", file=f)
774 with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:894 with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f:
775 print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)895 print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f)
776 print("Single-Version: yes", file=f)896 print("Single-Version: yes", file=f)
777 print("Hook-Name: b", file=f)897 print("Hook-Name: b", file=f)
778 with mkfile(os.path.join(hooks_dir, "c.hook")) as f:898 with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f:
779 print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f)899 print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f)
780 print("Single-Version: yes", file=f)900 print("Single-Version: yes", file=f)
781 os.mkdir(os.path.join(self.temp_dir, "a"))901 print("Hook-Name: b", file=f)
782 a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a")902 with mkfile(os.path.join(hooks_dir, "c.hook")) as f:
783 os.symlink("dummy", a_path)903 print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f)
784 os.mkdir(os.path.join(self.temp_dir, "b"))904 print("Single-Version: yes", file=f)
785 b_irrelevant_path = os.path.join(905 os.mkdir(os.path.join(self.temp_dir, "a"))
786 self.temp_dir, "b", "1-test_other-app_1.0.b")906 a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a")
787 os.symlink("dummy", b_irrelevant_path)907 os.symlink("dummy", a_path)
788 b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b")908 os.mkdir(os.path.join(self.temp_dir, "b"))
789 os.symlink("dummy", b_1_path)909 b_irrelevant_path = os.path.join(
790 b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b")910 self.temp_dir, "b", "1-test_other-app_1.0.b")
791 os.symlink("dummy", b_2_path)911 os.symlink("dummy", b_irrelevant_path)
792 os.mkdir(os.path.join(self.temp_dir, "c"))912 b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b")
793 package_dir = os.path.join(self.temp_dir, "test")913 os.symlink("dummy", b_1_path)
794 with mkfile(os.path.join(914 b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b")
795 package_dir, "1.0", ".click", "info", "test.manifest")) as f:915 os.symlink("dummy", b_2_path)
796 json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)916 os.mkdir(os.path.join(self.temp_dir, "c"))
797 with mkfile(os.path.join(917 package_dir = os.path.join(self.temp_dir, "test")
798 package_dir, "1.1", ".click", "info", "test.manifest")) as f:918 with mkfile(os.path.join(
799 json.dump(919 package_dir, "1.0", ".click", "info",
800 {"hooks": {920 "test.manifest")) as f:
801 "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}}921 json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f)
802 }, f)922 with mkfile(os.path.join(
803 with temp_hooks_dir(hooks_dir):923 package_dir, "1.1", ".click", "info",
804 package_install_hooks(self.db, "test", "1.0", "1.1")924 "test.manifest")) as f:
805 self.assertFalse(os.path.lexists(a_path))925 json.dump(
806 self.assertTrue(os.path.lexists(b_irrelevant_path))926 {"hooks": {
807 self.assertFalse(os.path.lexists(b_1_path))927 "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}}
808 self.assertFalse(os.path.lexists(b_2_path))928 }, f)
809 self.assertTrue(os.path.lexists(929 Click.package_install_hooks(self.db, "test", "1.0", "1.1")
810 os.path.join(self.temp_dir, "a", "test_app_1.1.a")))930 self.assertFalse(os.path.lexists(a_path))
811 self.assertTrue(os.path.lexists(931 self.assertTrue(os.path.lexists(b_irrelevant_path))
812 os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))932 self.assertFalse(os.path.lexists(b_1_path))
813 self.assertTrue(os.path.lexists(933 self.assertFalse(os.path.lexists(b_2_path))
814 os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))934 self.assertTrue(os.path.lexists(
815 self.assertTrue(os.path.lexists(935 os.path.join(self.temp_dir, "a", "test_app_1.1.a")))
816 os.path.join(self.temp_dir, "c", "test_app_1.1.c")))936 self.assertTrue(os.path.lexists(
937 os.path.join(self.temp_dir, "b", "1-test_app_1.1.b")))
938 self.assertTrue(os.path.lexists(
939 os.path.join(self.temp_dir, "b", "2-test_app_1.1.b")))
940 self.assertTrue(os.path.lexists(
941 os.path.join(self.temp_dir, "c", "test_app_1.1.c")))
817942
818943
819class TestPackageRemoveHooks(TestClickHookBase):944class TestPackageRemoveHooks(TestClickHookBase):
820 def test_removes_hooks(self):945 def test_removes_hooks(self):
821 hooks_dir = os.path.join(self.temp_dir, "hooks")946 with self.run_in_subprocess(
822 with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:947 "click_get_hooks_dir") as (enter, preloads):
823 print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)948 enter()
824 with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:949 hooks_dir = os.path.join(self.temp_dir, "hooks")
825 print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f)950 self._setup_hooks_dir(preloads, hooks_dir=hooks_dir)
826 print("Hook-Name: yelp", file=f)951 with mkfile(os.path.join(hooks_dir, "unity.hook")) as f:
827 with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:952 print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f)
828 print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f)953 with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f:
829 print("Hook-Name: yelp", file=f)954 print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir,
830 os.mkdir(os.path.join(self.temp_dir, "unity"))955 file=f)
831 unity_path = os.path.join(self.temp_dir, "unity", "test_app_1.0.scope")956 print("Hook-Name: yelp", file=f)
832 os.symlink("dummy", unity_path)957 with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f:
833 os.mkdir(os.path.join(self.temp_dir, "yelp"))958 print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir,
834 yelp_docs_path = os.path.join(959 file=f)
835 self.temp_dir, "yelp", "docs-test_app_1.0.txt")960 print("Hook-Name: yelp", file=f)
836 os.symlink("dummy", yelp_docs_path)961 os.mkdir(os.path.join(self.temp_dir, "unity"))
837 yelp_other_path = os.path.join(962 unity_path = os.path.join(
838 self.temp_dir, "yelp", "other-test_app_1.0.txt")963 self.temp_dir, "unity", "test_app_1.0.scope")
839 os.symlink("dummy", yelp_other_path)964 os.symlink("dummy", unity_path)
840 package_dir = os.path.join(self.temp_dir, "test")965 os.mkdir(os.path.join(self.temp_dir, "yelp"))
841 with mkfile(os.path.join(966 yelp_docs_path = os.path.join(
842 package_dir, "1.0", ".click", "info", "test.manifest")) as f:967 self.temp_dir, "yelp", "docs-test_app_1.0.txt")
843 json.dump(968 os.symlink("dummy", yelp_docs_path)
844 {"hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}},969 yelp_other_path = os.path.join(
845 f)970 self.temp_dir, "yelp", "other-test_app_1.0.txt")
846 with temp_hooks_dir(hooks_dir):971 os.symlink("dummy", yelp_other_path)
847 package_remove_hooks(self.db, "test", "1.0")972 package_dir = os.path.join(self.temp_dir, "test")
848 self.assertFalse(os.path.lexists(unity_path))973 with mkfile(os.path.join(
849 self.assertFalse(os.path.lexists(yelp_docs_path))974 package_dir, "1.0", ".click", "info",
850 self.assertFalse(os.path.lexists(yelp_other_path))975 "test.manifest")) as f:
976 json.dump(
977 {"hooks": {
978 "app": {"yelp": "foo.txt", "unity": "foo.scope"}}
979 }, f)
980 Click.package_remove_hooks(self.db, "test", "1.0")
981 self.assertFalse(os.path.lexists(unity_path))
982 self.assertFalse(os.path.lexists(yelp_docs_path))
983 self.assertFalse(os.path.lexists(yelp_other_path))
851984
=== modified file 'click/tests/test_install.py'
--- click/tests/test_install.py 2014-01-22 14:02:33 +0000
+++ click/tests/test_install.py 2014-03-06 07:04:44 +0000
@@ -34,10 +34,10 @@
34from unittest import skipUnless34from unittest import skipUnless
3535
36from debian.deb822 import Deb82236from debian.deb822 import Deb822
37from gi.repository import Click
3738
38from click import install, osextras39from click import install
39from click.build import ClickBuilder40from click.build import ClickBuilder
40from click.database import ClickDB
41from click.install import (41from click.install import (
42 ClickInstaller,42 ClickInstaller,
43 ClickInstallerAuditError,43 ClickInstallerAuditError,
@@ -68,7 +68,8 @@
68 def setUp(self):68 def setUp(self):
69 super(TestClickInstaller, self).setUp()69 super(TestClickInstaller, self).setUp()
70 self.use_temp_dir()70 self.use_temp_dir()
71 self.db = ClickDB(self.temp_dir)71 self.db = Click.DB()
72 self.db.add(self.temp_dir)
7273
73 def make_fake_package(self, control_fields=None, manifest=None,74 def make_fake_package(self, control_fields=None, manifest=None,
74 control_scripts=None, data_files=None):75 control_scripts=None, data_files=None):
@@ -90,7 +91,7 @@
90 for name, contents in control_scripts.items():91 for name, contents in control_scripts.items():
91 with mkfile(os.path.join(control_dir, name)) as script:92 with mkfile(os.path.join(control_dir, name)) as script:
92 script.write(contents)93 script.write(contents)
93 osextras.ensuredir(data_dir)94 Click.ensuredir(data_dir)
94 for name, path in data_files.items():95 for name, path in data_files.items():
95 if path is None:96 if path is None:
96 touch(os.path.join(data_dir, name))97 touch(os.path.join(data_dir, name))
@@ -108,11 +109,11 @@
108 old_dir = install.frameworks_dir109 old_dir = install.frameworks_dir
109 try:110 try:
110 install.frameworks_dir = os.path.join(self.temp_dir, "frameworks")111 install.frameworks_dir = os.path.join(self.temp_dir, "frameworks")
111 osextras.ensuredir(install.frameworks_dir)112 Click.ensuredir(install.frameworks_dir)
112 touch(os.path.join(install.frameworks_dir, "%s.framework" % name))113 touch(os.path.join(install.frameworks_dir, "%s.framework" % name))
113 yield114 yield
114 finally:115 finally:
115 osextras.unlink_force(116 Click.unlink_force(
116 os.path.join(install.frameworks_dir, "%s.framework" % name))117 os.path.join(install.frameworks_dir, "%s.framework" % name))
117 install.frameworks_dir = old_dir118 install.frameworks_dir = old_dir
118119
@@ -342,7 +343,7 @@
342 @skipUnless(343 @skipUnless(
343 os.path.exists(ClickInstaller(None)._preload_path()),344 os.path.exists(ClickInstaller(None)._preload_path()),
344 "preload bits not built; installing packages will fail")345 "preload bits not built; installing packages will fail")
345 @mock.patch("click.install.package_install_hooks")346 @mock.patch("gi.repository.Click.package_install_hooks")
346 def test_install(self, mock_package_install_hooks):347 def test_install(self, mock_package_install_hooks):
347 path = self.make_fake_package(348 path = self.make_fake_package(
348 control_fields={349 control_fields={
@@ -361,7 +362,8 @@
361 control_scripts={"preinst": static_preinst},362 control_scripts={"preinst": static_preinst},
362 data_files={"foo": None})363 data_files={"foo": None})
363 root = os.path.join(self.temp_dir, "root")364 root = os.path.join(self.temp_dir, "root")
364 db = ClickDB(root)365 db = Click.DB()
366 db.add(root)
365 installer = ClickInstaller(db)367 installer = ClickInstaller(db)
366 with self.make_framework("ubuntu-sdk-13.10"), \368 with self.make_framework("ubuntu-sdk-13.10"), \
367 mock_quiet_subprocess_call():369 mock_quiet_subprocess_call():
@@ -425,7 +427,9 @@
425 control_scripts={"preinst": static_preinst},427 control_scripts={"preinst": static_preinst},
426 data_files={"foo": None})428 data_files={"foo": None})
427 root = os.path.join(self.temp_dir, "root")429 root = os.path.join(self.temp_dir, "root")
428 installer = ClickInstaller(ClickDB(root))430 db = Click.DB()
431 db.add(root)
432 installer = ClickInstaller(db)
429 with self.make_framework("ubuntu-sdk-13.10"), \433 with self.make_framework("ubuntu-sdk-13.10"), \
430 mock.patch("subprocess.call") as mock_call:434 mock.patch("subprocess.call") as mock_call:
431 mock_call.side_effect = call_side_effect435 mock_call.side_effect = call_side_effect
@@ -437,8 +441,9 @@
437 @skipUnless(441 @skipUnless(
438 os.path.exists(ClickInstaller(None)._preload_path()),442 os.path.exists(ClickInstaller(None)._preload_path()),
439 "preload bits not built; installing packages will fail")443 "preload bits not built; installing packages will fail")
440 @mock.patch("click.install.package_install_hooks")444 @mock.patch("gi.repository.Click.package_install_hooks")
441 def test_upgrade(self, mock_package_install_hooks):445 def test_upgrade(self, mock_package_install_hooks):
446 os.environ["TEST_QUIET"] = "1"
442 path = self.make_fake_package(447 path = self.make_fake_package(
443 control_fields={448 control_fields={
444 "Package": "test-package",449 "Package": "test-package",
@@ -460,7 +465,8 @@
460 inst_dir = os.path.join(package_dir, "current")465 inst_dir = os.path.join(package_dir, "current")
461 os.makedirs(os.path.join(package_dir, "1.0"))466 os.makedirs(os.path.join(package_dir, "1.0"))
462 os.symlink("1.0", inst_dir)467 os.symlink("1.0", inst_dir)
463 db = ClickDB(root)468 db = Click.DB()
469 db.add(root)
464 installer = ClickInstaller(db)470 installer = ClickInstaller(db)
465 with self.make_framework("ubuntu-sdk-13.10"), \471 with self.make_framework("ubuntu-sdk-13.10"), \
466 mock_quiet_subprocess_call():472 mock_quiet_subprocess_call():
@@ -494,7 +500,7 @@
494 @skipUnless(500 @skipUnless(
495 os.path.exists(ClickInstaller(None)._preload_path()),501 os.path.exists(ClickInstaller(None)._preload_path()),
496 "preload bits not built; installing packages will fail")502 "preload bits not built; installing packages will fail")
497 @mock.patch("click.install.package_install_hooks")503 @mock.patch("gi.repository.Click.package_install_hooks")
498 def test_world_readable(self, mock_package_install_hooks):504 def test_world_readable(self, mock_package_install_hooks):
499 owner_only_file = os.path.join(self.temp_dir, "owner-only-file")505 owner_only_file = os.path.join(self.temp_dir, "owner-only-file")
500 touch(owner_only_file)506 touch(owner_only_file)
@@ -521,7 +527,8 @@
521 "world-readable-dir": owner_only_dir,527 "world-readable-dir": owner_only_dir,
522 })528 })
523 root = os.path.join(self.temp_dir, "root")529 root = os.path.join(self.temp_dir, "root")
524 db = ClickDB(root)530 db = Click.DB()
531 db.add(root)
525 installer = ClickInstaller(db)532 installer = ClickInstaller(db)
526 with self.make_framework("ubuntu-sdk-13.10"), \533 with self.make_framework("ubuntu-sdk-13.10"), \
527 mock_quiet_subprocess_call():534 mock_quiet_subprocess_call():
@@ -538,7 +545,7 @@
538 @skipUnless(545 @skipUnless(
539 os.path.exists(ClickInstaller(None)._preload_path()),546 os.path.exists(ClickInstaller(None)._preload_path()),
540 "preload bits not built; installing packages will fail")547 "preload bits not built; installing packages will fail")
541 @mock.patch("click.install.package_install_hooks")548 @mock.patch("gi.repository.Click.package_install_hooks")
542 @mock.patch("click.install.ClickInstaller._dpkg_architecture")549 @mock.patch("click.install.ClickInstaller._dpkg_architecture")
543 def test_single_architecture(self, mock_dpkg_architecture,550 def test_single_architecture(self, mock_dpkg_architecture,
544 mock_package_install_hooks):551 mock_package_install_hooks):
@@ -560,7 +567,8 @@
560 },567 },
561 control_scripts={"preinst": static_preinst})568 control_scripts={"preinst": static_preinst})
562 root = os.path.join(self.temp_dir, "root")569 root = os.path.join(self.temp_dir, "root")
563 db = ClickDB(root)570 db = Click.DB()
571 db.add(root)
564 installer = ClickInstaller(db)572 installer = ClickInstaller(db)
565 with self.make_framework("ubuntu-sdk-13.10"), \573 with self.make_framework("ubuntu-sdk-13.10"), \
566 mock_quiet_subprocess_call():574 mock_quiet_subprocess_call():
@@ -571,7 +579,7 @@
571 @skipUnless(579 @skipUnless(
572 os.path.exists(ClickInstaller(None)._preload_path()),580 os.path.exists(ClickInstaller(None)._preload_path()),
573 "preload bits not built; installing packages will fail")581 "preload bits not built; installing packages will fail")
574 @mock.patch("click.install.package_install_hooks")582 @mock.patch("gi.repository.Click.package_install_hooks")
575 @mock.patch("click.install.ClickInstaller._dpkg_architecture")583 @mock.patch("click.install.ClickInstaller._dpkg_architecture")
576 def test_multiple_architectures(self, mock_dpkg_architecture,584 def test_multiple_architectures(self, mock_dpkg_architecture,
577 mock_package_install_hooks):585 mock_package_install_hooks):
@@ -593,7 +601,8 @@
593 },601 },
594 control_scripts={"preinst": static_preinst})602 control_scripts={"preinst": static_preinst})
595 root = os.path.join(self.temp_dir, "root")603 root = os.path.join(self.temp_dir, "root")
596 db = ClickDB(root)604 db = Click.DB()
605 db.add(root)
597 installer = ClickInstaller(db)606 installer = ClickInstaller(db)
598 with self.make_framework("ubuntu-sdk-13.10"), \607 with self.make_framework("ubuntu-sdk-13.10"), \
599 mock_quiet_subprocess_call():608 mock_quiet_subprocess_call():
600609
=== modified file 'click/tests/test_osextras.py'
--- click/tests/test_osextras.py 2013-09-04 15:59:18 +0000
+++ click/tests/test_osextras.py 2014-03-06 07:04:44 +0000
@@ -17,35 +17,34 @@
1717
18from __future__ import print_function18from __future__ import print_function
19__all__ = [19__all__ = [
20 'TestOSExtras',20 'TestOSExtrasNative',
21 'TestOSExtrasPython',
21 ]22 ]
2223
2324
24import os25import os
2526
27from gi.repository import Click, GLib
28
26from click import osextras29from click import osextras
27from click.tests.helpers import TestCase, touch30from click.tests.helpers import TestCase, mock, touch
2831
2932
30class TestOSExtras(TestCase):33class TestOSExtrasBaseMixin:
31 def setUp(self):
32 super(TestOSExtras, self).setUp()
33 self.use_temp_dir()
34
35 def test_ensuredir_previously_missing(self):34 def test_ensuredir_previously_missing(self):
36 new_dir = os.path.join(self.temp_dir, "dir")35 new_dir = os.path.join(self.temp_dir, "dir")
37 osextras.ensuredir(new_dir)36 self.mod.ensuredir(new_dir)
38 self.assertTrue(os.path.isdir(new_dir))37 self.assertTrue(os.path.isdir(new_dir))
3938
40 def test_ensuredir_previously_present(self):39 def test_ensuredir_previously_present(self):
41 new_dir = os.path.join(self.temp_dir, "dir")40 new_dir = os.path.join(self.temp_dir, "dir")
42 os.mkdir(new_dir)41 os.mkdir(new_dir)
43 osextras.ensuredir(new_dir)42 self.mod.ensuredir(new_dir)
44 self.assertTrue(os.path.isdir(new_dir))43 self.assertTrue(os.path.isdir(new_dir))
4544
46 def test_find_on_path_missing_environment(self):45 def test_find_on_path_missing_environment(self):
47 os.environ.pop("PATH", None)46 os.environ.pop("PATH", None)
48 self.assertFalse(osextras.find_on_path("ls"))47 self.assertFalse(self.mod.find_on_path("ls"))
4948
50 def test_find_on_path_present_executable(self):49 def test_find_on_path_present_executable(self):
51 bin_dir = os.path.join(self.temp_dir, "bin")50 bin_dir = os.path.join(self.temp_dir, "bin")
@@ -53,69 +52,112 @@
53 touch(program)52 touch(program)
54 os.chmod(program, 0o755)53 os.chmod(program, 0o755)
55 os.environ["PATH"] = bin_dir54 os.environ["PATH"] = bin_dir
56 self.assertTrue(osextras.find_on_path("program"))55 self.assertTrue(self.mod.find_on_path("program"))
5756
58 def test_find_on_path_present_not_executable(self):57 def test_find_on_path_present_not_executable(self):
59 bin_dir = os.path.join(self.temp_dir, "bin")58 bin_dir = os.path.join(self.temp_dir, "bin")
60 touch(os.path.join(bin_dir, "program"))59 touch(os.path.join(bin_dir, "program"))
61 os.environ["PATH"] = bin_dir60 os.environ["PATH"] = bin_dir
62 self.assertFalse(osextras.find_on_path("program"))61 self.assertFalse(self.mod.find_on_path("program"))
6362
64 def test_listdir_directory_present(self):63 def test_find_on_path_requires_regular_file(self):
65 new_dir = os.path.join(self.temp_dir, "dir")64 bin_dir = os.path.join(self.temp_dir, "bin")
66 touch(os.path.join(new_dir, "file"))65 self.mod.ensuredir(os.path.join(bin_dir, "subdir"))
67 self.assertEqual(["file"], osextras.listdir_force(new_dir))66 os.environ["PATH"] = bin_dir
6867 self.assertFalse(self.mod.find_on_path("subdir"))
69 def test_listdir_directory_missing(self):
70 new_dir = os.path.join(self.temp_dir, "dir")
71 self.assertEqual([], osextras.listdir_force(new_dir))
72
73 def test_listdir_oserror(self):
74 not_dir = os.path.join(self.temp_dir, "file")
75 touch(not_dir)
76 self.assertRaises(OSError, osextras.listdir_force, not_dir)
7768
78 def test_unlink_file_present(self):69 def test_unlink_file_present(self):
79 path = os.path.join(self.temp_dir, "file")70 path = os.path.join(self.temp_dir, "file")
80 touch(path)71 touch(path)
81 osextras.unlink_force(path)72 self.mod.unlink_force(path)
82 self.assertFalse(os.path.exists(path))73 self.assertFalse(os.path.exists(path))
8374
84 def test_unlink_file_missing(self):75 def test_unlink_file_missing(self):
85 path = os.path.join(self.temp_dir, "file")76 path = os.path.join(self.temp_dir, "file")
86 osextras.unlink_force(path)77 self.mod.unlink_force(path)
87 self.assertFalse(os.path.exists(path))78 self.assertFalse(os.path.exists(path))
8879
89 def test_unlink_oserror(self):
90 path = os.path.join(self.temp_dir, "dir")
91 os.mkdir(path)
92 self.assertRaises(OSError, osextras.unlink_force, path)
93
94 def test_symlink_file_present(self):80 def test_symlink_file_present(self):
95 path = os.path.join(self.temp_dir, "link")81 path = os.path.join(self.temp_dir, "link")
96 touch(path)82 touch(path)
97 osextras.symlink_force("source", path)83 self.mod.symlink_force("source", path)
98 self.assertTrue(os.path.islink(path))84 self.assertTrue(os.path.islink(path))
99 self.assertEqual("source", os.readlink(path))85 self.assertEqual("source", os.readlink(path))
10086
101 def test_symlink_link_present(self):87 def test_symlink_link_present(self):
102 path = os.path.join(self.temp_dir, "link")88 path = os.path.join(self.temp_dir, "link")
103 os.symlink("old", path)89 os.symlink("old", path)
104 osextras.symlink_force("source", path)90 self.mod.symlink_force("source", path)
105 self.assertTrue(os.path.islink(path))91 self.assertTrue(os.path.islink(path))
106 self.assertEqual("source", os.readlink(path))92 self.assertEqual("source", os.readlink(path))
10793
108 def test_symlink_missing(self):94 def test_symlink_missing(self):
109 path = os.path.join(self.temp_dir, "link")95 path = os.path.join(self.temp_dir, "link")
110 osextras.symlink_force("source", path)96 self.mod.symlink_force("source", path)
111 self.assertTrue(os.path.islink(path))97 self.assertTrue(os.path.islink(path))
112 self.assertEqual("source", os.readlink(path))98 self.assertEqual("source", os.readlink(path))
11399
114 def test_umask(self):100 def test_umask(self):
115 old_mask = os.umask(0o040)101 old_mask = os.umask(0o040)
116 try:102 try:
117 self.assertEqual(0o040, osextras.get_umask())103 self.assertEqual(0o040, self.mod.get_umask())
118 os.umask(0o002)104 os.umask(0o002)
119 self.assertEqual(0o002, osextras.get_umask())105 self.assertEqual(0o002, self.mod.get_umask())
120 finally:106 finally:
121 os.umask(old_mask)107 os.umask(old_mask)
108
109
110class TestOSExtrasNative(TestCase, TestOSExtrasBaseMixin):
111 def setUp(self):
112 super(TestOSExtrasNative, self).setUp()
113 self.use_temp_dir()
114 self.mod = Click
115
116 def test_dir_read_name_directory_present(self):
117 new_dir = os.path.join(self.temp_dir, "dir")
118 touch(os.path.join(new_dir, "file"))
119 d = Click.Dir.open(new_dir, 0)
120 self.assertEqual("file", d.read_name())
121 self.assertIsNone(d.read_name())
122
123 def test_dir_read_name_directory_missing(self):
124 new_dir = os.path.join(self.temp_dir, "dir")
125 d = Click.Dir.open(new_dir, 0)
126 self.assertIsNone(d.read_name())
127
128 def test_dir_open_error(self):
129 not_dir = os.path.join(self.temp_dir, "file")
130 touch(not_dir)
131 self.assertRaisesFileError(
132 GLib.FileError.NOTDIR, Click.Dir.open, not_dir, 0)
133
134 def test_unlink_error(self):
135 path = os.path.join(self.temp_dir, "dir")
136 os.mkdir(path)
137 self.assertRaisesFileError(mock.ANY, self.mod.unlink_force, path)
138
139
140class TestOSExtrasPython(TestCase, TestOSExtrasBaseMixin):
141 def setUp(self):
142 super(TestOSExtrasPython, self).setUp()
143 self.use_temp_dir()
144 self.mod = osextras
145
146 def test_listdir_directory_present(self):
147 new_dir = os.path.join(self.temp_dir, "dir")
148 touch(os.path.join(new_dir, "file"))
149 self.assertEqual(["file"], osextras.listdir_force(new_dir))
150
151 def test_listdir_directory_missing(self):
152 new_dir = os.path.join(self.temp_dir, "dir")
153 self.assertEqual([], osextras.listdir_force(new_dir))
154
155 def test_listdir_oserror(self):
156 not_dir = os.path.join(self.temp_dir, "file")
157 touch(not_dir)
158 self.assertRaises(OSError, osextras.listdir_force, not_dir)
159
160 def test_unlink_oserror(self):
161 path = os.path.join(self.temp_dir, "dir")
162 os.mkdir(path)
163 self.assertRaises(OSError, self.mod.unlink_force, path)
122164
=== added file 'click/tests/test_query.py'
--- click/tests/test_query.py 1970-01-01 00:00:00 +0000
+++ click/tests/test_query.py 2014-03-06 07:04:44 +0000
@@ -0,0 +1,52 @@
1# Copyright (C) 2014 Canonical Ltd.
2# Author: Colin Watson <cjwatson@ubuntu.com>
3
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""Unit tests for click.query."""
17
18from __future__ import print_function
19__all__ = [
20 'TestQuery',
21 ]
22
23
24import os
25
26from gi.repository import Click
27
28from click.tests.helpers import TestCase, touch
29
30
31class TestQuery(TestCase):
32 def setUp(self):
33 super(TestQuery, self).setUp()
34 self.use_temp_dir()
35
36 def test_find_package_directory_missing(self):
37 path = os.path.join(self.temp_dir, "nonexistent")
38 self.assertRaisesQueryError(
39 Click.QueryError.PATH, Click.find_package_directory, path)
40
41 def test_find_package_directory(self):
42 info = os.path.join(self.temp_dir, ".click", "info")
43 path = os.path.join(self.temp_dir, "file")
44 Click.ensuredir(info)
45 touch(path)
46 pkgdir = Click.find_package_directory(path)
47 self.assertEqual(self.temp_dir, pkgdir)
48
49 def test_find_package_directory_outside(self):
50 self.assertRaisesQueryError(
51 Click.QueryError.NO_PACKAGE_DIR, Click.find_package_directory,
52 "/bin")
053
=== modified file 'click/tests/test_user.py'
--- click/tests/test_user.py 2014-03-01 23:54:08 +0000
+++ click/tests/test_user.py 2014-03-06 07:04:44 +0000
@@ -25,24 +25,26 @@
2525
26import os26import os
2727
28from click.database import ClickDB28from gi.repository import Click
29
29from click.tests.helpers import TestCase30from click.tests.helpers import TestCase
30from click.user import ClickUser
3131
3232
33class TestClickUser(TestCase):33class TestClickUser(TestCase):
34 def setUp(self):34 def setUp(self):
35 super(TestClickUser, self).setUp()35 super(TestClickUser, self).setUp()
36 self.use_temp_dir()36 self.use_temp_dir()
37 self.db = ClickDB(self.temp_dir, use_system=False)37 self.db = Click.DB()
38 self.db.add(self.temp_dir)
3839
39 def _setUpMultiDB(self):40 def _setUpMultiDB(self):
40 self.multi_db = ClickDB(use_system=False)41 self.multi_db = Click.DB()
41 self.multi_db.add(os.path.join(self.temp_dir, "custom"))42 self.multi_db.add(os.path.join(self.temp_dir, "custom"))
42 self.multi_db.add(os.path.join(self.temp_dir, "click"))43 self.multi_db.add(os.path.join(self.temp_dir, "click"))
43 user_dbs = [44 user_dbs = [
44 os.path.join(d.root, ".click", "users", "user")45 os.path.join(
45 for d in self.multi_db46 self.multi_db.get(i).props.root, ".click", "users", "user")
47 for i in range(self.multi_db.props.size)
46 ]48 ]
47 a_1_0 = os.path.join(self.temp_dir, "custom", "a", "1.0")49 a_1_0 = os.path.join(self.temp_dir, "custom", "a", "1.0")
48 os.makedirs(a_1_0)50 os.makedirs(a_1_0)
@@ -58,80 +60,90 @@
58 os.makedirs(user_dbs[1])60 os.makedirs(user_dbs[1])
59 os.symlink(a_1_1, os.path.join(user_dbs[1], "a"))61 os.symlink(a_1_1, os.path.join(user_dbs[1], "a"))
60 os.symlink(c_0_1, os.path.join(user_dbs[1], "c"))62 os.symlink(c_0_1, os.path.join(user_dbs[1], "c"))
61 return user_dbs, ClickUser(self.multi_db, "user")63 return user_dbs, Click.User.for_user(self.multi_db, "user")
6264
63 def test_overlay_db(self):65 def test_new_no_db(self):
66 with self.run_in_subprocess(
67 "click_get_db_dir", "g_get_user_name") as (enter, preloads):
68 enter()
69 preloads["click_get_db_dir"].side_effect = (
70 lambda: self.make_string(self.temp_dir))
71 preloads["g_get_user_name"].side_effect = (
72 lambda: self.make_string("test-user"))
73 db_root = os.path.join(self.temp_dir, "db")
74 os.makedirs(db_root)
75 with open(os.path.join(self.temp_dir, "db.conf"), "w") as f:
76 print("[Click Database]", file=f)
77 print("root = %s" % db_root, file=f)
78 registry = Click.User.for_user()
79 self.assertEqual(
80 os.path.join(db_root, ".click", "users", "test-user"),
81 registry.get_overlay_db())
82
83 def test_get_overlay_db(self):
64 self.assertEqual(84 self.assertEqual(
65 os.path.join(self.temp_dir, ".click", "users", "user"),85 os.path.join(self.temp_dir, ".click", "users", "user"),
66 ClickUser(self.db, "user").overlay_db)86 Click.User.for_user(self.db, "user").get_overlay_db())
6787
68 def test_iter_missing(self):88 def test_get_package_names_missing(self):
69 db = ClickDB(89 db = Click.DB()
70 os.path.join(self.temp_dir, "nonexistent"), use_system=False)90 db.add(os.path.join(self.temp_dir, "nonexistent"))
71 registry = ClickUser(db)91 registry = Click.User.for_user(db)
72 self.assertEqual([], list(registry))92 self.assertEqual([], list(registry.get_package_names()))
7393
74 def test_iter(self):94 def test_get_package_names(self):
75 registry = ClickUser(self.db, "user")95 registry = Click.User.for_user(self.db, "user")
76 os.makedirs(registry.overlay_db)96 os.makedirs(registry.get_overlay_db())
77 os.symlink("/1.0", os.path.join(registry.overlay_db, "a"))97 os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a"))
78 os.symlink("/1.1", os.path.join(registry.overlay_db, "b"))98 os.symlink("/1.1", os.path.join(registry.get_overlay_db(), "b"))
79 self.assertCountEqual(["a", "b"], list(registry))99 self.assertCountEqual(["a", "b"], list(registry.get_package_names()))
80100
81 def test_iter_multiple_root(self):101 def test_get_package_names_multiple_root(self):
82 _, registry = self._setUpMultiDB()102 _, registry = self._setUpMultiDB()
83 self.assertCountEqual(["a", "b", "c"], list(registry))103 self.assertCountEqual(
84104 ["a", "b", "c"], list(registry.get_package_names()))
85 def test_len_missing(self):105
86 db = ClickDB(106 def test_get_version_missing(self):
87 os.path.join(self.temp_dir, "nonexistent"), use_system=False)107 registry = Click.User.for_user(self.db, "user")
88 registry = ClickUser(db)108 self.assertRaisesUserError(
89 self.assertEqual(0, len(registry))109 Click.UserError.NO_SUCH_PACKAGE, registry.get_version, "a")
90110 self.assertFalse(registry.has_package_name("a"))
91 def test_len(self):111
92 registry = ClickUser(self.db, "user")112 def test_get_version(self):
93 os.makedirs(registry.overlay_db)113 registry = Click.User.for_user(self.db, "user")
94 os.symlink("/1.0", os.path.join(registry.overlay_db, "a"))114 os.makedirs(registry.get_overlay_db())
95 os.symlink("/1.1", os.path.join(registry.overlay_db, "b"))115 os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a"))
96 self.assertEqual(2, len(registry))116 self.assertEqual("1.0", registry.get_version("a"))
97117 self.assertTrue(registry.has_package_name("a"))
98 def test_len_multiple_root(self):118
99 _, registry = self._setUpMultiDB()119 def test_get_version_multiple_root(self):
100 self.assertEqual(3, len(registry))120 _, registry = self._setUpMultiDB()
101121 self.assertEqual("1.1", registry.get_version("a"))
102 def test_getitem_missing(self):122 self.assertEqual("2.0", registry.get_version("b"))
103 registry = ClickUser(self.db, "user")123 self.assertEqual("0.1", registry.get_version("c"))
104 self.assertRaises(KeyError, registry.__getitem__, "a")124 self.assertTrue(registry.has_package_name("a"))
105125 self.assertTrue(registry.has_package_name("b"))
106 def test_getitem(self):126 self.assertTrue(registry.has_package_name("c"))
107 registry = ClickUser(self.db, "user")
108 os.makedirs(registry.overlay_db)
109 os.symlink("/1.0", os.path.join(registry.overlay_db, "a"))
110 self.assertEqual("1.0", registry["a"])
111
112 def test_getitem_multiple_root(self):
113 _, registry = self._setUpMultiDB()
114 self.assertEqual("1.1", registry["a"])
115 self.assertEqual("2.0", registry["b"])
116 self.assertEqual("0.1", registry["c"])
117127
118 def test_set_version_missing_target(self):128 def test_set_version_missing_target(self):
119 registry = ClickUser(self.db, "user")129 registry = Click.User.for_user(self.db, "user")
120 self.assertRaises(KeyError, registry.set_version, "a", "1.0")130 self.assertRaisesDatabaseError(
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: