Merge lp://qastaging/~cmiller/desktopcouch/add-running-context-as-parameter into lp://qastaging/desktopcouch

Proposed by Chad Miller
Status: Merged
Approved by: Eric Casteleijn
Approved revision: 106
Merged at revision: not available
Proposed branch: lp://qastaging/~cmiller/desktopcouch/add-running-context-as-parameter
Merge into: lp://qastaging/desktopcouch
Diff against target: 1259 lines (+337/-228)
18 files modified
desktopcouch/__init__.py (+3/-3)
desktopcouch/contacts/contactspicker.py (+3/-2)
desktopcouch/contacts/testing/create.py (+4/-3)
desktopcouch/contacts/tests/test_contactspicker.py (+4/-4)
desktopcouch/local_files.py (+92/-60)
desktopcouch/pair/couchdb_pairing/couchdb_io.py (+37/-26)
desktopcouch/pair/tests/test_couchdb_io.py (+27/-19)
desktopcouch/records/couchgrid.py (+6/-4)
desktopcouch/records/doc/records.txt (+1/-1)
desktopcouch/records/server.py (+9/-6)
desktopcouch/records/tests/test_couchgrid.py (+19/-14)
desktopcouch/records/tests/test_record.py (+5/-1)
desktopcouch/records/tests/test_server.py (+3/-3)
desktopcouch/start_local_couchdb.py (+41/-25)
desktopcouch/tests/__init__.py (+42/-41)
desktopcouch/tests/test_local_files.py (+9/-7)
desktopcouch/tests/test_replication.py (+21/-0)
desktopcouch/tests/test_start_local_couchdb.py (+11/-9)
To merge this branch: bzr merge lp://qastaging/~cmiller/desktopcouch/add-running-context-as-parameter
Reviewer Review Type Date Requested Status
Eric Casteleijn (community) Approve
Nicola Larosa (community) Abstain
Guillermo Gonzalez Approve
Review via email: mp+14824@code.qastaging.launchpad.net

Commit message

Use an explicit test context for testing. Add an implicit context for normal usage.

Add test context to pairing test functions.

Add a context to the update_design_documents() function. Make it peek at the context data dir location explicitly.

To post a comment you must log in.
Revision history for this message
Guillermo Gonzalez (verterok) wrote :

looks good, all tests [OK]

review: Approve
Revision history for this message
Nicola Larosa (teknico) wrote :

I cannot find a way to test this, please document it somewhere.

First I run "setup.py build".

Then I run "setup.py check", pylint cannot find a config file, so it uses defaults, and spews out a *huge* amount of notices.

Last I run "setup.py test", it says "running test" but does not seem to do anything.

review: Abstain
Revision history for this message
Nicola Larosa (teknico) wrote :

Ok, it's "trial desktopcouch", there was some fog this morning. Tests pass, but I'm not looking at the code, I'll let Eric do that.

review: Abstain
Revision history for this message
Eric Casteleijn (thisfred) wrote :

After updating trunk and merging this in I get a fairly trivial conflict in desktopcouch/__init__.py.

Also in that file a default argument is used:

local_files.DEFAULT_CONTEXT

but local_files is never imported. Is this a kind of magic that I forgot about, or are no errors triggered since the tests always pass an argument? In that case, we should add a test that does not pass a kw argument (but does not manipulate anything in the context, since that was what we set out to prevent)

Looks good, but when running;

PYTHONPATH=. trial desktopcouch

I get asked for access to the keyring 4 times, which I'm pretty sure is wrong, because the tests should not be using the tokens from there, unless I'm mistaken. (denying access makes the tests still pass)

Tests all pass.

review: Needs Information
106. By Chad Miller

Merge from trunk.

Revision history for this message
Chad Miller (cmiller) wrote :

> Also in __init__.py a default argument is used:
>
> local_files.DEFAULT_CONTEXT
>
> but local_files is never imported. Is this a kind of magic that I forgot
> about, or are no errors triggered since the tests always pass an argument? In
> that case, we should add a test that does not pass a kw argument (but does not
> manipulate anything in the context, since that was what we set out to prevent)

It's the __init__ for this module. Other parts of this module are available. We don't need to import ourselves, e.g., and access desktopcouch.local_files .

Try it. In __init__.py, add "print local_files" and start a python shell and "import desktopcouch" .

Revision history for this message
Eric Casteleijn (thisfred) wrote :

> Try it. In __init__.py, add "print local_files" and start a python shell and
> "import desktopcouch" .

Doh! I usually avoid using these kinds of imports and it has softened my brain into not recognizing them when I see them. NM

Revision history for this message
Eric Casteleijn (thisfred) wrote :

All issues resolved!

Revision history for this message
Eric Casteleijn (thisfred) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'desktopcouch/__init__.py'
--- desktopcouch/__init__.py 2009-10-29 21:45:19 +0000
+++ desktopcouch/__init__.py 2009-11-18 18:51:14 +0000
@@ -40,14 +40,14 @@
40logging.getLogger('').setLevel(logging.DEBUG)40logging.getLogger('').setLevel(logging.DEBUG)
41log = logging.getLogger('')41log = logging.getLogger('')
4242
43def find_pid(start_if_not_running=True):43def find_pid(start_if_not_running=True, ctx=local_files.DEFAULT_CONTEXT):
44 # Work out whether CouchDB is running by looking at its pid file44 # Work out whether CouchDB is running by looking at its pid file
45 pid = read_pidfile()45 pid = read_pidfile(ctx=ctx)
46 if not process_is_couchdb(pid) and start_if_not_running:46 if not process_is_couchdb(pid) and start_if_not_running:
47 # start CouchDB by running the startup script47 # start CouchDB by running the startup script
48 log.info("Desktop CouchDB is not running; starting it.")48 log.info("Desktop CouchDB is not running; starting it.")
49 from desktopcouch import start_local_couchdb49 from desktopcouch import start_local_couchdb
50 pid = start_local_couchdb.start_couchdb()50 pid = start_local_couchdb.start_couchdb(ctx=ctx)
51 # now load the design documents, because it's started51 # now load the design documents, because it's started
52 start_local_couchdb.update_design_documents()52 start_local_couchdb.update_design_documents()
5353
5454
=== modified file 'desktopcouch/contacts/contactspicker.py'
--- desktopcouch/contacts/contactspicker.py 2009-08-24 20:35:25 +0000
+++ desktopcouch/contacts/contactspicker.py 2009-11-18 18:51:14 +0000
@@ -19,6 +19,7 @@
19"""A widget to allow users to pick contacts"""19"""A widget to allow users to pick contacts"""
2020
21import gtk21import gtk
22import desktopcouch
22from desktopcouch.contacts.record import CONTACT_RECORD_TYPE23from desktopcouch.contacts.record import CONTACT_RECORD_TYPE
23from desktopcouch.records.couchgrid import CouchGrid24from desktopcouch.records.couchgrid import CouchGrid
2425
@@ -26,7 +27,7 @@
26class ContactsPicker(gtk.VBox):27class ContactsPicker(gtk.VBox):
27 """A contacts picker"""28 """A contacts picker"""
2829
29 def __init__(self, uri=None):30 def __init__(self, uri=None, ctx=desktopcouch.local_files.DEFAULT_CONTEXT):
30 """Create a new ContactsPicker widget."""31 """Create a new ContactsPicker widget."""
3132
32 gtk.VBox.__init__(self)33 gtk.VBox.__init__(self)
@@ -43,7 +44,7 @@
43 hbox.pack_start(self.search_button, False, False, 3)44 hbox.pack_start(self.search_button, False, False, 3)
4445
45 # Create CouchGrid to contain list of contacts46 # Create CouchGrid to contain list of contacts
46 self.contacts_list = CouchGrid('contacts', uri=uri)47 self.contacts_list = CouchGrid('contacts', uri=uri, ctx=ctx)
47 self.contacts_list.editable = False48 self.contacts_list.editable = False
48 self.contacts_list.keys = [ "first_name", "last_name" ]49 self.contacts_list.keys = [ "first_name", "last_name" ]
49 self.contacts_list.record_type = CONTACT_RECORD_TYPE50 self.contacts_list.record_type = CONTACT_RECORD_TYPE
5051
=== modified file 'desktopcouch/contacts/testing/create.py'
--- desktopcouch/contacts/testing/create.py 2009-09-14 23:42:59 +0000
+++ desktopcouch/contacts/testing/create.py 2009-11-18 18:51:14 +0000
@@ -21,6 +21,7 @@
2121
22import random, string, uuid22import random, string, uuid
2323
24import desktopcouch.tests as test_environment
24from desktopcouch.records.server import OAuthCapableServer25from desktopcouch.records.server import OAuthCapableServer
25import desktopcouch26import desktopcouch
2627
@@ -163,10 +164,10 @@
163 app_annots=None, server_class=OAuthCapableServer):164 app_annots=None, server_class=OAuthCapableServer):
164 """Make many contacts and create their records"""165 """Make many contacts and create their records"""
165 if port is None:166 if port is None:
166 desktopcouch.find_pid()167 pid = desktopcouch.find_pid(ctx=test_environment.test_context)
167 port = desktopcouch.find_port()168 port = desktopcouch.find_port(pid)
168 server_url = 'http://%s:%s/' % (host, port)169 server_url = 'http://%s:%s/' % (host, port)
169 server = server_class(server_url)170 server = server_class(server_url, ctx=test_environment.test_context)
170 db = server[db_name] if db_name in server else server.create(db_name)171 db = server[db_name] if db_name in server else server.create(db_name)
171 record_ids = []172 record_ids = []
172 for maincount in range(1, num_contacts + 1):173 for maincount in range(1, num_contacts + 1):
173174
=== modified file 'desktopcouch/contacts/tests/test_contactspicker.py'
--- desktopcouch/contacts/tests/test_contactspicker.py 2009-09-01 22:29:01 +0000
+++ desktopcouch/contacts/tests/test_contactspicker.py 2009-11-18 18:51:14 +0000
@@ -21,7 +21,7 @@
21import testtools21import testtools
22import gtk22import gtk
2323
24from desktopcouch.tests import xdg_cache24import desktopcouch.tests as test_environment
2525
26from desktopcouch.contacts.contactspicker import ContactsPicker26from desktopcouch.contacts.contactspicker import ContactsPicker
27from desktopcouch.records.server import CouchDatabase27from desktopcouch.records.server import CouchDatabase
@@ -32,9 +32,9 @@
32 def setUp(self):32 def setUp(self):
33 """setup each test"""33 """setup each test"""
34 # Connect to CouchDB server34 # Connect to CouchDB server
35 self.assert_(xdg_cache)
36 self.dbname = 'contacts'35 self.dbname = 'contacts'
37 self.database = CouchDatabase(self.dbname, create=True)36 self.database = CouchDatabase(self.dbname, create=True,
37 ctx=test_environment.test_context)
3838
39 def tearDown(self):39 def tearDown(self):
40 """tear down each test"""40 """tear down each test"""
@@ -49,6 +49,6 @@
49 win.resize(300, 450)49 win.resize(300, 450)
5050
51 # Create the contacts picker widget51 # Create the contacts picker widget
52 picker = ContactsPicker()52 picker = ContactsPicker(ctx=test_environment.test_context)
53 win.get_content_area().pack_start(picker, True, True, 3)53 win.get_content_area().pack_start(picker, True, True, 3)
54 self.assert_(picker.get_contacts_list())54 self.assert_(picker.get_contacts_list())
5555
=== modified file 'desktopcouch/local_files.py'
--- desktopcouch/local_files.py 2009-09-30 19:54:16 +0000
+++ desktopcouch/local_files.py 2009-11-18 18:51:14 +0000
@@ -24,7 +24,7 @@
24"""24"""
25from __future__ import with_statement25from __future__ import with_statement
26import os26import os
27import xdg.BaseDirectory27import xdg.BaseDirectory as xdg_base_dirs
28import subprocess28import subprocess
29import logging29import logging
30try:30try:
@@ -32,23 +32,6 @@
32except ImportError:32except ImportError:
33 import configparser33 import configparser
3434
35def mkpath(rootdir, path):
36 "Remove .. from paths"
37 return os.path.realpath(os.path.join(rootdir, path))
38
39rootdir = os.path.join(xdg.BaseDirectory.xdg_cache_home, "desktop-couch")
40if not os.path.isdir(rootdir):
41 os.mkdir(rootdir)
42
43config_dir = xdg.BaseDirectory.save_config_path("desktop-couch")
44
45FILE_INI = os.path.join(config_dir, "desktop-couchdb.ini")
46DIR_DB = xdg.BaseDirectory.save_data_path("desktop-couch")
47
48FILE_PID = mkpath(rootdir, "desktop-couchdb.pid")
49FILE_LOG = mkpath(rootdir, "desktop-couchdb.log")
50FILE_STDOUT = mkpath(rootdir, "desktop-couchdb.stdout")
51FILE_STDERR = mkpath(rootdir, "desktop-couchdb.stderr")
5235
53COUCH_EXE = os.environ.get('COUCHDB')36COUCH_EXE = os.environ.get('COUCHDB')
54if not COUCH_EXE:37if not COUCH_EXE:
@@ -58,30 +41,85 @@
58if not COUCH_EXE:41if not COUCH_EXE:
59 raise ImportError("Could not find couchdb")42 raise ImportError("Could not find couchdb")
6043
61def couch_chain_ini_files():44
62 process = subprocess.Popen([COUCH_EXE, '-V'], shell=False,45class Context():
63 stdout=subprocess.PIPE)46 """A mimic of xdg BaseDirectory, with overridable values that do not
64 line = process.stdout.read().split('\n')[0]47 depend on environment variables."""
65 couchversion = line.split()[-1]48
6649 def __init__(self, run_dir, db_dir, config_dir): # (cache, data, config)
67 # Explicitly add default ini file50
68 ini_files = ["/etc/couchdb/default.ini"]51 self.couchdb_log_level = 'info'
6952
70 # find all ini files in the desktopcouch XDG_CONFIG_DIRS and add them to53 for d in (run_dir, db_dir, config_dir):
71 # the chain54 if not os.path.isdir(d):
72 xdg_config_dirs = xdg.BaseDirectory.load_config_paths("desktop-couch")55 os.makedirs(d, 0700)
73 # Reverse the list because it's in most-important-first order56 else:
74 for folder in reversed(list(xdg_config_dirs)):57 os.chmod(d, 0700)
75 ini_files.extend([os.path.join(folder, x)58
76 for x in sorted(os.listdir(folder))59 self.run_dir = os.path.realpath(run_dir)
77 if x.endswith(".ini")])60 self.config_dir = os.path.realpath(config_dir)
7861 self.db_dir = os.path.realpath(db_dir)
79 if FILE_INI not in ini_files:62
80 ini_files.append(FILE_INI)63 self.file_ini = os.path.join(config_dir, "desktop-couchdb.ini")
8164 self.file_pid = os.path.join(run_dir, "desktop-couchdb.pid")
82 chain = "-n -a %s " % " -a ".join(ini_files)65 self.file_log = os.path.join(run_dir, "desktop-couchdb.log")
8366 self.file_stdout = os.path.join(run_dir, "desktop-couchdb.stdout")
84 return chain67 self.file_stderr = os.path.join(run_dir, "desktop-couchdb.stderr")
68
69 # You will need to add -b or -k on the end of this
70 self.couch_exec_command = [COUCH_EXE, self.couch_chain_ini_files(),
71 '-p', self.file_pid,
72 '-o', self.file_stdout,
73 '-e', self.file_stderr]
74
75
76 def ensure_files_not_readable(self):
77 for descr in ("ini", "pid", "log", "stdout", "stderr",):
78 f = getattr(self, "file_" + descr)
79 if os.path.isfile(f):
80 os.chmod(f, 0600)
81
82 def load_config_paths(self):
83 """This is xdg/BaseDirectory.py load_config_paths() with hard-code to
84 use desktop-couch resource and read from this context."""
85 yield self.config_dir
86 for config_dir in \
87 os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':'):
88 path = os.path.join(config_dir, "desktop-couch")
89 if os.path.exists(path):
90 yield path
91
92 def couch_chain_ini_files(self):
93 process = subprocess.Popen([COUCH_EXE, '-V'], shell=False,
94 stdout=subprocess.PIPE)
95 line = process.stdout.read().split('\n')[0]
96 couchversion = line.split()[-1]
97
98 # Explicitly add default ini file
99 ini_files = ["/etc/couchdb/default.ini"]
100
101 # find all ini files in the desktopcouch XDG_CONFIG_DIRS and add them to
102 # the chain
103 config_dirs = self.load_config_paths()
104 # Reverse the list because it's in most-important-first order
105 for folder in reversed(list(config_dirs)):
106 ini_files.extend([os.path.join(folder, x)
107 for x in sorted(os.listdir(folder))
108 if x.endswith(".ini")])
109
110 if self.file_ini not in ini_files:
111 ini_files.append(self.file_ini)
112
113 chain = "-n -a %s " % " -a ".join(ini_files)
114
115 return chain
116
117
118DEFAULT_CONTEXT = Context(
119 os.path.join(xdg_base_dirs.xdg_cache_home, "desktop-couch"),
120 xdg_base_dirs.save_data_path("desktop-couch"),
121 xdg_base_dirs.save_config_path("desktop-couch"))
122
85123
86class NoOAuthTokenException(Exception):124class NoOAuthTokenException(Exception):
87 def __init__(self, file_name):125 def __init__(self, file_name):
@@ -91,7 +129,7 @@
91 return "OAuth details were not found in the ini file (%s)" % (129 return "OAuth details were not found in the ini file (%s)" % (
92 self.file_name)130 self.file_name)
93131
94def get_oauth_tokens(config_file_name=FILE_INI):132def get_oauth_tokens(config_file_name=None):
95 """Return the OAuth tokens from the desktop Couch ini file.133 """Return the OAuth tokens from the desktop Couch ini file.
96 CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth).134 CouchDB OAuth is two-legged OAuth (not three-legged like most OAuth).
97 We have one "consumer", defined by a consumer_key and a secret,135 We have one "consumer", defined by a consumer_key and a secret,
@@ -101,6 +139,9 @@
101 (More traditional 3-legged OAuth starts with a "request token" which is139 (More traditional 3-legged OAuth starts with a "request token" which is
102 then used to procure an "access token". We do not require this.)140 then used to procure an "access token". We do not require this.)
103 """141 """
142 if config_file_name is None:
143 config_file_name = DEFAULT_CONTEXT.file_ini
144
104 c = configparser.ConfigParser()145 c = configparser.ConfigParser()
105 # monkeypatch ConfigParser to stop it lower-casing option names146 # monkeypatch ConfigParser to stop it lower-casing option names
106 c.optionxform = lambda s: s147 c.optionxform = lambda s: s
@@ -123,9 +164,11 @@
123 raise NoOAuthTokenException(config_file_name)164 raise NoOAuthTokenException(config_file_name)
124 return out165 return out
125166
126167def get_bind_address(config_file_name=None):
127def get_bind_address(config_file_name=FILE_INI):
128 """Retreive a string if it exists, or None if it doesn't."""168 """Retreive a string if it exists, or None if it doesn't."""
169 if config_file_name is None:
170 config_file_name = DEFAULT_CONTEXT.file_ini
171
129 c = configparser.ConfigParser()172 c = configparser.ConfigParser()
130 try:173 try:
131 c.read(config_file_name)174 c.read(config_file_name)
@@ -134,7 +177,10 @@
134 logging.warn("config file %r error. %s", config_file_name, e)177 logging.warn("config file %r error. %s", config_file_name, e)
135 return None178 return None
136179
137def set_bind_address(address, config_file_name=FILE_INI):180def set_bind_address(address, config_file_name=None):
181 if config_file_name is None:
182 config_file_name = DEFAULT_CONTEXT.file_ini
183
138 c = configparser.SafeConfigParser()184 c = configparser.SafeConfigParser()
139 # monkeypatch ConfigParser to stop it lower-casing option names185 # monkeypatch ConfigParser to stop it lower-casing option names
140 c.optionxform = lambda s: s186 c.optionxform = lambda s: s
@@ -145,17 +191,3 @@
145 with open(config_file_name, 'wb') as configfile:191 with open(config_file_name, 'wb') as configfile:
146 c.write(configfile)192 c.write(configfile)
147193
148
149# You will need to add -b or -k on the end of this
150COUCH_EXEC_COMMAND = [COUCH_EXE, couch_chain_ini_files(), '-p', FILE_PID,
151 '-o', FILE_STDOUT, '-e', FILE_STDERR]
152
153
154# Set appropriate permissions on relevant files and folders
155for fn in [FILE_PID, FILE_STDOUT, FILE_STDERR, FILE_INI, FILE_LOG]:
156 if os.path.exists(fn):
157 os.chmod(fn, 0600)
158for dn in [rootdir, config_dir, DIR_DB]:
159 if os.path.isdir(dn):
160 os.chmod(dn, 0700)
161
162194
=== modified file 'desktopcouch/pair/couchdb_pairing/couchdb_io.py'
--- desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-10-27 19:00:33 +0000
+++ desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-11-18 18:51:14 +0000
@@ -24,7 +24,7 @@
24import datetime24import datetime
25from itertools import cycle25from itertools import cycle
2626
27from desktopcouch import find_pid, find_port as desktopcouch_find_port27from desktopcouch import find_pid, find_port as desktopcouch_find_port, local_files
28from desktopcouch.records import server28from desktopcouch.records import server
29from desktopcouch.records.record import Record29from desktopcouch.records.record import Record
3030
@@ -56,11 +56,13 @@
56 port = str(port)56 port = str(port)
57 return "%s://%s%s:%s/%s" % (protocol, auth, hostname, port, path)57 return "%s://%s%s:%s/%s" % (protocol, auth, hostname, port, path)
5858
59def _get_db(name, create=True, uri=None):59def _get_db(name, create=True, uri=None,
60 ctx=local_files.DEFAULT_CONTEXT):
60 """Get (and create?) a database."""61 """Get (and create?) a database."""
61 return server.CouchDatabase(name, create=create, uri=uri)62 return server.CouchDatabase(name, create=create, uri=uri, ctx=ctx)
6263
63def put_paired_host(oauth_data, uri=None, **kwargs):64def put_paired_host(oauth_data, uri=None,
65 ctx=local_files.DEFAULT_CONTEXT, **kwargs):
64 """Create a new paired-host record. OAuth information is required, and66 """Create a new paired-host record. OAuth information is required, and
65 after the uri, keyword parameters are added to the record."""67 after the uri, keyword parameters are added to the record."""
66 pairing_id = str(uuid.uuid4())68 pairing_id = str(uuid.uuid4())
@@ -77,25 +79,28 @@
77 "token_secret": str(oauth_data["token_secret"]),79 "token_secret": str(oauth_data["token_secret"]),
78 }80 }
79 data.update(kwargs)81 data.update(kwargs)
80 d = _get_db("management", uri=uri)82 d = _get_db("management", uri=uri, ctx=ctx)
81 r = Record(data)83 r = Record(data)
82 record_id = d.put_record(r)84 record_id = d.put_record(r)
83 return record_id85 return record_id
8486
85def put_static_paired_service(oauth_data, service_name, uri=None):87def put_static_paired_service(oauth_data, service_name, uri=None,
88 ctx=local_files.DEFAULT_CONTEXT):
86 """Create a new service record."""89 """Create a new service record."""
87 return put_paired_host(oauth_data, uri=uri, service_name=service_name,90 return put_paired_host(oauth_data, uri=uri, service_name=service_name,
88 pull_from_server=True, push_to_server=True)91 pull_from_server=True, push_to_server=True, ctx=ctx)
8992
90def put_dynamic_paired_host(hostname, remote_uuid, oauth_data, uri=None):93def put_dynamic_paired_host(hostname, remote_uuid, oauth_data, uri=None,
94 ctx=local_files.DEFAULT_CONTEXT):
91 """Create a new dynamic-host record."""95 """Create a new dynamic-host record."""
92 return put_paired_host(oauth_data, uri=uri, pairing_identifier=remote_uuid,96 return put_paired_host(oauth_data, uri=uri, pairing_identifier=remote_uuid,
93 push_to_server=True, server=hostname)97 push_to_server=True, server=hostname, ctx=ctx)
9498
95def get_static_paired_hosts(uri=None):99def get_static_paired_hosts(uri=None,
100 ctx=local_files.DEFAULT_CONTEXT):
96 """Retreive a list of static hosts' information in the form of101 """Retreive a list of static hosts' information in the form of
97 (ID, service name, to_push, to_pull) ."""102 (ID, service name, to_push, to_pull) ."""
98 db = _get_db("management", uri=uri)103 db = _get_db("management", uri=uri, ctx=ctx)
99 results = db.get_records(create_view=True)104 results = db.get_records(create_view=True)
100 found = dict()105 found = dict()
101 for row in results[PAIRED_SERVER_RECORD_TYPE]:106 for row in results[PAIRED_SERVER_RECORD_TYPE]:
@@ -113,14 +118,16 @@
113 logging.debug("static pairings are %s", unique_hosts)118 logging.debug("static pairings are %s", unique_hosts)
114 return unique_hosts119 return unique_hosts
115120
116def get_database_names_replicatable(uri, oauth_tokens=None, service=False):121def get_database_names_replicatable(uri, oauth_tokens=None, service=False,
122 ctx=local_files.DEFAULT_CONTEXT):
117 """Find a list of local databases, minus dbs that we do not want to123 """Find a list of local databases, minus dbs that we do not want to
118 replicate (explicitly or implicitly)."""124 replicate (explicitly or implicitly)."""
119 if not uri:125 if not uri:
120 find_pid()126 pid = find_pid(ctx=ctx)
121 port = desktopcouch_find_port()127 port = desktopcouch_find_port(pid=pid)
122 uri = "http://localhost:%s" % port128 uri = "http://localhost:%s" % port
123 couchdb_server = server.OAuthCapableServer(uri, oauth_tokens=oauth_tokens)129 couchdb_server = server.OAuthCapableServer(uri, oauth_tokens=oauth_tokens,
130 ctx=ctx)
124 try:131 try:
125 all_dbs = set([db_name for db_name in couchdb_server])132 all_dbs = set([db_name for db_name in couchdb_server])
126 except socket.error, e:133 except socket.error, e:
@@ -132,18 +139,19 @@
132 excluded.add("users")139 excluded.add("users")
133 if not service:140 if not service:
134 excluded_msets = _get_management_data(PAIRED_SERVER_RECORD_TYPE,141 excluded_msets = _get_management_data(PAIRED_SERVER_RECORD_TYPE,
135 "excluded_names", uri=uri)142 "excluded_names", uri=uri, ctx=ctx)
136 for excluded_mset in excluded_msets:143 for excluded_mset in excluded_msets:
137 excluded.update(excluded_mset)144 excluded.update(excluded_mset)
138145
139 return all_dbs - excluded146 return all_dbs - excluded
140147
141def get_my_host_unique_id(uri=None, create=True):148def get_my_host_unique_id(uri=None, create=True,
149 ctx=local_files.DEFAULT_CONTEXT):
142 """Returns a list of ids we call ourselves. We complain in the log if it's150 """Returns a list of ids we call ourselves. We complain in the log if it's
143 more than one, but it's really no error. If there are zero (id est, we've151 more than one, but it's really no error. If there are zero (id est, we've
144 never paired with anyone), then returns None."""152 never paired with anyone), then returns None."""
145153
146 db = _get_db("management", uri=uri)154 db = _get_db("management", uri=uri, ctx=ctx)
147 ids = _get_management_data(MY_ID_RECORD_TYPE, "self_identity", uri=uri)155 ids = _get_management_data(MY_ID_RECORD_TYPE, "self_identity", uri=uri)
148 ids = list(set(ids)) # uniqify156 ids = list(set(ids)) # uniqify
149 if len(ids) > 1:157 if len(ids) > 1:
@@ -165,11 +173,12 @@
165 logging.debug("set new self-identity value: %r", data["self_identity"])173 logging.debug("set new self-identity value: %r", data["self_identity"])
166 return [data["self_identity"]]174 return [data["self_identity"]]
167175
168def get_all_known_pairings(uri=None):176def get_all_known_pairings(uri=None,
177 ctx=local_files.DEFAULT_CONTEXT):
169 """Info dicts about all pairings, even if marked "unpaired", keyed on178 """Info dicts about all pairings, even if marked "unpaired", keyed on
170 hostid with another dict as the value."""179 hostid with another dict as the value."""
171 d = {}180 d = {}
172 db = _get_db("management", uri=uri)181 db = _get_db("management", uri=uri, ctx=ctx)
173 for row in db.get_records(PAIRED_SERVER_RECORD_TYPE):182 for row in db.get_records(PAIRED_SERVER_RECORD_TYPE):
174 v = dict()183 v = dict()
175 v["record_id"] = row.id184 v["record_id"] = row.id
@@ -182,8 +191,9 @@
182 d[hostid] = v191 d[hostid] = v
183 return d192 return d
184193
185def _get_management_data(record_type, key, uri=None):194def _get_management_data(record_type, key, uri=None,
186 db = _get_db("management", uri=uri)195 ctx=local_files.DEFAULT_CONTEXT):
196 db = _get_db("management", uri=uri, ctx=ctx)
187 results = db.get_records(create_view=True)197 results = db.get_records(create_view=True)
188 values = list()198 values = list()
189 for record in results[record_type]:199 for record in results[record_type]:
@@ -267,9 +277,9 @@
267 logging.exception("can't replicate %r %r <== %r", source_database,277 logging.exception("can't replicate %r %r <== %r", source_database,
268 url, obsfuscate(record))278 url, obsfuscate(record))
269279
270def get_pairings(uri=None):280def get_pairings(uri=None, ctx=local_files.DEFAULT_CONTEXT):
271 """Get a list of paired servers."""281 """Get a list of paired servers."""
272 db = _get_db("management", create=True, uri=None)282 db = _get_db("management", create=True, uri=None, ctx=ctx)
273283
274 design_doc = "paired_servers"284 design_doc = "paired_servers"
275 if not db.view_exists("paired_servers", design_doc):285 if not db.view_exists("paired_servers", design_doc):
@@ -292,10 +302,11 @@
292302
293 return db.execute_view("paired_servers")303 return db.execute_view("paired_servers")
294304
295def remove_pairing(record_id, is_reconciled, uri=None):305def remove_pairing(record_id, is_reconciled, uri=None,
306 ctx=local_files.DEFAULT_CONTEXT):
296 """Remove a pairing record (or mark it as dead so it can be cleaned up307 """Remove a pairing record (or mark it as dead so it can be cleaned up
297 properly later)."""308 properly later)."""
298 db = _get_db("management", create=True, uri=None)309 db = _get_db("management", create=True, uri=None, ctx=ctx)
299 if is_reconciled:310 if is_reconciled:
300 db.delete_record(record_id)311 db.delete_record(record_id)
301 else:312 else:
302313
=== modified file 'desktopcouch/pair/tests/test_couchdb_io.py'
--- desktopcouch/pair/tests/test_couchdb_io.py 2009-10-27 19:00:33 +0000
+++ desktopcouch/pair/tests/test_couchdb_io.py 2009-11-18 18:51:14 +0000
@@ -18,23 +18,26 @@
18import pygtk18import pygtk
19pygtk.require('2.0')19pygtk.require('2.0')
2020
21import desktopcouch.tests as dctests21import desktopcouch.tests as test_environment
2222
23from desktopcouch.pair.couchdb_pairing import couchdb_io23from desktopcouch.pair.couchdb_pairing import couchdb_io
24from desktopcouch.records.server import CouchDatabase24from desktopcouch.records.server import CouchDatabase
25from desktopcouch.records.record import Record25from desktopcouch.records.record import Record
26import unittest26from twisted.trial import unittest
27import uuid27import uuid
28import os28import os
29import httplib229import httplib2
30import socket
30URI = None # use autodiscovery that desktopcouch.tests permits.31URI = None # use autodiscovery that desktopcouch.tests permits.
3132
32class TestCouchdbIo(unittest.TestCase):33class TestCouchdbIo(unittest.TestCase):
3334
34 def setUp(self):35 def setUp(self):
35 """setup each test"""36 """setup each test"""
36 self.mgt_database = CouchDatabase('management', create=True, uri=URI)37 self.mgt_database = CouchDatabase('management', create=True, uri=URI,
37 self.foo_database = CouchDatabase('foo', create=True, uri=URI)38 ctx=test_environment.test_context)
39 self.foo_database = CouchDatabase('foo', create=True, uri=URI,
40 ctx=test_environment.test_context)
38 #create some records to pull out and test41 #create some records to pull out and test
39 self.foo_database.put_record(Record({42 self.foo_database.put_record(Record({
40 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",43 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
@@ -71,8 +74,8 @@
71 "token": str("opqrst"),74 "token": str("opqrst"),
72 "token_secret": str("uvwxyz"),75 "token_secret": str("uvwxyz"),
73 }76 }
74 couchdb_io.put_static_paired_service(oauth_data, service_name, uri=URI)77 couchdb_io.put_static_paired_service(oauth_data, service_name, uri=URI, ctx=test_environment.test_context)
75 pairings = list(couchdb_io.get_pairings())78 pairings = list(couchdb_io.get_pairings(ctx=test_environment.test_context))
7679
77 def test_put_dynamic_paired_host(self):80 def test_put_dynamic_paired_host(self):
78 hostname = "host%d" % (os.getpid(),)81 hostname = "host%d" % (os.getpid(),)
@@ -85,43 +88,48 @@
85 }88 }
8689
87 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,90 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
88 uri=URI)91 uri=URI, ctx=test_environment.test_context)
89 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,92 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
90 uri=URI)93 uri=URI, ctx=test_environment.test_context)
91 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,94 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
92 uri=URI)95 uri=URI, ctx=test_environment.test_context)
9396
94 pairings = list(couchdb_io.get_pairings())97 pairings = list(couchdb_io.get_pairings(ctx=test_environment.test_context))
95 self.assertEqual(3, len(pairings))98 self.assertEqual(3, len(pairings))
96 self.assertEqual(pairings[0].value["oauth"], oauth_data)99 self.assertEqual(pairings[0].value["oauth"], oauth_data)
97 self.assertEqual(pairings[0].value["server"], hostname)100 self.assertEqual(pairings[0].value["server"], hostname)
98 self.assertEqual(pairings[0].value["pairing_identifier"], remote_uuid)101 self.assertEqual(pairings[0].value["pairing_identifier"], remote_uuid)
99102
100 for i, row in enumerate(pairings):103 for i, row in enumerate(pairings):
101 couchdb_io.remove_pairing(row.id, i == 1)104 couchdb_io.remove_pairing(row.id, i == 1, ctx=test_environment.test_context)
102105
103 pairings = list(couchdb_io.get_pairings())106 pairings = list(couchdb_io.get_pairings(ctx=test_environment.test_context))
104 self.assertEqual(0, len(pairings))107 self.assertEqual(0, len(pairings))
105108
106109
107 def test_get_database_names_replicatable_bad_server(self):110 def test_get_database_names_replicatable_bad_server(self):
108 # If this resolves, FIRE YOUR DNS PROVIDER.111 hostname = "test.desktopcouch.example.com"
112 try:
113 socket.gethostbyname(hostname)
114 raise unittest.SkipTest("nxdomain hijacked")
115 except socket.gaierror:
116 pass
109117
110 try:118 try:
111 names = couchdb_io.get_database_names_replicatable(119 names = couchdb_io.get_database_names_replicatable(
112 uri='http://test.desktopcouch.example.com:9/')120 uri='http://' + hostname + ':9/')
113 self.assertEqual(set(), names)121 self.assertEqual(set(), names)
114 except httplib2.ServerNotFoundError:122 except httplib2.ServerNotFoundError:
115 pass123 pass
116124
117 def test_get_database_names_replicatable(self):125 def test_get_database_names_replicatable(self):
118 names = couchdb_io.get_database_names_replicatable(uri=URI)126 names = couchdb_io.get_database_names_replicatable(uri=URI, ctx=test_environment.test_context)
119 self.assertFalse('management' in names)127 self.assertFalse('management' in names)
120 self.assertTrue('foo' in names)128 self.assertTrue('foo' in names)
121129
122 def test_get_my_host_unique_id(self):130 def test_get_my_host_unique_id(self):
123 got = couchdb_io.get_my_host_unique_id(uri=URI)131 got = couchdb_io.get_my_host_unique_id(uri=URI, ctx=test_environment.test_context)
124 again = couchdb_io.get_my_host_unique_id(uri=URI)132 again = couchdb_io.get_my_host_unique_id(uri=URI, ctx=test_environment.test_context)
125 self.assertEquals(len(got), 1)133 self.assertEquals(len(got), 1)
126 self.assertEquals(got, again)134 self.assertEquals(got, again)
127135
128136
=== modified file 'desktopcouch/records/couchgrid.py'
--- desktopcouch/records/couchgrid.py 2009-10-10 23:56:45 +0000
+++ desktopcouch/records/couchgrid.py 2009-11-18 18:51:14 +0000
@@ -22,11 +22,12 @@
22import gobject22import gobject
23from server import CouchDatabase23from server import CouchDatabase
24from record import Record24from record import Record
25import desktopcouch
2526
26class CouchGrid(gtk.TreeView):27class CouchGrid(gtk.TreeView):
2728
28 def __init__(29 def __init__(self, database_name, record_type=None, keys=None, uri=None,
29 self, database_name, record_type=None, keys=None, uri=None):30 ctx=desktopcouch.local_files.DEFAULT_CONTEXT):
30 """Create a new Couchwidget31 """Create a new Couchwidget
31 arguments:32 arguments:
32 database_name - specify the name of the database in the desktop33 database_name - specify the name of the database in the desktop
@@ -63,6 +64,7 @@
63 self.__keys = keys64 self.__keys = keys
64 self.__editable = False65 self.__editable = False
65 self.uri = uri66 self.uri = uri
67 self.ctx = ctx
6668
67 #set the datatabase69 #set the datatabase
68 self.database = database_name70 self.database = database_name
@@ -122,9 +124,9 @@
122 @database.setter124 @database.setter
123 def database(self, db_name):125 def database(self, db_name):
124 if self.uri:126 if self.uri:
125 self.__db = CouchDatabase(db_name, create=True, uri=self.uri)127 self.__db = CouchDatabase(db_name, create=True, uri=self.uri, ctx=self.ctx)
126 else:128 else:
127 self.__db = CouchDatabase(db_name, create=True)129 self.__db = CouchDatabase(db_name, create=True, ctx=self.ctx)
128 if self.record_type != None:130 if self.record_type != None:
129 self.__populate_treeview()(self.record_type)131 self.__populate_treeview()(self.record_type)
130132
131133
=== modified file 'desktopcouch/records/doc/records.txt'
--- desktopcouch/records/doc/records.txt 2009-10-05 13:39:38 +0000
+++ desktopcouch/records/doc/records.txt 2009-11-18 18:51:14 +0000
@@ -6,7 +6,7 @@
6Create a database object. Your database needs to exist. If it doesn't, you6Create a database object. Your database needs to exist. If it doesn't, you
7can create it by passing create=True.7can create it by passing create=True.
88
9>>> db = CouchDatabase('testing', create=True)9 >> db = CouchDatabase('testing', create=True)
1010
11Create a Record object. Records have a record type, which should be a11Create a Record object. Records have a record type, which should be a
12URL. The URL should point to a human-readable document which12URL. The URL should point to a human-readable document which
1313
=== modified file 'desktopcouch/records/server.py'
--- desktopcouch/records/server.py 2009-10-14 17:28:43 +0000
+++ desktopcouch/records/server.py 2009-11-18 18:51:14 +0000
@@ -28,13 +28,15 @@
28import urlparse28import urlparse
2929
30class OAuthCapableServer(Server):30class OAuthCapableServer(Server):
31 def __init__(self, uri, oauth_tokens=None):31 def __init__(self, uri, oauth_tokens=None, ctx=None):
32 """Subclass of couchdb.client.Server which creates a custom32 """Subclass of couchdb.client.Server which creates a custom
33 httplib2.Http subclass which understands OAuth"""33 httplib2.Http subclass which understands OAuth"""
34 http = server_base.OAuthCapableHttp(scheme=urlparse.urlparse(uri)[0])34 http = server_base.OAuthCapableHttp(scheme=urlparse.urlparse(uri)[0])
35 http.force_exception_to_status_code = False35 http.force_exception_to_status_code = False
36 if ctx is None:
37 ctx = desktopcouch.local_files.DEFAULT_CONTEXT
36 if oauth_tokens is None:38 if oauth_tokens is None:
37 oauth_tokens = desktopcouch.local_files.get_oauth_tokens()39 oauth_tokens = desktopcouch.local_files.get_oauth_tokens(ctx.file_ini)
38 (consumer_key, consumer_secret, token, token_secret) = (40 (consumer_key, consumer_secret, token, token_secret) = (
39 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"], 41 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"],
40 oauth_tokens["token"], oauth_tokens["token_secret"])42 oauth_tokens["token"], oauth_tokens["token_secret"])
@@ -45,11 +47,12 @@
45 """An small records specific abstraction over a couch db database."""47 """An small records specific abstraction over a couch db database."""
46 48
47 def __init__(self, database, uri=None, record_factory=None, create=False,49 def __init__(self, database, uri=None, record_factory=None, create=False,
48 server_class=OAuthCapableServer, oauth_tokens=None):50 server_class=OAuthCapableServer, oauth_tokens=None,
51 ctx=desktopcouch.local_files.DEFAULT_CONTEXT):
49 if not uri:52 if not uri:
50 desktopcouch.find_pid()53 pid = desktopcouch.find_pid(ctx=ctx)
51 port = desktopcouch.find_port()54 port = desktopcouch.find_port(pid)
52 uri = "http://localhost:%s" % port55 uri = "http://localhost:%s" % port
53 super(CouchDatabase, self).__init__(56 super(CouchDatabase, self).__init__(
54 database, uri, record_factory=record_factory, create=create,57 database, uri, record_factory=record_factory, create=create,
55 server_class=server_class, oauth_tokens=oauth_tokens)58 server_class=server_class, oauth_tokens=oauth_tokens, ctx=ctx)
5659
=== modified file 'desktopcouch/records/tests/test_couchgrid.py'
--- desktopcouch/records/tests/test_couchgrid.py 2009-10-10 23:52:01 +0000
+++ desktopcouch/records/tests/test_couchgrid.py 2009-11-18 18:51:14 +0000
@@ -20,7 +20,7 @@
2020
21from testtools import TestCase21from testtools import TestCase
2222
23from desktopcouch.tests import xdg_cache23import desktopcouch.tests as test_environment
2424
25from desktopcouch.records.record import Record25from desktopcouch.records.record import Record
26from desktopcouch.records.server import CouchDatabase26from desktopcouch.records.server import CouchDatabase
@@ -31,9 +31,9 @@
31 """Test the CouchGrid functionality"""31 """Test the CouchGrid functionality"""
3232
33 def setUp(self):33 def setUp(self):
34 self.assert_(xdg_cache)
35 self.dbname = self._testMethodName34 self.dbname = self._testMethodName
36 self.db = CouchDatabase(self.dbname, create=True)35 self.db = CouchDatabase(self.dbname, create=True,
36 ctx=test_environment.test_context)
37 self.record_type = "test_record_type"37 self.record_type = "test_record_type"
3838
39 def tearDown(self):39 def tearDown(self):
@@ -46,7 +46,7 @@
46 database name.46 database name.
47 """47 """
48 try:48 try:
49 cw = CouchGrid(None)49 cw = CouchGrid(None, ctx=test_environment.test_context)
50 except TypeError, inst:50 except TypeError, inst:
51 self.assertEqual(51 self.assertEqual(
52 inst.args[0],"database_name is required and must be a string")52 inst.args[0],"database_name is required and must be a string")
@@ -55,7 +55,7 @@
55 """Test a simple creating a CouchGrid """55 """Test a simple creating a CouchGrid """
5656
57 #create a test widget with test database values57 #create a test widget with test database values
58 cw = CouchGrid(self.dbname)58 cw = CouchGrid(self.dbname, ctx=test_environment.test_context)
5959
60 #allow editing60 #allow editing
61 cw.editable = True61 cw.editable = True
@@ -87,7 +87,7 @@
8787
88 try:88 try:
89 #create a test widget with test database values89 #create a test widget with test database values
90 cw = CouchGrid(self.dbname)90 cw = CouchGrid(self.dbname, ctx=test_environment.test_context)
9191
92 #set the record_type for the TreeView92 #set the record_type for the TreeView
93 #it will not populate without this value being set93 #it will not populate without this value being set
@@ -113,7 +113,8 @@
113113
114 def test_all_from_database(self):114 def test_all_from_database(self):
115 #create some records115 #create some records
116 db = CouchDatabase(self.dbname, create=True)116 db = CouchDatabase(self.dbname, create=True,
117 ctx=test_environment.test_context)
117 db.put_record(Record({118 db.put_record(Record({
118 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",119 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
119 "record_type": self.record_type}))120 "record_type": self.record_type}))
@@ -122,7 +123,7 @@
122 "record_type": self.record_type}))123 "record_type": self.record_type}))
123124
124 #build the CouchGrid125 #build the CouchGrid
125 cw = CouchGrid(self.dbname)126 cw = CouchGrid(self.dbname, ctx=test_environment.test_context)
126 cw.record_type = self.record_type127 cw.record_type = self.record_type
127 #make sure there are three columns and two rows128 #make sure there are three columns and two rows
128 self.assertEqual(cw.get_model().get_n_columns(),4)129 self.assertEqual(cw.get_model().get_n_columns(),4)
@@ -130,7 +131,8 @@
130131
131 def test_selected_id_property(self):132 def test_selected_id_property(self):
132 #create some records133 #create some records
133 db = CouchDatabase(self.dbname, create=True)134 db = CouchDatabase(self.dbname, create=True,
135 ctx=test_environment.test_context)
134 id1 = db.put_record(Record({136 id1 = db.put_record(Record({
135 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",137 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
136 "record_type": self.record_type}))138 "record_type": self.record_type}))
@@ -139,7 +141,7 @@
139 "record_type": self.record_type}))141 "record_type": self.record_type}))
140142
141 #build the CouchGrid143 #build the CouchGrid
142 cw = CouchGrid(self.dbname)144 cw = CouchGrid(self.dbname, ctx=test_environment.test_context)
143 cw.record_type = self.record_type145 cw.record_type = self.record_type
144146
145 #make sure the record ids are selected properly147 #make sure the record ids are selected properly
@@ -158,7 +160,7 @@
158 "key1_1": "val2_1", "key1_2": "val2_2", "key1_3": "val2_3",160 "key1_1": "val2_1", "key1_2": "val2_2", "key1_3": "val2_3",
159 "record_type": self.record_type}))161 "record_type": self.record_type}))
160 #build the CouchGrid162 #build the CouchGrid
161 cw = CouchGrid(self.dbname)163 cw = CouchGrid(self.dbname, ctx=test_environment.test_context)
162 cw.keys = ["key1_1"]164 cw.keys = ["key1_1"]
163 cw.record_type = self.record_type165 cw.record_type = self.record_type
164 #make sure there are three columns and two rows166 #make sure there are three columns and two rows
@@ -176,7 +178,8 @@
176 "record_type": self.record_type}))178 "record_type": self.record_type}))
177179
178 #create a test widget with test database values180 #create a test widget with test database values
179 cw = CouchGrid(self.dbname, record_type=self.record_type)181 cw = CouchGrid(self.dbname, record_type=self.record_type,
182 ctx=test_environment.test_context)
180183
181 #make sure there are three columns and two rows184 #make sure there are three columns and two rows
182 self.assertEqual(cw.get_model().get_n_columns(),4)185 self.assertEqual(cw.get_model().get_n_columns(),4)
@@ -188,7 +191,8 @@
188 #create a test widget with test database values191 #create a test widget with test database values
189 cw = CouchGrid(192 cw = CouchGrid(
190 self.dbname, record_type=self.record_type,193 self.dbname, record_type=self.record_type,
191 keys=["Key1", "Key2", "Key3", "Key4"])194 keys=["Key1", "Key2", "Key3", "Key4"],
195 ctx=test_environment.test_context)
192196
193 #create a row with all four columns set197 #create a row with all four columns set
194 cw.append_row(["val1", "val2", "val3", "val4"])198 cw.append_row(["val1", "val2", "val3", "val4"])
@@ -214,7 +218,8 @@
214 "record_type": self.record_type}))218 "record_type": self.record_type}))
215219
216 #create a test widget with test database values220 #create a test widget with test database values
217 cw = CouchGrid(self.dbname, record_type=self.record_type)221 cw = CouchGrid(self.dbname, record_type=self.record_type,
222 ctx=test_environment.test_context)
218223
219 #allow editing224 #allow editing
220 cw.append_row(["boo", "ray"])225 cw.append_row(["boo", "ray"])
221226
=== modified file 'desktopcouch/records/tests/test_record.py'
--- desktopcouch/records/tests/test_record.py 2009-11-17 22:03:11 +0000
+++ desktopcouch/records/tests/test_record.py 2009-11-18 18:51:14 +0000
@@ -23,8 +23,10 @@
2323
24# pylint does not like relative imports from containing packages24# pylint does not like relative imports from containing packages
25# pylint: disable-msg=F040125# pylint: disable-msg=F0401
26from desktopcouch.records.server import CouchDatabase
26from desktopcouch.records.record import (Record, RecordDict, MergeableList,27from desktopcouch.records.record import (Record, RecordDict, MergeableList,
27 record_factory, IllegalKeyException, validate, NoRecordTypeSpecified)28 record_factory, IllegalKeyException, validate, NoRecordTypeSpecified)
29import desktopcouch.tests as test_environment
2830
2931
30class TestRecords(TestCase):32class TestRecords(TestCase):
@@ -202,7 +204,9 @@
202 self.record.record_type)204 self.record.record_type)
203205
204 def test_run_doctests(self):206 def test_run_doctests(self):
205 results = doctest.testfile('../doc/records.txt')207 ctx = test_environment.test_context
208 globs = { "db": CouchDatabase('testing', create=True, ctx=ctx) }
209 results = doctest.testfile('../doc/records.txt', globs=globs)
206 self.assertEqual(0, results.failed)210 self.assertEqual(0, results.failed)
207211
208212
209213
=== modified file 'desktopcouch/records/tests/test_server.py'
--- desktopcouch/records/tests/test_server.py 2009-11-18 18:29:04 +0000
+++ desktopcouch/records/tests/test_server.py 2009-11-18 18:51:14 +0000
@@ -19,7 +19,7 @@
19"""testing database/contact.py module"""19"""testing database/contact.py module"""
20import testtools20import testtools
2121
22from desktopcouch.tests import xdg_cache22import desktopcouch.tests as test_environment
23from desktopcouch.records.server import CouchDatabase23from desktopcouch.records.server import CouchDatabase
24from desktopcouch.records.server_base import row_is_deleted, NoSuchDatabase24from desktopcouch.records.server_base import row_is_deleted, NoSuchDatabase
25from desktopcouch.records.record import Record25from desktopcouch.records.record import Record
@@ -41,9 +41,9 @@
41 def setUp(self):41 def setUp(self):
42 """setup each test"""42 """setup each test"""
43 # Connect to CouchDB server43 # Connect to CouchDB server
44 self.assert_(xdg_cache)
45 self.dbname = self._testMethodName44 self.dbname = self._testMethodName
46 self.database = CouchDatabase(self.dbname, create=True)45 self.database = CouchDatabase(self.dbname, create=True,
46 ctx=test_environment.test_context)
47 #create some records to pull out and test47 #create some records to pull out and test
48 self.database.put_record(Record({48 self.database.put_record(Record({
49 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",49 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
5050
=== modified file 'desktopcouch/start_local_couchdb.py'
--- desktopcouch/start_local_couchdb.py 2009-11-15 21:11:21 +0000
+++ desktopcouch/start_local_couchdb.py 2009-11-18 18:51:14 +0000
@@ -41,6 +41,8 @@
41import errno41import errno
42import time, gnomekeyring42import time, gnomekeyring
43from desktopcouch.records.server import CouchDatabase43from desktopcouch.records.server import CouchDatabase
44import logging
45import itertools
4446
45ACCEPTABLE_USERNAME_PASSWORD_CHARS = string.lowercase + string.uppercase47ACCEPTABLE_USERNAME_PASSWORD_CHARS = string.lowercase + string.uppercase
4648
@@ -58,9 +60,12 @@
58 fd.write("\n")60 fd.write("\n")
59 fd.close()61 fd.close()
6062
61def create_ini_file(port="0"):63def create_ini_file(port="0", ctx=local_files.DEFAULT_CONTEXT):
62 """Write CouchDB ini file if not already present"""64 """Write CouchDB ini file if not already present"""
63 if os.path.exists(local_files.FILE_INI):65
66 print "ini file is at", ctx.file_ini
67
68 if os.path.exists(ctx.file_ini):
64 # load the username and password from the keyring69 # load the username and password from the keyring
65 try:70 try:
66 data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,71 data = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
@@ -85,19 +90,18 @@
85 consumer_secret = make_random_string(10)90 consumer_secret = make_random_string(10)
86 token = make_random_string(10)91 token = make_random_string(10)
87 token_secret = make_random_string(10)92 token_secret = make_random_string(10)
88 db_dir = local_files.DIR_DB
89 local = {93 local = {
90 'couchdb': {94 'couchdb': {
91 'database_dir': db_dir,95 'database_dir': ctx.db_dir,
92 'view_index_dir': db_dir,96 'view_index_dir': ctx.db_dir,
93 },97 },
94 'httpd': {98 'httpd': {
95 'bind_address': '127.0.0.1',99 'bind_address': '127.0.0.1',
96 'port': port,100 'port': port,
97 },101 },
98 'log': {102 'log': {
99 'file': local_files.FILE_LOG,103 'file': ctx.file_log,
100 'level': 'info',104 'level': ctx.couchdb_log_level,
101 },105 },
102 'admins': {106 'admins': {
103 admin_account_username: admin_account_basic_auth_password107 admin_account_username: admin_account_basic_auth_password
@@ -116,7 +120,9 @@
116 }120 }
117 }121 }
118122
119 dump_ini(local, local_files.FILE_INI)123 dump_ini(local, ctx.file_ini)
124 ctx.ensure_files_not_readable()
125
120 # save admin account details in keyring126 # save admin account details in keyring
121 item_id = gnomekeyring.item_create_sync(127 item_id = gnomekeyring.item_create_sync(
122 None,128 None,
@@ -169,25 +175,31 @@
169except KeyError:175except KeyError:
170 raise NotImplementedError("os %r is not yet supported" % (os_name,))176 raise NotImplementedError("os %r is not yet supported" % (os_name,))
171177
172def read_pidfile():178def read_pidfile(ctx=local_files.DEFAULT_CONTEXT):
173 try:179 try:
174 pid_file = local_files.FILE_PID180 pid_file = ctx.file_pid
181 if not os.path.exists(pid_file):
182 return None
175 with open(pid_file) as fp:183 with open(pid_file) as fp:
176 try:184 try:
177 contents = fp.read()185 contents = fp.read()
186 if contents == "\n":
187 return None # not yet written to pid file
178 return int(contents)188 return int(contents)
179 except ValueError:189 except ValueError:
190 logging.warn("Pid file does not contain int: %r", contents)
180 return None191 return None
181 except IOError:192 except IOError, e:
193 logging.warn("Reading pid file caused error. %s", e)
182 return None194 return None
183195
184def run_couchdb():196def run_couchdb(ctx=local_files.DEFAULT_CONTEXT):
185 """Actually start the CouchDB process. Return its PID."""197 """Actually start the CouchDB process. Return its PID."""
186 pid = read_pidfile()198 pid = read_pidfile(ctx)
187 if pid is not None and not process_is_couchdb(pid):199 if pid is not None and not process_is_couchdb(pid):
188 print "Removing stale, deceptive pid file."200 print "Removing stale, deceptive pid file."
189 os.remove(local_files.FILE_PID)201 os.remove(ctx.file_pid)
190 local_exec = local_files.COUCH_EXEC_COMMAND + ['-b']202 local_exec = ctx.couch_exec_command + ['-b']
191 try:203 try:
192 # subprocess is buggy. Chad patched, but that takes time to propagate.204 # subprocess is buggy. Chad patched, but that takes time to propagate.
193 proc = subprocess.Popen(local_exec)205 proc = subprocess.Popen(local_exec)
@@ -209,14 +221,15 @@
209221
210 # give the process a chance to start222 # give the process a chance to start
211 for timeout in xrange(1000):223 for timeout in xrange(1000):
212 pid = read_pidfile()224 pid = read_pidfile(ctx=ctx)
213 time.sleep(0.3)225 time.sleep(0.3)
214 if pid is not None and process_is_couchdb(pid):226 if pid is not None and process_is_couchdb(pid):
215 break227 break
216 print "...waiting for couchdb to start..."228 print "...waiting for couchdb to start..."
229 ctx.ensure_files_not_readable()
217 return pid230 return pid
218231
219def update_design_documents():232def update_design_documents(ctx=local_files.DEFAULT_CONTEXT):
220 """Check system design documents and update any that need updating233 """Check system design documents and update any that need updating
221234
222 A database should be created if235 A database should be created if
@@ -225,17 +238,20 @@
225 $XDG_DATA_DIRs/desktop-couch/databases/dbname/_design/designdocname/views/viewname/map.js238 $XDG_DATA_DIRs/desktop-couch/databases/dbname/_design/designdocname/views/viewname/map.js
226 reduce.js may also exist in the same folder.239 reduce.js may also exist in the same folder.
227 """240 """
228 for base in xdg.BaseDirectory.xdg_data_dirs:241 ctx_data_dir = os.path.split(ctx.db_dir)[0]
242 for base in itertools.chain([ctx_data_dir], xdg.BaseDirectory.xdg_data_dirs):
243 # FIXME: base may have magic chars. assert not glob.has_magic(base) ?
229 db_spec = os.path.join(244 db_spec = os.path.join(
230 base, "desktop-couch", "databases", "*", "database.cfg")245 base, "desktop-couch", "databases", "*", "database.cfg")
231 for database_path in glob.glob(db_spec):246 for database_path in glob.glob(db_spec):
232 database_root = os.path.split(database_path)[0]247 database_root = os.path.split(database_path)[0]
233 database_name = os.path.split(database_root)[1]248 database_name = os.path.split(database_root)[1]
234 # Just the presence of database.cfg is enough to create the database249 # Just the presence of database.cfg is enough to create the database
235 db = CouchDatabase(database_name, create=True)250 db = CouchDatabase(database_name, create=True, ctx=ctx)
236 # look for design documents251 # look for design documents
237 dd_spec = os.path.join(252 dd_spec = os.path.join(
238 database_root, "_design", "*", "views", "*", "map.js")253 database_root, "_design", "*", "views", "*", "map.js")
254 # FIXME: dd_path may have magic chars.
239 for dd_path in glob.glob(dd_spec):255 for dd_path in glob.glob(dd_spec):
240 view_root = os.path.split(dd_path)[0]256 view_root = os.path.split(dd_path)[0]
241 view_name = os.path.split(view_root)[1]257 view_name = os.path.split(view_root)[1]
@@ -258,9 +274,9 @@
258 # than inefficiently just overwriting it regardless274 # than inefficiently just overwriting it regardless
259 db.add_view(view_name, mapjs, reducejs, dd_name)275 db.add_view(view_name, mapjs, reducejs, dd_name)
260276
261def write_bookmark_file(username, password, pid):277def write_bookmark_file(username, password, pid, ctx=local_files.DEFAULT_CONTEXT):
262 """Write out an HTML document that the user can bookmark to find their DB"""278 """Write out an HTML document that the user can bookmark to find their DB"""
263 bookmark_file = os.path.join(local_files.DIR_DB, "couchdb.html")279 bookmark_file = os.path.join(ctx.db_dir, "couchdb.html")
264280
265 if os.path.exists(281 if os.path.exists(
266 os.path.join(os.path.split(__file__)[0], "../data/couchdb.tmpl")):282 os.path.join(os.path.split(__file__)[0], "../data/couchdb.tmpl")):
@@ -292,16 +308,16 @@
292 finally:308 finally:
293 fp.close()309 fp.close()
294310
295def start_couchdb():311def start_couchdb(ctx=local_files.DEFAULT_CONTEXT):
296 """Execute each step to start a desktop CouchDB."""312 """Execute each step to start a desktop CouchDB."""
297 username, password = create_ini_file()313 username, password = create_ini_file(ctx=ctx)
298 pid = run_couchdb()314 pid = run_couchdb(ctx=ctx)
299 # Note that we do not call update_design_documents here. This is because315 # Note that we do not call update_design_documents here. This is because
300 # Couch won't actually have started yet, so when update_design_documents316 # Couch won't actually have started yet, so when update_design_documents
301 # calls the Records API, that will call back into get_port and we end up317 # calls the Records API, that will call back into get_port and we end up
302 # starting Couch again. Instead, get_port calls update_design_documents318 # starting Couch again. Instead, get_port calls update_design_documents
303 # *after* Couch startup has occurred.319 # *after* Couch startup has occurred.
304 write_bookmark_file(username, password, pid)320 write_bookmark_file(username, password, pid, ctx=ctx)
305 return pid321 return pid
306322
307323
308324
=== modified file 'desktopcouch/tests/__init__.py'
--- desktopcouch/tests/__init__.py 2009-09-14 15:56:42 +0000
+++ desktopcouch/tests/__init__.py 2009-11-18 18:51:14 +0000
@@ -1,47 +1,48 @@
1"""Tests for Desktop CouchDB"""1"""Tests for Desktop CouchDB"""
22
3import os, tempfile, atexit, shutil3import os, tempfile, atexit, shutil
4from desktopcouch.start_local_couchdb import start_couchdb, read_pidfile
4from desktopcouch.stop_local_couchdb import stop_couchdb5from desktopcouch.stop_local_couchdb import stop_couchdb
56from desktopcouch import local_files
6def stop_test_couch():7
7 from desktopcouch.start_local_couchdb import read_pidfile8import gobject
8 pid = read_pidfile()9gobject.set_application_name("desktopcouch testing")
9 stop_couchdb(pid=pid)10
10 shutil.rmtree(basedir)11
1112def create_new_test_environment():
12atexit.register(stop_test_couch)13
1314 basedir = tempfile.mkdtemp()
14basedir = tempfile.mkdtemp()15 if not os.path.exists(basedir):
15if not os.path.exists(basedir):16 os.mkdir(basedir)
16 os.mkdir(basedir)17
1718 cache = os.path.join(basedir, 'cache')
18xdg_cache = os.path.join(basedir, 'xdg_cache')19 data = os.path.join(basedir, 'data')
19if not os.path.exists(xdg_cache):20 config = os.path.join(basedir, 'config')
20 os.mkdir(xdg_cache)21 new_context = local_files.Context(cache, data, config)
2122 new_context.couchdb_log_level = 'debug'
22xdg_data = os.path.join(basedir, 'xdg_data')23
23if not os.path.exists(xdg_data):24 # Add etc folder to config
24 os.mkdir(xdg_data)25 SOURCE_TREE_ETC_FOLDER = os.path.realpath(
2526 os.path.join(os.path.split(__file__)[0], "..", "..", "config")
26xdg_config = os.path.join(basedir, 'xdg_config')27 )
27if not os.path.exists(xdg_config):28 if os.path.isdir(SOURCE_TREE_ETC_FOLDER):
28 os.mkdir(xdg_config)29 os.environ["XDG_CONFIG_DIRS"] = SOURCE_TREE_ETC_FOLDER
2930
30# Add etc folder to config31 def stop_test_couch(temp_dir, ctx):
31SOURCE_TREE_ETC_FOLDER = os.path.realpath(32 pid = read_pidfile(ctx)
32 os.path.join(os.path.split(__file__)[0], "..", "..", "config")33 stop_couchdb(pid=pid)
33)34 shutil.rmtree(temp_dir)
34if os.path.isdir(SOURCE_TREE_ETC_FOLDER):35
35 os.environ["XDG_CONFIG_DIRS"] = SOURCE_TREE_ETC_FOLDER36 start_couchdb(ctx=new_context)
3637 atexit.register(stop_test_couch, basedir, new_context)
37os.environ['XDG_CACHE_HOME'] = xdg_cache38
38os.environ['XDG_DATA_HOME'] = xdg_data39 return new_context
39os.environ['XDG_CONFIG_HOME'] = xdg_config40
41
42# TODO: Remove these after you're sure nothing we care about uses these env.
43os.environ['XDG_CACHE_HOME'] = "/cachehome"
44os.environ['XDG_DATA_HOME'] = "/datahome"
45os.environ['XDG_CONFIG_HOME'] = "/confighome"
40os.environ['XDG_DATA_DIRS'] = ''46os.environ['XDG_DATA_DIRS'] = ''
4147
42# Force reload packages, so that the correct dekstopcouch will be48test_context = create_new_test_environment()
43# started.
44import xdg.BaseDirectory
45reload(xdg.BaseDirectory)
46from desktopcouch import local_files
47reload(local_files)
4849
=== modified file 'desktopcouch/tests/test_local_files.py'
--- desktopcouch/tests/test_local_files.py 2009-11-17 22:03:11 +0000
+++ desktopcouch/tests/test_local_files.py 2009-11-18 18:51:14 +0000
@@ -1,7 +1,7 @@
1"""testing desktopcouch/local_files.py module"""1"""testing desktopcouch/local_files.py module"""
22
3import testtools3import testtools
4import desktopcouch.tests4import desktopcouch.tests as test_environment
5import desktopcouch5import desktopcouch
6import os6import os
77
@@ -11,19 +11,21 @@
11 "Does local_files list all the files that it needs to?"11 "Does local_files list all the files that it needs to?"
12 import desktopcouch.local_files12 import desktopcouch.local_files
13 for required in [13 for required in [
14 "FILE_LOG", "FILE_INI", "FILE_PID", "FILE_STDOUT",14 "file_log", "file_ini", "file_pid", "file_stdout",
15 "FILE_STDERR", "DIR_DB", "COUCH_EXE", "COUCH_EXEC_COMMAND"]:15 "file_stderr", "db_dir"]:
16 self.assertTrue(required in dir(desktopcouch.local_files))16 #"couch_exe", "couch_exec_command"
17 self.assertTrue(required in dir(test_environment.test_context))
1718
18 def test_xdg_overwrite_works(self):19 def test_xdg_overwrite_works(self):
19 # this should really check that it's in os.environ["TMP"]20 # this should really check that it's in os.environ["TMP"]
20 self.assertTrue(desktopcouch.local_files.FILE_INI.startswith("/tmp"))21 self.assertTrue(test_environment.test_context.file_ini.startswith("/tmp"))
2122
22 def test_couch_chain_ini_files(self):23 def test_couch_chain_ini_files(self):
23 "Is compulsory-auth.ini picked up by the ini file finder?"24 "Is compulsory-auth.ini picked up by the ini file finder?"
24 import desktopcouch.local_files25 import desktopcouch.local_files
25 ok = [x for x in desktopcouch.local_files.couch_chain_ini_files().split()26 ok = [x for x
26 if x.endswith("compulsory-auth.ini")]27 in test_environment.test_context.couch_chain_ini_files().split()
28 if x.endswith("compulsory-auth.ini")]
27 self.assertTrue(len(ok) > 0)29 self.assertTrue(len(ok) > 0)
2830
29 def test_bind_address(self):31 def test_bind_address(self):
3032
=== added file 'desktopcouch/tests/test_replication.py'
--- desktopcouch/tests/test_replication.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/tests/test_replication.py 2009-11-18 18:51:14 +0000
@@ -0,0 +1,21 @@
1"""testing desktopcouch/start_local_couchdb.py module"""
2
3import testtools
4import os, sys
5import desktopcouch.tests as test_environment
6import desktopcouch
7sys.path.append(
8 os.path.join(os.path.split(desktopcouch.__file__)[0], "..", "contrib"))
9
10class TestReplication(testtools.TestCase):
11 """Testing that the database/designdoc filesystem loader works"""
12
13 def setUp(self):
14 self.db_apple = desktopcouch.records.server.CouchDatabase("apple",
15 create=True, ctx=test_environment.test_context)
16 banana = test_environment.create_new_test_environment()
17 self.db_banana = desktopcouch.records.server.CouchDatabase("banana",
18 create=True, ctx=banana)
19
20 def test_creation(self):
21 pass
022
=== modified file 'desktopcouch/tests/test_start_local_couchdb.py'
--- desktopcouch/tests/test_start_local_couchdb.py 2009-09-14 15:56:42 +0000
+++ desktopcouch/tests/test_start_local_couchdb.py 2009-11-18 18:51:14 +0000
@@ -2,7 +2,7 @@
22
3import testtools3import testtools
4import os, sys4import os, sys
5from desktopcouch.tests import xdg_data5import desktopcouch.tests as test_environment
6import desktopcouch6import desktopcouch
7sys.path.append(7sys.path.append(
8 os.path.join(os.path.split(desktopcouch.__file__)[0], "..", "contrib"))8 os.path.join(os.path.split(desktopcouch.__file__)[0], "..", "contrib"))
@@ -71,6 +71,7 @@
7171
72 def setUp(self):72 def setUp(self):
73 # create temp folder with databases and design documents in73 # create temp folder with databases and design documents in
74 xdg_data = os.path.split(test_environment.test_context.db_dir)[0]
74 try:75 try:
75 os.mkdir(os.path.join(xdg_data, "desktop-couch"))76 os.mkdir(os.path.join(xdg_data, "desktop-couch"))
76 except OSError:77 except OSError:
@@ -90,21 +91,22 @@
90 couchdb = mocker.replace("desktopcouch.records.server.CouchDatabase")91 couchdb = mocker.replace("desktopcouch.records.server.CouchDatabase")
9192
92 # databases that should be created93 # databases that should be created
93 couchdb("cfg", create=True)94 couchdb("cfg", create=True, ctx=test_environment.test_context)
94 couchdb("cfg_and_empty_design", create=True)95 couchdb("cfg_and_empty_design", create=True, ctx=test_environment.test_context)
95 couchdb("cfg_and_design_no_views", create=True)96 couchdb("cfg_and_design_no_views", create=True, ctx=test_environment.test_context)
96 couchdb("cfg_and_design_one_view_no_map", create=True)97 couchdb("cfg_and_design_one_view_no_map", create=True, ctx=test_environment.test_context)
97 couchdb("cfg_and_design_one_view_map_no_reduce", create=True)98 couchdb("cfg_and_design_one_view_map_no_reduce", create=True, ctx=test_environment.test_context)
99
98 dbmock1 = mocker.mock()100 dbmock1 = mocker.mock()
99 mocker.result(dbmock1)101 mocker.result(dbmock1)
100 dbmock1.add_view("view1", "cfg_and_design_one_view_map_no_reduce:map",102 dbmock1.add_view("view1", "cfg_and_design_one_view_map_no_reduce:map",
101 None, "doc1")103 None, "doc1")
102 couchdb("cfg_and_design_one_view_map_reduce", create=True)104 couchdb("cfg_and_design_one_view_map_reduce", create=True, ctx=test_environment.test_context)
103 dbmock2 = mocker.mock()105 dbmock2 = mocker.mock()
104 mocker.result(dbmock2)106 mocker.result(dbmock2)
105 dbmock2.add_view("view1", "cfg_and_design_one_view_map_reduce:map",107 dbmock2.add_view("view1", "cfg_and_design_one_view_map_reduce:map",
106 "cfg_and_design_one_view_map_reduce:reduce", "doc1")108 "cfg_and_design_one_view_map_reduce:reduce", "doc1")
107 couchdb("cfg_and_design_two_views_map_reduce", create=True)109 couchdb("cfg_and_design_two_views_map_reduce", create=True, ctx=test_environment.test_context)
108 dbmock3 = mocker.mock()110 dbmock3 = mocker.mock()
109 mocker.result(dbmock3)111 mocker.result(dbmock3)
110 dbmock3.add_view("view1", "cfg_and_design_two_views_map_reduce:map1",112 dbmock3.add_view("view1", "cfg_and_design_two_views_map_reduce:map1",
@@ -116,7 +118,7 @@
116 # all the right things118 # all the right things
117 mocker.replay()119 mocker.replay()
118 from desktopcouch.start_local_couchdb import update_design_documents120 from desktopcouch.start_local_couchdb import update_design_documents
119 update_design_documents()121 update_design_documents(ctx=test_environment.test_context)
120122
121 mocker.restore()123 mocker.restore()
122 mocker.verify()124 mocker.verify()

Subscribers

People subscribed via source and target branches