Merge lp:~facundo/magicicada-client/all-venv into lp:magicicada-client

Proposed by Facundo Batista
Status: Rejected
Rejected by: Facundo Batista
Proposed branch: lp:~facundo/magicicada-client/all-venv
Merge into: lp:magicicada-client
Diff against target: 2996 lines (+2723/-51)
29 files modified
Makefile (+1/-1)
contrib/dirspec/__init__.py (+16/-0)
contrib/dirspec/basedir.py (+159/-0)
contrib/dirspec/utils.py (+188/-0)
contrib/u1trial (+40/-0)
data/dbus-session.conf.in (+63/-0)
dependencies-devel.txt (+0/-3)
dependencies.txt (+5/-8)
requirements-devel.txt (+2/-0)
requirements.txt (+6/-1)
run-tests (+1/-1)
setup.py (+14/-37)
ubuntuone/devtools/__init__.py (+1/-0)
ubuntuone/devtools/compat.py (+49/-0)
ubuntuone/devtools/errors.py (+35/-0)
ubuntuone/devtools/handlers.py (+97/-0)
ubuntuone/devtools/reactors/__init__.py (+1/-0)
ubuntuone/devtools/reactors/gi.py (+53/-0)
ubuntuone/devtools/runners/__init__.py (+304/-0)
ubuntuone/devtools/runners/txrunner.py (+133/-0)
ubuntuone/devtools/services/__init__.py (+66/-0)
ubuntuone/devtools/services/dbus.py (+121/-0)
ubuntuone/devtools/testcases/__init__.py (+187/-0)
ubuntuone/devtools/testcases/dbus.py (+138/-0)
ubuntuone/devtools/testcases/txsocketserver.py (+357/-0)
ubuntuone/devtools/testing/__init__.py (+1/-0)
ubuntuone/devtools/testing/txcheck.py (+381/-0)
ubuntuone/devtools/testing/txwebserver.py (+124/-0)
ubuntuone/devtools/utils.py (+180/-0)
To merge this branch: bzr merge lp:~facundo/magicicada-client/all-venv
Reviewer Review Type Date Requested Status
chicharreros Pending
Review via email: mp+347169@code.launchpad.net

Commit message

Full virtualenv.

To post a comment you must log in.

Unmerged revisions

1445. By Facundo Batista

Full virtualenv.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2018-04-14 23:34:20 +0000
3+++ Makefile 2018-05-30 21:37:37 +0000
4@@ -43,7 +43,7 @@
5 cat dependencies-devel.txt | xargs apt-get install -y --no-install-recommends
6
7 venv:
8- virtualenv --system-site-packages $(ENV)
9+ virtualenv $(ENV)
10 $(ENV)/bin/pip install -r requirements.txt -r requirements-devel.txt
11
12 lint:
13
14=== added directory 'contrib/dirspec'
15=== added file 'contrib/dirspec/__init__.py'
16--- contrib/dirspec/__init__.py 1970-01-01 00:00:00 +0000
17+++ contrib/dirspec/__init__.py 2018-05-30 21:37:37 +0000
18@@ -0,0 +1,16 @@
19+# -*- coding: utf-8 -*-
20+#
21+# Copyright 2011 Canonical Ltd.
22+#
23+# This program is free software: you can redistribute it and/or modify
24+# it under the terms of the GNU Lesser General Public License version 3
25+# as published by the Free Software Foundation.
26+#
27+# This program is distributed in the hope that it will be useful,
28+# but WITHOUT ANY WARRANTY; without even the implied warranty of
29+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30+# GNU Lesser General Public License for more details.
31+#
32+# You should have received a copy of the GNU Lesser General Public License
33+# along with this program. If not, see <http://www.gnu.org/licenses/>.
34+"""dirspec package."""
35
36=== added file 'contrib/dirspec/basedir.py'
37--- contrib/dirspec/basedir.py 1970-01-01 00:00:00 +0000
38+++ contrib/dirspec/basedir.py 2018-05-30 21:37:37 +0000
39@@ -0,0 +1,159 @@
40+# -*- coding: utf-8 -*-
41+#
42+# Copyright 2011-2012 Canonical Ltd.
43+#
44+# This program is free software: you can redistribute it and/or modify
45+# it under the terms of the GNU Lesser General Public License version 3
46+# as published by the Free Software Foundation.
47+#
48+# This program is distributed in the hope that it will be useful,
49+# but WITHOUT ANY WARRANTY; without even the implied warranty of
50+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
51+# GNU Lesser General Public License for more details.
52+#
53+# You should have received a copy of the GNU Lesser General Public License
54+# along with this program. If not, see <http://www.gnu.org/licenses/>.
55+"""XDG Base Directory paths."""
56+
57+from __future__ import unicode_literals, print_function
58+
59+import os
60+
61+from dirspec.utils import (default_cache_home,
62+ default_config_path, default_config_home,
63+ default_data_path, default_data_home,
64+ get_env_path, unicode_path)
65+
66+__all__ = ['xdg_cache_home',
67+ 'xdg_config_home',
68+ 'xdg_data_home',
69+ 'xdg_config_dirs',
70+ 'xdg_data_dirs',
71+ 'load_config_paths',
72+ 'load_data_paths',
73+ 'load_first_config',
74+ 'save_config_path',
75+ 'save_data_path',
76+ ]
77+
78+
79+def get_xdg_cache_home():
80+ """Get the path for XDG cache directory in user's HOME."""
81+ return get_env_path('XDG_CACHE_HOME', default_cache_home)
82+
83+
84+def get_xdg_config_home():
85+ """Get the path for XDG config directory in user's HOME."""
86+ return get_env_path('XDG_CONFIG_HOME', default_config_home)
87+
88+
89+def get_xdg_data_home():
90+ """Get the path for XDG data directory in user's HOME."""
91+ return get_env_path('XDG_DATA_HOME', default_data_home)
92+
93+
94+def get_xdg_config_dirs():
95+ """Get the paths for the XDG config directories."""
96+ result = [get_xdg_config_home()]
97+ result.extend([x.encode('utf-8') for x in get_env_path(
98+ 'XDG_CONFIG_DIRS',
99+ default_config_path).decode('utf-8').split(os.pathsep)])
100+ return result
101+
102+
103+def get_xdg_data_dirs():
104+ """Get the paths for the XDG data directories."""
105+ result = [get_xdg_data_home()]
106+ result.extend([x.encode('utf-8') for x in get_env_path(
107+ 'XDG_DATA_DIRS',
108+ default_data_path).decode('utf-8').split(os.pathsep)])
109+ return result
110+
111+
112+def load_config_paths(*resource):
113+ """Iterator of configuration paths.
114+
115+ Return an iterator which gives each directory named 'resource' in
116+ the configuration search path. Information provided by earlier
117+ directories should take precedence over later ones (ie, the user's
118+ config dir comes first).
119+ """
120+ resource = os.path.join(*resource)
121+ assert not resource.startswith('/')
122+ for config_dir in get_xdg_config_dirs():
123+ path = os.path.join(config_dir, resource.encode('utf-8'))
124+ # access the file system always with unicode
125+ # to properly behave in some operating systems
126+ if os.path.exists(unicode_path(path)):
127+ yield path
128+
129+
130+def load_data_paths(*resource):
131+ """Iterator of data paths.
132+
133+ Return an iterator which gives each directory named 'resource' in
134+ the stored data search path. Information provided by earlier
135+ directories should take precedence over later ones.
136+ """
137+ resource = os.path.join(*resource)
138+ assert not resource.startswith('/')
139+ for data_dir in get_xdg_data_dirs():
140+ path = os.path.join(data_dir, resource.encode('utf-8'))
141+ # access the file system always with unicode
142+ # to properly behave in some operating systems
143+ if os.path.exists(unicode_path(path)):
144+ yield path
145+
146+
147+def load_first_config(*resource):
148+ """Returns the first result from load_config_paths, or None if nothing
149+ is found to load.
150+ """
151+ for path in load_config_paths(*resource):
152+ return path
153+ return None
154+
155+
156+def save_config_path(*resource):
157+ """Path to save configuration.
158+
159+ Ensure $XDG_CONFIG_HOME/<resource>/ exists, and return its path.
160+ 'resource' should normally be the name of your application. Use this
161+ when SAVING configuration settings. Use the xdg_config_dirs variable
162+ for loading.
163+ """
164+ resource = os.path.join(*resource)
165+ assert not resource.startswith('/')
166+ path = os.path.join(get_xdg_config_home(), resource.encode('utf-8'))
167+ # access the file system always with unicode
168+ # to properly behave in some operating systems
169+ if not os.path.isdir(unicode_path(path)):
170+ os.makedirs(unicode_path(path), 0o700)
171+ return path
172+
173+
174+def save_data_path(*resource):
175+ """Path to save data.
176+
177+ Ensure $XDG_DATA_HOME/<resource>/ exists, and return its path.
178+ 'resource' should normally be the name of your application. Use this
179+ when STORING a resource. Use the xdg_data_dirs variable for loading.
180+ """
181+ resource = os.path.join(*resource)
182+ assert not resource.startswith('/')
183+ path = os.path.join(get_xdg_data_home(), resource.encode('utf-8'))
184+ # access the file system always with unicode
185+ # to properly behave in some operating systems
186+ if not os.path.isdir(unicode_path(path)):
187+ os.makedirs(unicode_path(path), 0o700)
188+ return path
189+
190+
191+# pylint: disable=C0103
192+xdg_cache_home = get_xdg_cache_home()
193+xdg_config_home = get_xdg_config_home()
194+xdg_data_home = get_xdg_data_home()
195+
196+xdg_config_dirs = [x for x in get_xdg_config_dirs() if x]
197+xdg_data_dirs = [x for x in get_xdg_data_dirs() if x]
198+# pylint: disable=C0103
199
200=== added file 'contrib/dirspec/utils.py'
201--- contrib/dirspec/utils.py 1970-01-01 00:00:00 +0000
202+++ contrib/dirspec/utils.py 2018-05-30 21:37:37 +0000
203@@ -0,0 +1,188 @@
204+# -*- coding: utf-8 -*-
205+#
206+# Copyright 2011-2012 Canonical Ltd.
207+#
208+# This program is free software: you can redistribute it and/or modify
209+# it under the terms of the GNU Lesser General Public License version 3
210+# as published by the Free Software Foundation.
211+#
212+# This program is distributed in the hope that it will be useful,
213+# but WITHOUT ANY WARRANTY; without even the implied warranty of
214+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
215+# GNU Lesser General Public License for more details.
216+#
217+# You should have received a copy of the GNU Lesser General Public License
218+# along with this program. If not, see <http://www.gnu.org/licenses/>.
219+"""Utilities for multiplatform support of XDG directory handling."""
220+
221+from __future__ import unicode_literals, print_function
222+
223+import errno
224+import os
225+import sys
226+
227+__all__ = ['user_home',
228+ 'default_cache_home',
229+ 'default_config_home',
230+ 'default_config_path',
231+ 'default_data_home',
232+ 'default_data_path',
233+ 'get_env_path',
234+ 'get_program_path',
235+ 'unicode_path',
236+ ]
237+
238+
239+def _get_exe_path_frozen_win32(exe_name):
240+ """Get path to the helper .exe on packaged windows."""
241+ # all the .exes are in the same place on windows:
242+ cur_exec_path = os.path.abspath(sys.executable)
243+ exe_dir = os.path.dirname(cur_exec_path)
244+ return os.path.join(exe_dir, exe_name + ".exe")
245+
246+
247+def _get_exe_path_frozen_darwin(exe_name, app_names):
248+ """Get path to the sub-app executable on packaged darwin."""
249+
250+ sub_app_name = app_names[exe_name]
251+ main_app_dir = "".join(__file__.partition(".app")[:-1])
252+ main_app_resources_dir = os.path.join(main_app_dir,
253+ "Contents",
254+ "Resources")
255+ exe_bin = os.path.join(main_app_resources_dir,
256+ sub_app_name,
257+ "Contents", "MacOS",
258+ exe_name)
259+ return exe_bin
260+
261+
262+def get_program_path(program_name, *args, **kwargs):
263+ """Given a program name, returns the path to run that program.
264+
265+ Raises OSError if the program is not found.
266+
267+ :param program_name: The name of the program to find. For darwin and win32
268+ platforms, the behavior is changed slightly, when sys.frozen is set,
269+ to look in the packaged program locations for the program.
270+ :param search_dirs: A list of directories to look for the program in. This
271+ is only available as a keyword argument.
272+ :param app_names: A dict of program names mapped to sub-app names. Used
273+ for discovering paths in embedded .app bundles on the darwin platform.
274+ This is only available as a keyword argument.
275+ :return: The path to the discovered program.
276+ """
277+ search_dirs = kwargs.get('fallback_dirs', None)
278+ app_names = kwargs.get('app_names', None)
279+
280+ if getattr(sys, "frozen", None) is not None:
281+ if sys.platform == 'win32':
282+ program_path = _get_exe_path_frozen_win32(program_name)
283+ elif sys.platform == 'darwin':
284+ program_path = _get_exe_path_frozen_darwin(program_name,
285+ app_names)
286+ else:
287+ raise Exception("Unsupported platform for frozen execution: %r" %
288+ sys.platform)
289+ else:
290+ if search_dirs is not None:
291+ for dirname in search_dirs:
292+ program_path = os.path.join(dirname, program_name)
293+ if os.path.exists(program_path):
294+ return program_path
295+ else:
296+ # Check in normal system $PATH, if no fallback dirs specified
297+ from distutils.spawn import find_executable
298+ program_path = find_executable(program_name)
299+
300+ if program_path is None or not os.path.exists(program_path):
301+ raise OSError(errno.ENOENT,
302+ "Could not find executable %r" % program_name)
303+
304+ return program_path
305+
306+
307+def get_env_path(key, default):
308+ """Get a UTF-8 encoded path from an environment variable."""
309+ if key in os.environ:
310+ # on windows, environment variables are mbcs bytes
311+ # so we must turn them into utf-8 Syncdaemon paths
312+ try:
313+ path = os.environb.get(key.encode('utf-8'))
314+ except AttributeError:
315+ path = os.environ[key]
316+ return path.decode(sys.getfilesystemencoding()).encode('utf-8')
317+ else:
318+ if not isinstance(default, bytes):
319+ return default.encode('utf-8')
320+ return default
321+
322+
323+def unicode_path(utf8path):
324+ """Turn an utf8 path into a unicode path."""
325+ if isinstance(utf8path, bytes):
326+ return utf8path.decode("utf-8")
327+ return utf8path
328+
329+
330+def get_special_folders():
331+ """ Routine to grab all the Windows Special Folders locations.
332+
333+ If successful, returns dictionary
334+ of shell folder locations indexed on Windows keyword for each;
335+ otherwise, returns an empty dictionary.
336+ """
337+ # pylint: disable=W0621, F0401, E0611
338+ special_folders = {}
339+
340+ if sys.platform == 'win32':
341+ from win32com.shell import shell, shellcon
342+ # CSIDL_LOCAL_APPDATA = C:\Users\<username>\AppData\Local
343+ # CSIDL_PROFILE = C:\Users\<username>
344+ # CSIDL_COMMON_APPDATA = C:\ProgramData
345+ # More information on these constants at
346+ # http://msdn.microsoft.com/en-us/library/bb762494
347+
348+ # per http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181,
349+ # SHGetFolderPath is deprecated, replaced by SHGetKnownFolderPath
350+ # (http://msdn.microsoft.com/en-us/library/windows/desktop/bb762188)
351+ def get_path(name):
352+ return shell.SHGetFolderPath(0, getattr(shellcon, name),
353+ None, 0).encode('utf8')
354+ special_folders['Personal'] = get_path("CSIDL_PROFILE")
355+ special_folders['Local AppData'] = get_path("CSIDL_LOCAL_APPDATA")
356+ special_folders['AppData'] = os.path.dirname(
357+ special_folders['Local AppData'])
358+ special_folders['Common AppData'] = get_path("CSIDL_COMMON_APPDATA")
359+
360+ return special_folders
361+
362+
363+# pylint: disable=C0103
364+if sys.platform == 'win32':
365+ special_folders = get_special_folders()
366+ user_home = special_folders['Personal']
367+ default_config_path = special_folders['Common AppData']
368+ default_config_home = special_folders['Local AppData']
369+ default_data_path = os.path.join(default_config_path, b'xdg')
370+ default_data_home = os.path.join(default_config_home, b'xdg')
371+ default_cache_home = os.path.join(default_data_home, b'cache')
372+elif sys.platform == 'darwin':
373+ user_home = os.path.expanduser(b'~')
374+ default_cache_home = os.path.join(user_home, b'Library', b'Caches')
375+ default_config_path = b'/Library/Preferences:/etc/xdg'
376+ default_config_home = os.path.join(user_home, b'Library', b'Preferences')
377+ default_data_path = b':'.join([b'/Library/Application Support',
378+ b'/usr/local/share',
379+ b'/usr/share'])
380+ default_data_home = os.path.join(user_home, b'Library',
381+ b'Application Support')
382+else:
383+ user_home = os.path.expanduser(b'~')
384+ default_cache_home = os.path.join(user_home,
385+ b'.cache')
386+ default_config_path = b'/etc/xdg'
387+ default_config_home = os.path.join(user_home,
388+ b'.config')
389+ default_data_path = b'/usr/local/share:/usr/share'
390+ default_data_home = os.path.join(user_home,
391+ b'.local', b'share')
392
393=== added file 'contrib/u1trial'
394--- contrib/u1trial 1970-01-01 00:00:00 +0000
395+++ contrib/u1trial 2018-05-30 21:37:37 +0000
396@@ -0,0 +1,40 @@
397+#! /usr/bin/python
398+#
399+# Copyright 2009-2012 Canonical Ltd.
400+#
401+# This program is free software: you can redistribute it and/or modify it
402+# under the terms of the GNU General Public License version 3, as published
403+# by the Free Software Foundation.
404+#
405+# This program is distributed in the hope that it will be useful, but
406+# WITHOUT ANY WARRANTY; without even the implied warranties of
407+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
408+# PURPOSE. See the GNU General Public License for more details.
409+#
410+# You should have received a copy of the GNU General Public License along
411+# with this program. If not, see <http://www.gnu.org/licenses/>.
412+#
413+# In addition, as a special exception, the copyright holders give
414+# permission to link the code of portions of this program with the
415+# OpenSSL library under certain conditions as described in each
416+# individual source file, and distribute linked combinations
417+# including the two.
418+# You must obey the GNU General Public License in all respects
419+# for all of the code used other than OpenSSL. If you modify
420+# file(s) with this exception, you may extend this exception to your
421+# version of the file(s), but you are not obligated to do so. If you
422+# do not wish to do so, delete this exception statement from your
423+# version. If you delete this exception statement from all source
424+# files in the program, then also delete it here.
425+"""Test runner which works with special services and main loops."""
426+
427+import os
428+import sys
429+
430+sys.path.insert(0, os.path.abspath("."))
431+
432+from ubuntuone.devtools.runners import main # noqa
433+
434+
435+if __name__ == '__main__':
436+ main()
437
438=== added file 'data/dbus-session.conf.in'
439--- data/dbus-session.conf.in 1970-01-01 00:00:00 +0000
440+++ data/dbus-session.conf.in 2018-05-30 21:37:37 +0000
441@@ -0,0 +1,63 @@
442+<!-- This configuration file controls our test-only session bus -->
443+
444+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
445+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
446+<busconfig>
447+ <!-- We only use a session bus -->
448+ <type>session</type>
449+
450+ <listen>@ADDRESS@</listen>
451+
452+ <!-- Load our own services.
453+ To make other dbus service in this session bus, just add another servicedir entry. -->
454+ <servicedir>dbus-session</servicedir>
455+ <!-- Load the standard session services -->
456+ <!--standard_session_servicedirs /-->
457+
458+ <policy context="default">
459+ <!-- Allow everything to be sent -->
460+ <allow send_destination="*" eavesdrop="true"/>
461+ <!-- Allow everything to be received -->
462+ <allow eavesdrop="true"/>
463+ <!-- Allow anyone to own anything -->
464+ <allow own="*"/>
465+ </policy>
466+
467+ <!-- Config files are placed here that among other things,
468+ further restrict the above policy for specific services. -->
469+ <includedir>/etc/dbus-1/session.d</includedir>
470+
471+ <!-- raise the service start timeout to 40 seconds as it can timeout
472+ on the live cd on slow machines -->
473+ <limit name="service_start_timeout">60000</limit>
474+
475+ <!-- This is included last so local configuration can override what's
476+ in this standard file -->
477+ <include ignore_missing="yes">session-local.conf</include>
478+
479+ <include ignore_missing="yes" if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
480+
481+ <!-- For the session bus, override the default relatively-low limits
482+ with essentially infinite limits, since the bus is just running
483+ as the user anyway, using up bus resources is not something we need
484+ to worry about. In some cases, we do set the limits lower than
485+ "all available memory" if exceeding the limit is almost certainly a bug,
486+ having the bus enforce a limit is nicer than a huge memory leak. But the
487+ intent is that these limits should never be hit. -->
488+
489+ <!-- the memory limits are 1G instead of say 4G because they can't exceed 32-bit signed int max -->
490+ <limit name="max_incoming_bytes">1000000000</limit>
491+ <limit name="max_outgoing_bytes">1000000000</limit>
492+ <limit name="max_message_size">1000000000</limit>
493+ <limit name="service_start_timeout">120000</limit>
494+ <limit name="auth_timeout">240000</limit>
495+ <limit name="max_completed_connections">100000</limit>
496+ <limit name="max_incomplete_connections">10000</limit>
497+ <limit name="max_connections_per_user">100000</limit>
498+ <limit name="max_pending_service_starts">10000</limit>
499+ <limit name="max_names_per_connection">50000</limit>
500+ <limit name="max_match_rules_per_connection">50000</limit>
501+ <limit name="max_replies_per_connection">50000</limit>
502+ <limit name="reply_timeout">300000</limit>
503+
504+</busconfig>
505
506=== modified file 'dependencies-devel.txt'
507--- dependencies-devel.txt 2018-03-18 11:59:08 +0000
508+++ dependencies-devel.txt 2018-05-30 21:37:37 +0000
509@@ -1,5 +1,2 @@
510 bzr
511 make
512-python-mocker
513-ubuntuone-dev-tools
514-virtualenv
515
516=== modified file 'dependencies.txt'
517--- dependencies.txt 2018-04-14 23:34:20 +0000
518+++ dependencies.txt 2018-05-30 21:37:37 +0000
519@@ -1,8 +1,5 @@
520-gir1.2-soup-2.4
521-python-configglue
522-python-dirspec
523-python-distutils-extra
524-python-gi
525-python-protobuf
526-python-pyinotify
527-python-twisted
528+libgirepository1.0-dev
529+libgtk2.0-dev
530+pkg-config
531+python-dev
532+virtualenv
533
534=== modified file 'requirements-devel.txt'
535--- requirements-devel.txt 2018-03-18 15:03:09 +0000
536+++ requirements-devel.txt 2018-05-30 21:37:37 +0000
537@@ -1,1 +1,3 @@
538+coverage==3.7.1
539 flake8==3.5.0
540+mocker==1.1.1
541
542=== modified file 'requirements.txt'
543--- requirements.txt 2018-04-14 23:34:20 +0000
544+++ requirements.txt 2018-05-30 21:37:37 +0000
545@@ -1,2 +1,7 @@
546+configglue==1.1.3.post0
547+dbus-python==1.2.8
548+magicicadaprotocol==2.0
549+PyGObject==3.28.2
550+pyinotify==0.9.6
551 Send2Trash==1.5.0
552-magicicadaprotocol==2.0
553+Twisted==18.4.0
554
555=== modified file 'run-tests'
556--- run-tests 2018-03-18 11:59:08 +0000
557+++ run-tests 2018-05-30 21:37:37 +0000
558@@ -49,5 +49,5 @@
559
560 echo "*** Running test suite for ""$MODULE"" ***"
561 export SSL_CERTIFICATES_DIR=/etc/ssl/certs
562-.env/bin/python /usr/bin/u1trial -i "$IGNORE_FILES" -p "$IGNORE_PATHS" $MODULE
563+.env/bin/python contrib/u1trial -i "$IGNORE_FILES" -p "$IGNORE_PATHS" $MODULE
564 rm -rf _trial_temp
565
566=== modified file 'setup.py'
567--- setup.py 2018-04-23 02:21:03 +0000
568+++ setup.py 2018-05-30 21:37:37 +0000
569@@ -32,15 +32,9 @@
570 import os
571 import sys
572
573-try:
574- from DistUtilsExtra.command import build_extra, build_i18n
575- import DistUtilsExtra.auto
576-except ImportError:
577- print >> sys.stderr, 'To build this program you need '\
578- 'https://launchpad.net/python-distutils-extra'
579- raise
580-assert DistUtilsExtra.auto.__version__ >= '2.18', \
581- 'needs DistUtilsExtra.auto >= 2.18'
582+from setuptools import setup
583+from setuptools.command.install import install
584+from distutils.command import build, clean
585
586
587 PROJECT_NAME = 'magicicada-client'
588@@ -83,7 +77,7 @@
589 out_file.write(content)
590
591
592-class Install(DistUtilsExtra.auto.install_auto):
593+class Install(install):
594 """Class to install proper files."""
595
596 def run(self):
597@@ -110,7 +104,8 @@
598 prefix = self.install_data.replace(
599 self.root if self.root is not None else '', '')
600 replace_variables(SERVICE_FILES, prefix)
601- DistUtilsExtra.auto.install_auto.run(self)
602+ install.run(self)
603+
604 # Replace the CLIENTDEFS paths here, so that we can do it directly in
605 # the installed copy, rather than the lcoal copy. This allows us to
606 # have a semi-generated version for use in tests, and a full version
607@@ -127,7 +122,7 @@
608 out_file.write(content)
609
610
611-class Build(build_extra.build_extra):
612+class Build(build.build):
613 """Build PyQt (.ui) files and resources."""
614
615 description = "build PyQt GUIs (.ui) and resources (.qrc)"
616@@ -135,10 +130,10 @@
617 def run(self):
618 """Execute the command."""
619 replace_variables(BUILD_FILES)
620- build_extra.build_extra.run(self)
621-
622-
623-class Clean(DistUtilsExtra.auto.clean_build_tree):
624+ build.build.run(self)
625+
626+
627+class Clean(clean.clean):
628 """Class to clean up after the build."""
629
630 def run(self):
631@@ -147,24 +142,7 @@
632 if os.path.exists(built_file):
633 os.unlink(built_file)
634
635- DistUtilsExtra.auto.clean_build_tree.run(self)
636-
637-
638-class BuildLocale(build_i18n.build_i18n):
639- """Work around a bug in DistUtilsExtra."""
640-
641- def run(self):
642- """Magic."""
643- build_i18n.build_i18n.run(self)
644- i = 0
645- for df in self.distribution.data_files:
646- if df[0].startswith('etc/xdg/'):
647- if sys.platform not in ('darwin', 'win32'):
648- new_df = (df[0].replace('etc/xdg/', '/etc/xdg/'), df[1])
649- self.distribution.data_files[i] = new_df
650- else:
651- self.distribution.data_files.pop(i)
652- i += 1
653+ clean.clean.run(self)
654
655
656 def set_py2exe_paths():
657@@ -191,10 +169,9 @@
658
659
660 cmdclass = {
661- 'install': Install,
662 'build': Build,
663 'clean': Clean,
664- 'build_i18n': BuildLocale,
665+ 'install': Install,
666 }
667
668 bin_scripts = [
669@@ -236,7 +213,7 @@
670 scripts.extend(bin_scripts)
671 extra = {}
672
673-DistUtilsExtra.auto.setup(
674+setup(
675 name=PROJECT_NAME,
676 version=VERSION,
677 license='GPL v3',
678
679=== added directory 'ubuntuone/devtools'
680=== added file 'ubuntuone/devtools/__init__.py'
681--- ubuntuone/devtools/__init__.py 1970-01-01 00:00:00 +0000
682+++ ubuntuone/devtools/__init__.py 2018-05-30 21:37:37 +0000
683@@ -0,0 +1,1 @@
684+"""Testing utilities for Ubuntu One client code."""
685
686=== added file 'ubuntuone/devtools/compat.py'
687--- ubuntuone/devtools/compat.py 1970-01-01 00:00:00 +0000
688+++ ubuntuone/devtools/compat.py 2018-05-30 21:37:37 +0000
689@@ -0,0 +1,49 @@
690+# -*- coding: utf-8 -*-
691+#
692+# Copyright 2012 Canonical Ltd.
693+#
694+# This program is free software: you can redistribute it and/or modify it
695+# under the terms of the GNU General Public License version 3, as published
696+# by the Free Software Foundation.
697+#
698+# This program is distributed in the hope that it will be useful, but
699+# WITHOUT ANY WARRANTY; without even the implied warranties of
700+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
701+# PURPOSE. See the GNU General Public License for more details.
702+#
703+# You should have received a copy of the GNU General Public License along
704+# with this program. If not, see <http://www.gnu.org/licenses/>.
705+#
706+# In addition, as a special exception, the copyright holders give
707+# permission to link the code of portions of this program with the
708+# OpenSSL library under certain conditions as described in each
709+# individual source file, and distribute linked combinations
710+# including the two.
711+# You must obey the GNU General Public License in all respects
712+# for all of the code used other than OpenSSL. If you modify
713+# file(s) with this exception, you may extend this exception to your
714+# version of the file(s), but you are not obligated to do so. If you
715+# do not wish to do so, delete this exception statement from your
716+# version. If you delete this exception statement from all source
717+# files in the program, then also delete it here.
718+"""Python 2 and 3 compatibility."""
719+
720+from __future__ import unicode_literals
721+
722+# The following approach was outlined in Lennart Regebro's
723+# "Porting to Python 3" book.
724+# http://python3porting.com/noconv.html#more-bytes-strings-and-unicode
725+
726+import sys
727+
728+# Disable redefined builtin, invalid name warning
729+# pylint: disable=W0622,C0103
730+
731+if sys.version_info < (3,):
732+ text_type = unicode
733+ binary_type = str
734+ basestring = basestring
735+else:
736+ text_type = str
737+ binary_type = bytes
738+ basestring = str
739
740=== added file 'ubuntuone/devtools/errors.py'
741--- ubuntuone/devtools/errors.py 1970-01-01 00:00:00 +0000
742+++ ubuntuone/devtools/errors.py 2018-05-30 21:37:37 +0000
743@@ -0,0 +1,35 @@
744+# Copyright 2012 Canonical Ltd.
745+#
746+# This program is free software: you can redistribute it and/or modify it
747+# under the terms of the GNU General Public License version 3, as published
748+# by the Free Software Foundation.
749+#
750+# This program is distributed in the hope that it will be useful, but
751+# WITHOUT ANY WARRANTY; without even the implied warranties of
752+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
753+# PURPOSE. See the GNU General Public License for more details.
754+#
755+# You should have received a copy of the GNU General Public License along
756+# with this program. If not, see <http://www.gnu.org/licenses/>.
757+#
758+# In addition, as a special exception, the copyright holders give
759+# permission to link the code of portions of this program with the
760+# OpenSSL library under certain conditions as described in each
761+# individual source file, and distribute linked combinations
762+# including the two.
763+# You must obey the GNU General Public License in all respects
764+# for all of the code used other than OpenSSL. If you modify
765+# file(s) with this exception, you may extend this exception to your
766+# version of the file(s), but you are not obligated to do so. If you
767+# do not wish to do so, delete this exception statement from your
768+# version. If you delete this exception statement from all source
769+# files in the program, then also delete it here.
770+"""Custom error types for Ubuntu One developer tools."""
771+
772+
773+class TestError(Exception):
774+ """An error occurred in attempting to load or start the tests."""
775+
776+
777+class UsageError(Exception):
778+ """An error occurred in parsing the command line arguments."""
779
780=== added file 'ubuntuone/devtools/handlers.py'
781--- ubuntuone/devtools/handlers.py 1970-01-01 00:00:00 +0000
782+++ ubuntuone/devtools/handlers.py 2018-05-30 21:37:37 +0000
783@@ -0,0 +1,97 @@
784+# -*- coding: utf-8 -*-
785+
786+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
787+# Author: Facundo Batista <facundo@canonical.com>
788+#
789+# Copyright 2009-2012 Canonical Ltd.
790+#
791+# This program is free software: you can redistribute it and/or modify it
792+# under the terms of the GNU General Public License version 3, as published
793+# by the Free Software Foundation.
794+#
795+# This program is distributed in the hope that it will be useful, but
796+# WITHOUT ANY WARRANTY; without even the implied warranties of
797+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
798+# PURPOSE. See the GNU General Public License for more details.
799+#
800+# You should have received a copy of the GNU General Public License along
801+# with this program. If not, see <http://www.gnu.org/licenses/>.
802+#
803+# In addition, as a special exception, the copyright holders give
804+# permission to link the code of portions of this program with the
805+# OpenSSL library under certain conditions as described in each
806+# individual source file, and distribute linked combinations
807+# including the two.
808+# You must obey the GNU General Public License in all respects
809+# for all of the code used other than OpenSSL. If you modify
810+# file(s) with this exception, you may extend this exception to your
811+# version of the file(s), but you are not obligated to do so. If you
812+# do not wish to do so, delete this exception statement from your
813+# version. If you delete this exception statement from all source
814+# files in the program, then also delete it here.
815+"""Set of helpers handlers."""
816+
817+from __future__ import print_function
818+
819+import logging
820+
821+
822+class MementoHandler(logging.Handler):
823+ """ A handler class which store logging records in a list """
824+
825+ def __init__(self, *args, **kwargs):
826+ """ Create the instance, and add a records attribute. """
827+ logging.Handler.__init__(self, *args, **kwargs)
828+ self.records = []
829+ self.debug = False
830+
831+ def emit(self, record):
832+ """ Just add the record to self.records. """
833+ self.format(record)
834+ self.records.append(record)
835+
836+ def dump_contents(self, msgs):
837+ """Dumps the contents of the MementoHandler."""
838+ if self.debug:
839+ print("Expecting:")
840+ for msg in msgs:
841+ print("\t", msg)
842+ print("MementoHandler contents:")
843+ for rec in self.records:
844+ print("\t", rec.exc_info)
845+ print("\t", logging.getLevelName(rec.levelno))
846+ print("\t\t", rec.message)
847+ print("\t\t", rec.exc_text)
848+
849+ def check(self, level, *msgs):
850+ """Verifies that the msgs are logged in the specified level"""
851+ for rec in self.records:
852+ if rec.levelno == level and all(m in rec.message for m in msgs):
853+ return rec
854+ self.dump_contents(msgs)
855+ return False
856+
857+ def check_debug(self, *msgs):
858+ """Shortcut for checking in DEBUG."""
859+ return self.check(logging.DEBUG, *msgs)
860+
861+ def check_info(self, *msgs):
862+ """Shortcut for checking in INFO."""
863+ return self.check(logging.INFO, *msgs)
864+
865+ def check_warning(self, *msgs):
866+ """Shortcut for checking in WARNING."""
867+ return self.check(logging.WARNING, *msgs)
868+
869+ def check_error(self, *msgs):
870+ """Shortcut for checking in ERROR."""
871+ return self.check(logging.ERROR, *msgs)
872+
873+ def check_exception(self, exception_info, *msgs):
874+ """Shortcut for checking exceptions."""
875+ for rec in self.records:
876+ if rec.levelno == logging.ERROR and \
877+ all(m in rec.exc_text + rec.message for m in msgs) and \
878+ exception_info in rec.exc_info:
879+ return True
880+ return False
881
882=== added directory 'ubuntuone/devtools/reactors'
883=== added file 'ubuntuone/devtools/reactors/__init__.py'
884--- ubuntuone/devtools/reactors/__init__.py 1970-01-01 00:00:00 +0000
885+++ ubuntuone/devtools/reactors/__init__.py 2018-05-30 21:37:37 +0000
886@@ -0,0 +1,1 @@
887+"""Twisted reactors for testing."""
888
889=== added file 'ubuntuone/devtools/reactors/gi.py'
890--- ubuntuone/devtools/reactors/gi.py 1970-01-01 00:00:00 +0000
891+++ ubuntuone/devtools/reactors/gi.py 2018-05-30 21:37:37 +0000
892@@ -0,0 +1,53 @@
893+# Copyright 2009-2012 Canonical Ltd.
894+#
895+# This program is free software: you can redistribute it and/or modify it
896+# under the terms of the GNU General Public License version 3, as published
897+# by the Free Software Foundation.
898+#
899+# This program is distributed in the hope that it will be useful, but
900+# WITHOUT ANY WARRANTY; without even the implied warranties of
901+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
902+# PURPOSE. See the GNU General Public License for more details.
903+#
904+# You should have received a copy of the GNU General Public License along
905+# with this program. If not, see <http://www.gnu.org/licenses/>.
906+#
907+# In addition, as a special exception, the copyright holders give
908+# permission to link the code of portions of this program with the
909+# OpenSSL library under certain conditions as described in each
910+# individual source file, and distribute linked combinations
911+# including the two.
912+# You must obey the GNU General Public License in all respects
913+# for all of the code used other than OpenSSL. If you modify
914+# file(s) with this exception, you may extend this exception to your
915+# version of the file(s), but you are not obligated to do so. If you
916+# do not wish to do so, delete this exception statement from your
917+# version. If you delete this exception statement from all source
918+# files in the program, then also delete it here.
919+"""The introspection based main loop integration reactor for testing."""
920+
921+REACTOR_URL = 'http://twistedmatrix.com/trac/ticket/4558'
922+
923+
924+def load_reactor(reactor_name=None):
925+ """Load the reactor module and return it."""
926+ return __import__(reactor_name, None, None, [''])
927+
928+
929+def install(options=None):
930+ """Install the reactor and parse any options we might need."""
931+ reactor = None
932+ if options is not None and options['gui']:
933+ try:
934+ reactor = load_reactor('twisted.internet.gtk3reactor')
935+ except ImportError:
936+ print('Falling back to gtk2reactor module.')
937+ reactor = load_reactor('twisted.internet.gtk2reactor')
938+ else:
939+ try:
940+ reactor = load_reactor('twisted.internet.gireactor')
941+ except ImportError:
942+ print('Falling back to glib2reactor module.')
943+ reactor = load_reactor('twisted.internet.glib2reactor')
944+
945+ reactor.install()
946
947=== added directory 'ubuntuone/devtools/runners'
948=== added file 'ubuntuone/devtools/runners/__init__.py'
949--- ubuntuone/devtools/runners/__init__.py 1970-01-01 00:00:00 +0000
950+++ ubuntuone/devtools/runners/__init__.py 2018-05-30 21:37:37 +0000
951@@ -0,0 +1,304 @@
952+# Copyright 2009-2012 Canonical Ltd.
953+#
954+# This program is free software: you can redistribute it and/or modify it
955+# under the terms of the GNU General Public License version 3, as published
956+# by the Free Software Foundation.
957+#
958+# This program is distributed in the hope that it will be useful, but
959+# WITHOUT ANY WARRANTY; without even the implied warranties of
960+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
961+# PURPOSE. See the GNU General Public License for more details.
962+#
963+# You should have received a copy of the GNU General Public License along
964+# with this program. If not, see <http://www.gnu.org/licenses/>.
965+#
966+# In addition, as a special exception, the copyright holders give
967+# permission to link the code of portions of this program with the
968+# OpenSSL library under certain conditions as described in each
969+# individual source file, and distribute linked combinations
970+# including the two.
971+# You must obey the GNU General Public License in all respects
972+# for all of the code used other than OpenSSL. If you modify
973+# file(s) with this exception, you may extend this exception to your
974+# version of the file(s), but you are not obligated to do so. If you
975+# do not wish to do so, delete this exception statement from your
976+# version. If you delete this exception statement from all source
977+# files in the program, then also delete it here.
978+"""The base test runner object."""
979+
980+from __future__ import print_function, unicode_literals
981+
982+import coverage
983+import gc
984+import inspect
985+import os
986+import re
987+import sys
988+import unittest
989+
990+from ubuntuone.devtools.errors import TestError, UsageError
991+from ubuntuone.devtools.testing.txcheck import TXCheckSuite
992+from ubuntuone.devtools.utils import OptionParser
993+from ubuntuone.devtools.compat import text_type
994+
995+__all__ = ['BaseTestOptions', 'BaseTestRunner', 'main']
996+
997+
998+def _is_in_ignored_path(testcase, paths):
999+ """Return if the testcase is in one of the ignored paths."""
1000+ for ignored_path in paths:
1001+ if testcase.startswith(ignored_path):
1002+ return True
1003+ return False
1004+
1005+
1006+class BaseTestRunner(object):
1007+ """The base test runner type. Does not actually run tests."""
1008+
1009+ def __init__(self, options=None, *args, **kwargs):
1010+ super(BaseTestRunner, self).__init__(*args, **kwargs)
1011+
1012+ # set $HOME to the _trial_temp dir, to avoid breaking user files
1013+ trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())
1014+ homedir = os.path.join(trial_temp_dir, options['temp-directory'])
1015+ os.environ['HOME'] = homedir
1016+
1017+ # setup $XDG_*_HOME variables and create the directories
1018+ xdg_cache = os.path.join(homedir, 'xdg_cache')
1019+ xdg_config = os.path.join(homedir, 'xdg_config')
1020+ xdg_data = os.path.join(homedir, 'xdg_data')
1021+ os.environ['XDG_CACHE_HOME'] = xdg_cache
1022+ os.environ['XDG_CONFIG_HOME'] = xdg_config
1023+ os.environ['XDG_DATA_HOME'] = xdg_data
1024+
1025+ if not os.path.exists(xdg_cache):
1026+ os.makedirs(xdg_cache)
1027+ if not os.path.exists(xdg_config):
1028+ os.makedirs(xdg_config)
1029+ if not os.path.exists(xdg_data):
1030+ os.makedirs(xdg_data)
1031+
1032+ # setup the ROOTDIR env var
1033+ os.environ['ROOTDIR'] = os.getcwd()
1034+
1035+ # Need an attribute for tempdir so we can use it later
1036+ self.tempdir = homedir
1037+ self.working_dir = os.path.join(self.tempdir, 'trial')
1038+
1039+ self.source_files = []
1040+ self.required_services = []
1041+
1042+ def _load_unittest(self, relpath):
1043+ """Load unit tests from a Python module with the given 'relpath'."""
1044+ assert relpath.endswith(".py"), (
1045+ "%s does not appear to be a Python module" % relpath)
1046+ if not os.path.basename(relpath).startswith('test_'):
1047+ return
1048+ modpath = relpath.replace(os.path.sep, ".")[:-3]
1049+ module = __import__(modpath, None, None, [""])
1050+
1051+ # If the module specifies required_services, make sure we get them
1052+ members = [x[1] for x in inspect.getmembers(module, inspect.isclass)]
1053+ for member_type in members:
1054+ if hasattr(member_type, 'required_services'):
1055+ member = member_type()
1056+ for service in member.required_services():
1057+ if service not in self.required_services:
1058+ self.required_services.append(service)
1059+ del member
1060+ gc.collect()
1061+
1062+ # If the module has a 'suite' or 'test_suite' function, use that
1063+ # to load the tests.
1064+ if hasattr(module, "suite"):
1065+ return module.suite()
1066+ elif hasattr(module, "test_suite"):
1067+ return module.test_suite()
1068+ else:
1069+ return unittest.defaultTestLoader.loadTestsFromModule(module)
1070+
1071+ def _collect_tests(self, path, test_pattern, ignored_modules,
1072+ ignored_paths):
1073+ """Return the set of unittests."""
1074+ suite = TXCheckSuite()
1075+ if test_pattern:
1076+ pattern = re.compile('.*%s.*' % test_pattern)
1077+ else:
1078+ pattern = None
1079+
1080+ # Disable this lint warning as we need to access _tests in the
1081+ # test suites, to collect the tests
1082+ # pylint: disable=W0212
1083+ if path:
1084+ try:
1085+ module_suite = self._load_unittest(path)
1086+ if pattern:
1087+ for inner_suite in module_suite._tests:
1088+ for test in inner_suite._tests:
1089+ if pattern.match(test.id()):
1090+ suite.addTest(test)
1091+ else:
1092+ suite.addTests(module_suite)
1093+ return suite
1094+ except AssertionError:
1095+ pass
1096+ else:
1097+ raise TestError('Path should be defined.')
1098+
1099+ # We don't use the dirs variable, so ignore the warning
1100+ # pylint: disable=W0612
1101+ for root, dirs, files in os.walk(path):
1102+ for test in files:
1103+ filepath = os.path.join(root, test)
1104+ if test.endswith(".py") and test not in ignored_modules and \
1105+ not _is_in_ignored_path(filepath, ignored_paths):
1106+ self.source_files.append(filepath)
1107+ if test.startswith("test_"):
1108+ module_suite = self._load_unittest(filepath)
1109+ if pattern:
1110+ for inner_suite in module_suite._tests:
1111+ for test in inner_suite._tests:
1112+ if pattern.match(test.id()):
1113+ suite.addTest(test)
1114+ else:
1115+ suite.addTests(module_suite)
1116+ return suite
1117+
1118+ def get_suite(self, config):
1119+ """Get the test suite to use."""
1120+ suite = unittest.TestSuite()
1121+ for path in config['tests']:
1122+ suite.addTest(self._collect_tests(path, config['test'],
1123+ config['ignore-modules'],
1124+ config['ignore-paths']))
1125+ if config['loop']:
1126+ old_suite = suite
1127+ suite = unittest.TestSuite()
1128+ for _ in range(config['loop']):
1129+ suite.addTest(old_suite)
1130+
1131+ return suite
1132+
1133+ def run_tests(self, suite):
1134+ """Run the test suite."""
1135+ return False
1136+
1137+
1138+class BaseTestOptions(OptionParser):
1139+ """Base options for our test runner."""
1140+
1141+ optFlags = [['coverage', 'c', 'Generate a coverage report for the tests.'],
1142+ ['gui', None, 'Use the GUI mode of some runners.'],
1143+ ['help', 'h', ''],
1144+ ['help-runners', None, 'List information about test runners.'],
1145+ ]
1146+
1147+ optParameters = [['test', 't', None, None],
1148+ ['loop', None, 1, None],
1149+ ['ignore-modules', 'i', '', None],
1150+ ['ignore-paths', 'p', '', None],
1151+ ['runner', None, 'txrunner', None],
1152+ ['temp-directory', None, b'_trial_temp', None],
1153+ ]
1154+
1155+ def __init__(self, *args, **kwargs):
1156+ super(BaseTestOptions, self).__init__(*args, **kwargs)
1157+
1158+ def opt_help_runners(self):
1159+ """List the runners which are supported."""
1160+ sys.exit(0)
1161+
1162+ def opt_ignore_modules(self, option):
1163+ """Comma-separate list of test modules to ignore,
1164+ e.g: test_gtk.py, test_account.py
1165+ """
1166+ self['ignore-modules'] = list(map(text_type.strip, option.split(',')))
1167+
1168+ def opt_ignore_paths(self, option):
1169+ """Comma-separated list of relative paths to ignore,
1170+ e.g: tests/platform/windows, tests/platform/macosx
1171+ """
1172+ self['ignore-paths'] = list(map(text_type.strip, option.split(',')))
1173+
1174+ def opt_loop(self, option):
1175+ """Loop tests the specified number of times."""
1176+ try:
1177+ self['loop'] = int(option)
1178+ except ValueError:
1179+ raise UsageError('A positive integer value must be specified.')
1180+
1181+ def opt_temp_directory(self, option):
1182+ """Path to use as a working directory for tests.
1183+ [default: _trial_temp]
1184+ """
1185+ self['temp-directory'] = option
1186+
1187+ def opt_test(self, option):
1188+ """Run specific tests, e.g: className.methodName"""
1189+ self['test'] = option
1190+
1191+ # We use some camelcase names for trial compatibility here.
1192+ def parseArgs(self, *args):
1193+ """Handle the extra arguments."""
1194+ if isinstance(self.tests, set):
1195+ self['tests'].update(args)
1196+ elif isinstance(self.tests, list):
1197+ self['tests'].extend(args)
1198+
1199+
1200+def _get_runner_options(runner_name):
1201+ """Return the test runner module, and its options object."""
1202+ module_name = 'ubuntuone.devtools.runners.%s' % runner_name
1203+ runner = __import__(module_name, None, None, [''])
1204+ options = None
1205+ if getattr(runner, 'TestOptions', None) is not None:
1206+ options = runner.TestOptions()
1207+ if options is None:
1208+ options = BaseTestOptions()
1209+ return (runner, options)
1210+
1211+
1212+def main():
1213+ """Do the deed."""
1214+ if len(sys.argv) == 1:
1215+ sys.argv.append('--help')
1216+
1217+ try:
1218+ pos = sys.argv.index('--runner')
1219+ runner_name = sys.argv.pop(pos + 1)
1220+ sys.argv.pop(pos)
1221+ except ValueError:
1222+ runner_name = 'txrunner'
1223+ finally:
1224+ runner, options = _get_runner_options(runner_name)
1225+ options.parseOptions()
1226+
1227+ test_runner = runner.TestRunner(options=options)
1228+ suite = test_runner.get_suite(options)
1229+
1230+ if options['coverage']:
1231+ coverage.erase()
1232+ coverage.start()
1233+
1234+ running_services = []
1235+
1236+ succeeded = False
1237+ try:
1238+ # Start any required services
1239+ for service_obj in test_runner.required_services:
1240+ service = service_obj()
1241+ service.start_service(tempdir=test_runner.tempdir)
1242+ running_services.append(service)
1243+
1244+ succeeded = test_runner.run_tests(suite)
1245+ finally:
1246+ # Stop all the running services
1247+ for service in running_services:
1248+ service.stop_service()
1249+
1250+ if options['coverage']:
1251+ coverage.stop()
1252+ coverage.report(test_runner.source_files, ignore_errors=True,
1253+ show_missing=False)
1254+
1255+ sys.exit(not succeeded)
1256
1257=== added file 'ubuntuone/devtools/runners/txrunner.py'
1258--- ubuntuone/devtools/runners/txrunner.py 1970-01-01 00:00:00 +0000
1259+++ ubuntuone/devtools/runners/txrunner.py 2018-05-30 21:37:37 +0000
1260@@ -0,0 +1,133 @@
1261+# Copyright 2009-2012 Canonical Ltd.
1262+#
1263+# This program is free software: you can redistribute it and/or modify it
1264+# under the terms of the GNU General Public License version 3, as published
1265+# by the Free Software Foundation.
1266+#
1267+# This program is distributed in the hope that it will be useful, but
1268+# WITHOUT ANY WARRANTY; without even the implied warranties of
1269+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1270+# PURPOSE. See the GNU General Public License for more details.
1271+#
1272+# You should have received a copy of the GNU General Public License along
1273+# with this program. If not, see <http://www.gnu.org/licenses/>.
1274+#
1275+# In addition, as a special exception, the copyright holders give
1276+# permission to link the code of portions of this program with the
1277+# OpenSSL library under certain conditions as described in each
1278+# individual source file, and distribute linked combinations
1279+# including the two.
1280+# You must obey the GNU General Public License in all respects
1281+# for all of the code used other than OpenSSL. If you modify
1282+# file(s) with this exception, you may extend this exception to your
1283+# version of the file(s), but you are not obligated to do so. If you
1284+# do not wish to do so, delete this exception statement from your
1285+# version. If you delete this exception statement from all source
1286+# files in the program, then also delete it here.
1287+"""The twisted test runner and options."""
1288+
1289+from __future__ import print_function, unicode_literals
1290+
1291+import sys
1292+
1293+from twisted.scripts import trial
1294+from twisted.trial.runner import TrialRunner
1295+
1296+from ubuntuone.devtools.errors import TestError
1297+from ubuntuone.devtools.runners import BaseTestOptions, BaseTestRunner
1298+
1299+__all__ = ['TestRunner', 'TestOptions']
1300+
1301+
1302+class TestRunner(BaseTestRunner, TrialRunner):
1303+ """The twisted test runner implementation."""
1304+
1305+ def __init__(self, options=None):
1306+ # Handle running trial in debug or dry-run mode
1307+ self.config = options
1308+
1309+ try:
1310+ reactor_name = ('ubuntuone.devtools.reactors.%s' %
1311+ self.config['reactor'])
1312+ reactor = __import__(reactor_name, None, None, [''])
1313+ except ImportError:
1314+ raise TestError('The specified reactor is not supported.')
1315+ else:
1316+ try:
1317+ reactor.install(options=self.config)
1318+ except ImportError:
1319+ raise TestError(
1320+ 'The Python package providing the requested reactor is '
1321+ 'not installed. You can find it here: %s' %
1322+ reactor.REACTOR_URL)
1323+
1324+ mode = None
1325+ if self.config['debug']:
1326+ mode = TrialRunner.DEBUG
1327+ if self.config['dry-run']:
1328+ mode = TrialRunner.DRY_RUN
1329+
1330+ # Hook up to the parent test runner
1331+ super(TestRunner, self).__init__(
1332+ options=options,
1333+ reporterFactory=self.config['reporter'],
1334+ mode=mode,
1335+ profile=self.config['profile'],
1336+ logfile=self.config['logfile'],
1337+ tracebackFormat=self.config['tbformat'],
1338+ realTimeErrors=self.config['rterrors'],
1339+ uncleanWarnings=self.config['unclean-warnings'],
1340+ forceGarbageCollection=self.config['force-gc'])
1341+ # Named for trial compatibility.
1342+ # pylint: disable=C0103
1343+ self.workingDirectory = self.working_dir
1344+ # pylint: enable=C0103
1345+
1346+ def run_tests(self, suite):
1347+ """Run the twisted test suite."""
1348+ if self.config['until-failure']:
1349+ result = self.runUntilFailure(suite)
1350+ else:
1351+ result = self.run(suite)
1352+ return result.wasSuccessful()
1353+
1354+
1355+def _get_default_reactor():
1356+ """Return the platform-dependent default reactor to use."""
1357+ default_reactor = 'gi'
1358+ if sys.platform in ['darwin', 'win32']:
1359+ default_reactor = 'twisted'
1360+ return default_reactor
1361+
1362+
1363+class TestOptions(trial.Options, BaseTestOptions):
1364+ """Class for twisted options handling."""
1365+
1366+ optFlags = [["help-reactors", None],
1367+ ]
1368+
1369+ optParameters = [["reactor", "r", _get_default_reactor()],
1370+ ]
1371+
1372+ def __init__(self, *args, **kwargs):
1373+ super(TestOptions, self).__init__(*args, **kwargs)
1374+ self['rterrors'] = True
1375+
1376+ def opt_coverage(self, option):
1377+ """Handle special flags."""
1378+ self['coverage'] = True
1379+ opt_c = opt_coverage
1380+
1381+ def opt_help_reactors(self):
1382+ """Help on available reactors for use with tests"""
1383+ synopsis = ('')
1384+ print(synopsis)
1385+ print('Need to get list of reactors and print them here.\n')
1386+ sys.exit(0)
1387+
1388+ def opt_reactor(self, option):
1389+ """Which reactor to use (see --help-reactors for a list
1390+ of possibilities)
1391+ """
1392+ self['reactor'] = option
1393+ opt_r = opt_reactor
1394
1395=== added directory 'ubuntuone/devtools/services'
1396=== added file 'ubuntuone/devtools/services/__init__.py'
1397--- ubuntuone/devtools/services/__init__.py 1970-01-01 00:00:00 +0000
1398+++ ubuntuone/devtools/services/__init__.py 2018-05-30 21:37:37 +0000
1399@@ -0,0 +1,66 @@
1400+#
1401+# Copyright 2011-2012 Canonical Ltd.
1402+#
1403+# This program is free software: you can redistribute it and/or modify it
1404+# under the terms of the GNU General Public License version 3, as published
1405+# by the Free Software Foundation.
1406+#
1407+# This program is distributed in the hope that it will be useful, but
1408+# WITHOUT ANY WARRANTY; without even the implied warranties of
1409+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1410+# PURPOSE. See the GNU General Public License for more details.
1411+#
1412+# You should have received a copy of the GNU General Public License along
1413+# with this program. If not, see <http://www.gnu.org/licenses/>.
1414+#
1415+# In addition, as a special exception, the copyright holders give
1416+# permission to link the code of portions of this program with the
1417+# OpenSSL library under certain conditions as described in each
1418+# individual source file, and distribute linked combinations
1419+# including the two.
1420+# You must obey the GNU General Public License in all respects
1421+# for all of the code used other than OpenSSL. If you modify
1422+# file(s) with this exception, you may extend this exception to your
1423+# version of the file(s), but you are not obligated to do so. If you
1424+# do not wish to do so, delete this exception statement from your
1425+# version. If you delete this exception statement from all source
1426+# files in the program, then also delete it here.
1427+"""Service runners for testing."""
1428+
1429+import os
1430+import socket
1431+
1432+from dirspec.basedir import load_data_paths
1433+
1434+
1435+def find_config_file(in_config_file):
1436+ """Find the first appropriate conf to use."""
1437+ # In case we're running from within the source tree
1438+ path = os.path.abspath(os.path.join(os.path.dirname(__file__),
1439+ os.path.pardir, os.path.pardir,
1440+ os.path.pardir,
1441+ "data", in_config_file))
1442+ if not os.path.exists(path):
1443+ # Use the installed file in $pkgdatadir as source
1444+ for path in load_data_paths("ubuntuone-dev-tools", in_config_file):
1445+ if os.path.exists(path):
1446+ break
1447+
1448+ # Check to make sure we didn't just fall out of the loop
1449+ if not os.path.exists(path):
1450+ raise IOError('Could not locate suitable %s' % in_config_file)
1451+ return path
1452+
1453+
1454+def get_arbitrary_port():
1455+ """
1456+ Find an unused port, and return it.
1457+
1458+ There might be a small race condition here, but we aren't
1459+ worried about it.
1460+ """
1461+ sock = socket.socket()
1462+ sock.bind(('localhost', 0))
1463+ _, port = sock.getsockname()
1464+ sock.close()
1465+ return port
1466
1467=== added file 'ubuntuone/devtools/services/dbus.py'
1468--- ubuntuone/devtools/services/dbus.py 1970-01-01 00:00:00 +0000
1469+++ ubuntuone/devtools/services/dbus.py 2018-05-30 21:37:37 +0000
1470@@ -0,0 +1,121 @@
1471+#
1472+# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
1473+#
1474+# Copyright 2009-2012 Canonical Ltd.
1475+#
1476+# This program is free software: you can redistribute it and/or modify it
1477+# under the terms of the GNU General Public License version 3, as published
1478+# by the Free Software Foundation.
1479+#
1480+# This program is distributed in the hope that it will be useful, but
1481+# WITHOUT ANY WARRANTY; without even the implied warranties of
1482+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1483+# PURPOSE. See the GNU General Public License for more details.
1484+#
1485+# You should have received a copy of the GNU General Public License along
1486+# with this program. If not, see <http://www.gnu.org/licenses/>.
1487+#
1488+# In addition, as a special exception, the copyright holders give
1489+# permission to link the code of portions of this program with the
1490+# OpenSSL library under certain conditions as described in each
1491+# individual source file, and distribute linked combinations
1492+# including the two.
1493+# You must obey the GNU General Public License in all respects
1494+# for all of the code used other than OpenSSL. If you modify
1495+# file(s) with this exception, you may extend this exception to your
1496+# version of the file(s), but you are not obligated to do so. If you
1497+# do not wish to do so, delete this exception statement from your
1498+# version. If you delete this exception statement from all source
1499+# files in the program, then also delete it here.
1500+"""Utilities for finding and running a dbus session bus for testing."""
1501+
1502+from __future__ import unicode_literals
1503+
1504+import os
1505+import signal
1506+import subprocess
1507+
1508+from distutils.spawn import find_executable
1509+
1510+# pylint: disable=F0401,E0611
1511+try:
1512+ from urllib.parse import quote
1513+except ImportError:
1514+ from urllib import quote
1515+# pylint: enable=F0401,E0611
1516+
1517+from ubuntuone.devtools.services import find_config_file
1518+DBUS_CONFIG_FILE = 'dbus-session.conf.in'
1519+
1520+
1521+class DBusLaunchError(Exception):
1522+ """Error while launching dbus-daemon"""
1523+ pass
1524+
1525+
1526+class NotFoundError(Exception):
1527+ """Not found error"""
1528+ pass
1529+
1530+
1531+class DBusRunner(object):
1532+ """Class for running dbus-daemon with a private session."""
1533+
1534+ def __init__(self):
1535+ self.dbus_address = None
1536+ self.dbus_pid = None
1537+ self.running = False
1538+ self.config_file = None
1539+
1540+ def _generate_config_file(self, tempdir=None):
1541+ """Find the first appropriate dbus-session.conf to use."""
1542+ # load the config file
1543+ path = find_config_file(DBUS_CONFIG_FILE)
1544+ # replace config settings
1545+ self.config_file = os.path.join(tempdir, 'dbus-session.conf')
1546+ dbus_address = 'unix:tmpdir=%s' % quote(tempdir)
1547+ with open(path) as in_file:
1548+ content = in_file.read()
1549+ with open(self.config_file, 'w') as out_file:
1550+ out_file.write(content.replace('@ADDRESS@', dbus_address))
1551+
1552+ def start_service(self, tempdir=None):
1553+ """Start our own session bus daemon for testing."""
1554+ dbus = find_executable("dbus-daemon")
1555+ if not dbus:
1556+ raise NotFoundError("dbus-daemon was not found.")
1557+
1558+ self._generate_config_file(tempdir)
1559+
1560+ dbus_args = ["--fork",
1561+ "--config-file=" + self.config_file,
1562+ "--print-address=1",
1563+ "--print-pid=2"]
1564+ sp = subprocess.Popen([dbus] + dbus_args,
1565+ bufsize=4096, stdout=subprocess.PIPE,
1566+ stderr=subprocess.PIPE)
1567+
1568+ # Call wait here as under the qt4 reactor we get an error about
1569+ # interrupted system call if we don't.
1570+ sp.wait()
1571+ self.dbus_address = b"".join(sp.stdout.readlines()).strip()
1572+ self.dbus_pid = int(b"".join(sp.stderr.readlines()).strip())
1573+
1574+ if self.dbus_address != "":
1575+ os.environ["DBUS_SESSION_BUS_ADDRESS"] = \
1576+ self.dbus_address.decode("utf8")
1577+ else:
1578+ os.kill(self.dbus_pid, signal.SIGKILL)
1579+ raise DBusLaunchError("There was a problem launching dbus-daemon.")
1580+ self.running = True
1581+
1582+ def stop_service(self):
1583+ """Stop our DBus session bus daemon."""
1584+ try:
1585+ del os.environ["DBUS_SESSION_BUS_ADDRESS"]
1586+ except KeyError:
1587+ pass
1588+ os.kill(self.dbus_pid, signal.SIGKILL)
1589+ self.running = False
1590+ os.unlink(self.config_file)
1591+ self.config_file = None
1592
1593=== added directory 'ubuntuone/devtools/testcases'
1594=== added file 'ubuntuone/devtools/testcases/__init__.py'
1595--- ubuntuone/devtools/testcases/__init__.py 1970-01-01 00:00:00 +0000
1596+++ ubuntuone/devtools/testcases/__init__.py 2018-05-30 21:37:37 +0000
1597@@ -0,0 +1,187 @@
1598+# -*- coding: utf-8 -*-
1599+#
1600+# Copyright 2009-2012 Canonical Ltd.
1601+#
1602+# This program is free software: you can redistribute it and/or modify it
1603+# under the terms of the GNU General Public License version 3, as published
1604+# by the Free Software Foundation.
1605+#
1606+# This program is distributed in the hope that it will be useful, but
1607+# WITHOUT ANY WARRANTY; without even the implied warranties of
1608+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1609+# PURPOSE. See the GNU General Public License for more details.
1610+#
1611+# You should have received a copy of the GNU General Public License along
1612+# with this program. If not, see <http://www.gnu.org/licenses/>.
1613+#
1614+# In addition, as a special exception, the copyright holders give
1615+# permission to link the code of portions of this program with the
1616+# OpenSSL library under certain conditions as described in each
1617+# individual source file, and distribute linked combinations
1618+# including the two.
1619+# You must obey the GNU General Public License in all respects
1620+# for all of the code used other than OpenSSL. If you modify
1621+# file(s) with this exception, you may extend this exception to your
1622+# version of the file(s), but you are not obligated to do so. If you
1623+# do not wish to do so, delete this exception statement from your
1624+# version. If you delete this exception statement from all source
1625+# files in the program, then also delete it here.
1626+"""Base tests cases and test utilities."""
1627+
1628+from __future__ import with_statement
1629+
1630+import contextlib
1631+import os
1632+import shutil
1633+import sys
1634+
1635+from functools import wraps
1636+
1637+from twisted.trial.unittest import TestCase, SkipTest
1638+
1639+
1640+@contextlib.contextmanager
1641+def environ(env_var, new_value):
1642+ """context manager to replace/add an environ value"""
1643+ old_value = os.environ.get(env_var, None)
1644+ os.environ[env_var] = new_value
1645+ yield
1646+ if old_value is None:
1647+ os.environ.pop(env_var)
1648+ else:
1649+ os.environ[env_var] = old_value
1650+
1651+
1652+def _id(obj):
1653+ """Return the obj calling the funct."""
1654+ return obj
1655+
1656+
1657+# pylint: disable=C0103
1658+def skipTest(reason):
1659+ """Unconditionally skip a test."""
1660+
1661+ def decorator(test_item):
1662+ """Decorate the test so that it is skipped."""
1663+ if not (isinstance(test_item, type) and
1664+ issubclass(test_item, TestCase)):
1665+
1666+ @wraps(test_item)
1667+ def skip_wrapper(*args, **kwargs):
1668+ """Skip a test method raising an exception."""
1669+ raise SkipTest(reason)
1670+ test_item = skip_wrapper
1671+
1672+ # tell twisted.trial.unittest to skip the test, pylint will complain
1673+ # since it thinks we are redefining a name out of the scope
1674+ # pylint: disable=W0621,W0612
1675+ test_item.skip = reason
1676+ # pylint: enable=W0621,W0612
1677+ # because the item was skipped, we will make sure that no
1678+ # services are started for it
1679+ if hasattr(test_item, "required_services"):
1680+ # pylint: disable=W0612
1681+ test_item.required_services = lambda *args, **kwargs: []
1682+ # pylint: enable=W0612
1683+ return test_item
1684+ return decorator
1685+
1686+
1687+def skipIf(condition, reason):
1688+ """Skip a test if the condition is true."""
1689+ if condition:
1690+ return skipTest(reason)
1691+ return _id
1692+
1693+
1694+def skipIfOS(current_os, reason):
1695+ """Skip test for a particular os or lists of them."""
1696+ if os:
1697+ if sys.platform in current_os or sys.platform == current_os:
1698+ return skipTest(reason)
1699+ return _id
1700+ return _id
1701+
1702+
1703+def skipIfNotOS(current_os, reason):
1704+ """Skip test we are not in a particular os."""
1705+ if os:
1706+ if sys.platform not in current_os or \
1707+ sys.platform != current_os:
1708+ return skipTest(reason)
1709+ return _id
1710+ return _id
1711+
1712+
1713+def skipIfJenkins(current_os, reason):
1714+ """Skip test for a particular os or lists of them
1715+ when running on Jenkins."""
1716+ if os.getenv("JENKINS", False) and (sys.platform in current_os or
1717+ sys.platform == current_os):
1718+ return skipTest(reason)
1719+ return _id
1720+
1721+
1722+# pylint: enable=C0103
1723+
1724+
1725+class BaseTestCase(TestCase):
1726+ """Base TestCase with helper methods to handle temp dir.
1727+
1728+ This class provides:
1729+ mktemp(name): helper to create temporary dirs
1730+ rmtree(path): support read-only shares
1731+ makedirs(path): support read-only shares
1732+
1733+ """
1734+
1735+ def required_services(self):
1736+ """Return the list of required services for DBusTestCase."""
1737+ return []
1738+
1739+ def mktemp(self, name='temp'):
1740+ """Customized mktemp that accepts an optional name argument."""
1741+ tempdir = os.path.join(self.tmpdir, name)
1742+ if os.path.exists(tempdir):
1743+ self.rmtree(tempdir)
1744+ self.makedirs(tempdir)
1745+ return tempdir
1746+
1747+ @property
1748+ def tmpdir(self):
1749+ """Default tmpdir: module/class/test_method."""
1750+ # check if we already generated the root path
1751+ root_dir = getattr(self, '__root', None)
1752+ if root_dir:
1753+ return root_dir
1754+ max_filename = 32 # some platforms limit lengths of filenames
1755+ base = os.path.join(self.__class__.__module__[:max_filename],
1756+ self.__class__.__name__[:max_filename],
1757+ self._testMethodName[:max_filename])
1758+ # use _trial_temp dir, it should be os.gwtcwd()
1759+ # define the root temp dir of the testcase, pylint: disable=W0201
1760+ self.__root = os.path.join(os.getcwd(), base)
1761+ return self.__root
1762+
1763+ def rmtree(self, path):
1764+ """Custom rmtree that handle ro parent(s) and childs."""
1765+ if not os.path.exists(path):
1766+ return
1767+ # change perms to rw, so we can delete the temp dir
1768+ if path != getattr(self, '__root', None):
1769+ os.chmod(os.path.dirname(path), 0o755)
1770+ if not os.access(path, os.W_OK):
1771+ os.chmod(path, 0o755)
1772+ # pylint: disable=W0612
1773+ for dirpath, dirs, files in os.walk(path):
1774+ for dirname in dirs:
1775+ if not os.access(os.path.join(dirpath, dirname), os.W_OK):
1776+ os.chmod(os.path.join(dirpath, dirname), 0o777)
1777+ shutil.rmtree(path)
1778+
1779+ def makedirs(self, path):
1780+ """Custom makedirs that handle ro parent."""
1781+ parent = os.path.dirname(path)
1782+ if os.path.exists(parent):
1783+ os.chmod(parent, 0o755)
1784+ os.makedirs(path)
1785
1786=== added file 'ubuntuone/devtools/testcases/dbus.py'
1787--- ubuntuone/devtools/testcases/dbus.py 1970-01-01 00:00:00 +0000
1788+++ ubuntuone/devtools/testcases/dbus.py 2018-05-30 21:37:37 +0000
1789@@ -0,0 +1,138 @@
1790+# -*- coding: utf-8 -*-
1791+#
1792+# Copyright 2009-2012 Canonical Ltd.
1793+#
1794+# This program is free software: you can redistribute it and/or modify it
1795+# under the terms of the GNU General Public License version 3, as published
1796+# by the Free Software Foundation.
1797+#
1798+# This program is distributed in the hope that it will be useful, but
1799+# WITHOUT ANY WARRANTY; without even the implied warranties of
1800+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1801+# PURPOSE. See the GNU General Public License for more details.
1802+#
1803+# You should have received a copy of the GNU General Public License along
1804+# with this program. If not, see <http://www.gnu.org/licenses/>.
1805+#
1806+# In addition, as a special exception, the copyright holders give
1807+# permission to link the code of portions of this program with the
1808+# OpenSSL library under certain conditions as described in each
1809+# individual source file, and distribute linked combinations
1810+# including the two.
1811+# You must obey the GNU General Public License in all respects
1812+# for all of the code used other than OpenSSL. If you modify
1813+# file(s) with this exception, you may extend this exception to your
1814+# version of the file(s), but you are not obligated to do so. If you
1815+# do not wish to do so, delete this exception statement from your
1816+# version. If you delete this exception statement from all source
1817+# files in the program, then also delete it here.
1818+"""Base dbus tests cases and test utilities."""
1819+
1820+from __future__ import absolute_import, with_statement
1821+
1822+import os
1823+
1824+from twisted.internet import defer
1825+
1826+from ubuntuone.devtools.testcases import BaseTestCase, skipIf
1827+
1828+# DBusRunner for DBusTestCase using tests
1829+from ubuntuone.devtools.services.dbus import DBusRunner
1830+
1831+
1832+# pylint: disable=F0401,C0103,W0406,E0611
1833+try:
1834+ import dbus
1835+except ImportError as e:
1836+ dbus = None
1837+
1838+try:
1839+ import dbus.service as service
1840+except ImportError:
1841+ service = None
1842+
1843+try:
1844+ from dbus.mainloop.glib import DBusGMainLoop
1845+except ImportError:
1846+ DBusGMainLoop = None
1847+
1848+# pylint: enable=F0401,C0103,W0406,E0611
1849+
1850+
1851+class InvalidSessionBus(Exception):
1852+ """Error when we are connected to the wrong session bus in tests."""
1853+
1854+
1855+class FakeDBusInterface(object):
1856+ """A fake DBusInterface..."""
1857+
1858+ def shutdown(self, with_restart=False):
1859+ """...that only knows how to go away"""
1860+
1861+
1862+@skipIf(dbus is None or service is None or DBusGMainLoop is None,
1863+ "The test requires dbus.")
1864+class DBusTestCase(BaseTestCase):
1865+ """Test the DBus event handling."""
1866+
1867+ def required_services(self):
1868+ """Return the list of required services for DBusTestCase."""
1869+ services = super(DBusTestCase, self).required_services()
1870+ services.extend([DBusRunner])
1871+ return services
1872+
1873+ @defer.inlineCallbacks
1874+ def setUp(self):
1875+ """Setup the infrastructure fo the test (dbus service)."""
1876+ # Class 'BaseTestCase' has no 'setUp' member
1877+ # pylint: disable=E1101
1878+ # dbus modules will be imported by the decorator
1879+ # pylint: disable=E0602
1880+ yield super(DBusTestCase, self).setUp()
1881+
1882+ # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here
1883+ # pylint: disable=F0401,E0611
1884+ try:
1885+ from urllib.parse import unquote
1886+ except ImportError:
1887+ from urllib import unquote
1888+ # pylint: enable=F0401,E0611
1889+
1890+ bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
1891+ if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
1892+ != os.path.dirname(os.getcwd()):
1893+ raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRESS is wrong.')
1894+
1895+ # Set up the main loop and bus connection
1896+ self.loop = DBusGMainLoop(set_as_default=True)
1897+
1898+ # NOTE: The address_or_type value must remain explicitly as
1899+ # str instead of anything from ubuntuone.devtools.compat. dbus
1900+ # expects this to be str regardless of version.
1901+ self.bus = dbus.bus.BusConnection(address_or_type=str(bus_address),
1902+ mainloop=self.loop)
1903+
1904+ # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we
1905+ # always point at our own private bus instance.
1906+ self.patch(dbus, 'SessionBus', lambda: self.bus)
1907+ self.patch(dbus, 'SystemBus', lambda: self.bus)
1908+
1909+ # Check that we are on the correct bus for real
1910+# Disable this for now, because our tests are extremely broken :(
1911+# bus_names = self.bus.list_names()
1912+# if len(bus_names) > 2:
1913+# raise InvalidSessionBus('Too many bus connections: %s (%r)' %
1914+# (len(bus_names), bus_names))
1915+
1916+ # monkeypatch busName.__del__ to avoid errors on gc
1917+ # we take care of releasing the name in shutdown
1918+ service.BusName.__del__ = lambda _: None
1919+ yield self.bus.set_exit_on_disconnect(False)
1920+ self.signal_receivers = set()
1921+
1922+ @defer.inlineCallbacks
1923+ def tearDown(self):
1924+ """Cleanup the test."""
1925+ yield self.bus.flush()
1926+ yield self.bus.close()
1927+ yield super(DBusTestCase, self).tearDown()
1928
1929=== added file 'ubuntuone/devtools/testcases/txsocketserver.py'
1930--- ubuntuone/devtools/testcases/txsocketserver.py 1970-01-01 00:00:00 +0000
1931+++ ubuntuone/devtools/testcases/txsocketserver.py 2018-05-30 21:37:37 +0000
1932@@ -0,0 +1,357 @@
1933+# -*- coding: utf-8 -*-
1934+# Copyright 2012 Canonical Ltd.
1935+#
1936+# This program is free software: you can redistribute it and/or modify it
1937+# under the terms of the GNU General Public License version 3, as published
1938+# by the Free Software Foundation.
1939+#
1940+# This program is distributed in the hope that it will be useful, but
1941+# WITHOUT ANY WARRANTY; without even the implied warranties of
1942+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1943+# PURPOSE. See the GNU General Public License for more details.
1944+#
1945+# You should have received a copy of the GNU General Public License along
1946+# with this program. If not, see <http://www.gnu.org/licenses/>.
1947+#
1948+# In addition, as a special exception, the copyright holders give
1949+# permission to link the code of portions of this program with the
1950+# OpenSSL library under certain conditions as described in each
1951+# individual source file, and distribute linked combinations
1952+# including the two.
1953+# You must obey the GNU General Public License in all respects
1954+# for all of the code used other than OpenSSL. If you modify
1955+# file(s) with this exception, you may extend this exception to your
1956+# version of the file(s), but you are not obligated to do so. If you
1957+# do not wish to do so, delete this exception statement from your
1958+# version. If you delete this exception statement from all source
1959+# files in the program, then also delete it here.
1960+
1961+"""Base test case for twisted servers."""
1962+
1963+import os
1964+import shutil
1965+import tempfile
1966+
1967+from twisted.internet import defer, endpoints, protocol
1968+from twisted.spread import pb
1969+
1970+from ubuntuone.devtools.testcases import BaseTestCase
1971+
1972+# no init method + twisted common warnings
1973+# pylint: disable=W0232, C0103, E1101
1974+
1975+
1976+def server_protocol_factory(cls):
1977+ """Factory to create tidy protocols."""
1978+
1979+ if cls is None:
1980+ cls = protocol.Protocol
1981+
1982+ class ServerTidyProtocol(cls):
1983+ """A tidy protocol."""
1984+
1985+ def connectionLost(self, *args):
1986+ """Lost the connection."""
1987+ cls.connectionLost(self, *args)
1988+ # lets tell everyone
1989+ # pylint: disable=W0212
1990+ if (self.factory._disconnecting and
1991+ self.factory.testserver_on_connection_lost is not None and
1992+ not self.factory.testserver_on_connection_lost.called):
1993+ self.factory.testserver_on_connection_lost.callback(self)
1994+ # pylint: enable=W0212
1995+
1996+ return ServerTidyProtocol
1997+
1998+
1999+def server_factory_factory(cls):
2000+ """Factory that creates special types of factories for tests."""
2001+
2002+ if cls is None:
2003+ cls = protocol.ServerFactory
2004+
2005+ class TidyServerFactory(cls):
2006+ """A tidy factory."""
2007+
2008+ testserver_on_connection_lost = None
2009+
2010+ def buildProtocol(self, addr):
2011+ prot = cls.buildProtocol(self, addr)
2012+ self.testserver_on_connection_lost = defer.Deferred()
2013+ return prot
2014+
2015+ return TidyServerFactory
2016+
2017+
2018+def client_protocol_factory(cls):
2019+ """Factory to create tidy protocols."""
2020+
2021+ if cls is None:
2022+ cls = protocol.Protocol
2023+
2024+ class ClientTidyProtocol(cls):
2025+ """A tidy protocol."""
2026+
2027+ def connectionLost(self, *a):
2028+ """Connection list."""
2029+ cls.connectionLost(self, *a)
2030+ # pylint: disable=W0212
2031+ if (self.factory._disconnecting and
2032+ self.factory.testserver_on_connection_lost is not None and
2033+ not self.factory.testserver_on_connection_lost.called):
2034+ self.factory.testserver_on_connection_lost.callback(self)
2035+ # pylint: enable=W0212
2036+
2037+ return ClientTidyProtocol
2038+
2039+
2040+class TidySocketServer(object):
2041+ """Ensure that twisted servers are correctly managed in tests.
2042+
2043+ Closing a twisted server is a complicated matter. In order to do so you
2044+ have to ensure that three different deferreds are fired:
2045+
2046+ 1. The server must stop listening.
2047+ 2. The client connection must disconnect.
2048+ 3. The server connection must disconnect.
2049+
2050+ This class allows to create a server and a client that will ensure that
2051+ the reactor is left clean by following the pattern described at
2052+ http://mumak.net/stuff/twisted-disconnect.html
2053+ """
2054+ def __init__(self):
2055+ """Create a new instance."""
2056+ self.listener = None
2057+ self.server_factory = None
2058+
2059+ self.connector = None
2060+ self.client_factory = None
2061+
2062+ def get_server_endpoint(self):
2063+ """Return the server endpoint description."""
2064+ raise NotImplementedError('To be implemented by child classes.')
2065+
2066+ def get_client_endpoint(self):
2067+ """Return the client endpoint description."""
2068+ raise NotImplementedError('To be implemented by child classes.')
2069+
2070+ @defer.inlineCallbacks
2071+ def listen_server(self, server_class, *args, **kwargs):
2072+ """Start a server in a random port."""
2073+ from twisted.internet import reactor
2074+ tidy_class = server_factory_factory(server_class)
2075+ self.server_factory = tidy_class(*args, **kwargs)
2076+ self.server_factory._disconnecting = False
2077+ self.server_factory.protocol = server_protocol_factory(
2078+ self.server_factory.protocol)
2079+ endpoint = endpoints.serverFromString(reactor,
2080+ self.get_server_endpoint())
2081+ self.listener = yield endpoint.listen(self.server_factory)
2082+ defer.returnValue(self.server_factory)
2083+
2084+ @defer.inlineCallbacks
2085+ def connect_client(self, client_class, *args, **kwargs):
2086+ """Conect a client to a given server."""
2087+ from twisted.internet import reactor
2088+
2089+ if self.server_factory is None:
2090+ raise ValueError('Server Factory was not provided.')
2091+ if self.listener is None:
2092+ raise ValueError('%s has not started listening.',
2093+ self.server_factory)
2094+
2095+ self.client_factory = client_class(*args, **kwargs)
2096+ self.client_factory._disconnecting = False
2097+ self.client_factory.protocol = client_protocol_factory(
2098+ self.client_factory.protocol)
2099+ self.client_factory.testserver_on_connection_lost = defer.Deferred()
2100+ endpoint = endpoints.clientFromString(reactor,
2101+ self.get_client_endpoint())
2102+ self.connector = yield endpoint.connect(self.client_factory)
2103+ defer.returnValue(self.client_factory)
2104+
2105+ def clean_up(self):
2106+ """Action to be performed for clean up."""
2107+ if self.server_factory is None or self.listener is None:
2108+ # nothing to clean
2109+ return defer.succeed(None)
2110+
2111+ if self.listener and self.connector:
2112+ # clean client and server
2113+ self.server_factory._disconnecting = True
2114+ self.client_factory._disconnecting = True
2115+ d = defer.maybeDeferred(self.listener.stopListening)
2116+ self.connector.transport.loseConnection()
2117+ if self.server_factory.testserver_on_connection_lost:
2118+ return defer.gatherResults(
2119+ [d,
2120+ self.client_factory.testserver_on_connection_lost,
2121+ self.server_factory.testserver_on_connection_lost])
2122+ else:
2123+ return defer.gatherResults(
2124+ [d,
2125+ self.client_factory.testserver_on_connection_lost])
2126+ if self.listener:
2127+ # just clean the server since there is no client
2128+ # pylint: disable=W0201
2129+ self.server_factory._disconnecting = True
2130+ return defer.maybeDeferred(self.listener.stopListening)
2131+ # pylint: enable=W0201
2132+
2133+
2134+class TidyTCPServer(TidySocketServer):
2135+ """A tidy tcp domain sockets server."""
2136+
2137+ client_endpoint_pattern = 'tcp:host=127.0.0.1:port=%s'
2138+ server_endpoint_pattern = 'tcp:0:interface=127.0.0.1'
2139+
2140+ def get_server_endpoint(self):
2141+ """Return the server endpoint description."""
2142+ return self.server_endpoint_pattern
2143+
2144+ def get_client_endpoint(self):
2145+ """Return the client endpoint description."""
2146+ if self.server_factory is None:
2147+ raise ValueError('Server Factory was not provided.')
2148+ if self.listener is None:
2149+ raise ValueError('%s has not started listening.',
2150+ self.server_factory)
2151+ return self.client_endpoint_pattern % self.listener.getHost().port
2152+
2153+
2154+class TidyUnixServer(TidySocketServer):
2155+ """A tidy unix domain sockets server."""
2156+
2157+ client_endpoint_pattern = 'unix:path=%s'
2158+ server_endpoint_pattern = 'unix:%s'
2159+
2160+ def __init__(self):
2161+ """Create a new instance."""
2162+ super(TidyUnixServer, self).__init__()
2163+ self.temp_dir = tempfile.mkdtemp()
2164+ self.path = os.path.join(self.temp_dir, 'tidy_unix_server')
2165+
2166+ def get_server_endpoint(self):
2167+ """Return the server endpoint description."""
2168+ return self.server_endpoint_pattern % self.path
2169+
2170+ def get_client_endpoint(self):
2171+ """Return the client endpoint description."""
2172+ return self.client_endpoint_pattern % self.path
2173+
2174+ def clean_up(self):
2175+ """Action to be performed for clean up."""
2176+ result = super(TidyUnixServer, self).clean_up()
2177+ # remove the dir once we are disconnected
2178+ result.addCallback(lambda _: shutil.rmtree(self.temp_dir))
2179+ return result
2180+
2181+
2182+class ServerTestCase(BaseTestCase):
2183+ """Base test case for tidy servers."""
2184+
2185+ @defer.inlineCallbacks
2186+ def setUp(self):
2187+ """Set the diff tests."""
2188+ yield super(ServerTestCase, self).setUp()
2189+
2190+ try:
2191+ self.server_runner = self.get_server()
2192+ except NotImplementedError:
2193+ self.server_runner = None
2194+
2195+ self.server_factory = None
2196+ self.client_factory = None
2197+ self.server_disconnected = None
2198+ self.client_connected = None
2199+ self.client_disconnected = None
2200+ self.listener = None
2201+ self.connector = None
2202+ self.addCleanup(self.tear_down_server_client)
2203+
2204+ def get_server(self):
2205+ """Return the server to be used to run the tests."""
2206+ raise NotImplementedError('To be implemented by child classes.')
2207+
2208+ @defer.inlineCallbacks
2209+ def listen_server(self, server_class, *args, **kwargs):
2210+ """Listen a server.
2211+
2212+ The method takes the server class and the arguments that should be
2213+ passed to the server constructor.
2214+ """
2215+ self.server_factory = yield self.server_runner.listen_server(
2216+ server_class, *args, **kwargs)
2217+ self.server_disconnected = \
2218+ self.server_factory.testserver_on_connection_lost
2219+ self.listener = self.server_runner.listener
2220+
2221+ @defer.inlineCallbacks
2222+ def connect_client(self, client_class, *args, **kwargs):
2223+ """Connect the client.
2224+
2225+ The method takes the client factory class and the arguments that
2226+ should be passed to the client constructor.
2227+ """
2228+ self.client_factory = yield self.server_runner.connect_client(
2229+ client_class, *args, **kwargs)
2230+ self.client_disconnected = \
2231+ self.client_factory.testserver_on_connection_lost
2232+ self.connector = self.server_runner.connector
2233+
2234+ def tear_down_server_client(self):
2235+ """Clean the server and client."""
2236+ if self.server_runner:
2237+ return self.server_runner.clean_up()
2238+
2239+
2240+class TCPServerTestCase(ServerTestCase):
2241+ """Test that uses a single twisted server."""
2242+
2243+ def get_server(self):
2244+ """Return the server to be used to run the tests."""
2245+ return TidyTCPServer()
2246+
2247+
2248+class UnixServerTestCase(ServerTestCase):
2249+ """Test that uses a single twisted server."""
2250+
2251+ def get_server(self):
2252+ """Return the server to be used to run the tests."""
2253+ return TidyUnixServer()
2254+
2255+
2256+class PbServerTestCase(ServerTestCase):
2257+ """Test a pb server."""
2258+
2259+ def get_server(self):
2260+ """Return the server to be used to run the tests."""
2261+ raise NotImplementedError('To be implemented by child classes.')
2262+
2263+ @defer.inlineCallbacks
2264+ def listen_server(self, *args, **kwargs):
2265+ """Listen a pb server."""
2266+ yield super(PbServerTestCase, self).listen_server(pb.PBServerFactory,
2267+ *args, **kwargs)
2268+
2269+ @defer.inlineCallbacks
2270+ def connect_client(self, *args, **kwargs):
2271+ """Connect a pb client."""
2272+ yield super(PbServerTestCase, self).connect_client(pb.PBClientFactory,
2273+ *args, **kwargs)
2274+
2275+
2276+class TCPPbServerTestCase(PbServerTestCase):
2277+ """Test a pb server over TCP."""
2278+
2279+ def get_server(self):
2280+ """Return the server to be used to run the tests."""
2281+ return TidyTCPServer()
2282+
2283+
2284+class UnixPbServerTestCase(PbServerTestCase):
2285+ """Test a pb server over Unix domain sockets."""
2286+
2287+ def get_server(self):
2288+ """Return the server to be used to run the tests."""
2289+ return TidyUnixServer()
2290
2291=== added directory 'ubuntuone/devtools/testing'
2292=== added file 'ubuntuone/devtools/testing/__init__.py'
2293--- ubuntuone/devtools/testing/__init__.py 1970-01-01 00:00:00 +0000
2294+++ ubuntuone/devtools/testing/__init__.py 2018-05-30 21:37:37 +0000
2295@@ -0,0 +1,1 @@
2296+"""Testing helpers."""
2297
2298=== added file 'ubuntuone/devtools/testing/txcheck.py'
2299--- ubuntuone/devtools/testing/txcheck.py 1970-01-01 00:00:00 +0000
2300+++ ubuntuone/devtools/testing/txcheck.py 2018-05-30 21:37:37 +0000
2301@@ -0,0 +1,381 @@
2302+# -*- coding: utf-8 -*-
2303+
2304+# Author: Tim Cole <tim.cole@canonical.com>
2305+#
2306+# Copyright 2011-2012 Canonical Ltd.
2307+#
2308+# This program is free software: you can redistribute it and/or modify it
2309+# under the terms of the GNU General Public License version 3, as published
2310+# by the Free Software Foundation.
2311+#
2312+# This program is distributed in the hope that it will be useful, but
2313+# WITHOUT ANY WARRANTY; without even the implied warranties of
2314+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2315+# PURPOSE. See the GNU General Public License for more details.
2316+#
2317+# You should have received a copy of the GNU General Public License along
2318+# with this program. If not, see <http://www.gnu.org/licenses/>.
2319+#
2320+# In addition, as a special exception, the copyright holders give
2321+# permission to link the code of portions of this program with the
2322+# OpenSSL library under certain conditions as described in each
2323+# individual source file, and distribute linked combinations
2324+# including the two.
2325+# You must obey the GNU General Public License in all respects
2326+# for all of the code used other than OpenSSL. If you modify
2327+# file(s) with this exception, you may extend this exception to your
2328+# version of the file(s), but you are not obligated to do so. If you
2329+# do not wish to do so, delete this exception statement from your
2330+# version. If you delete this exception statement from all source
2331+# files in the program, then also delete it here.
2332+"""Utilities for performing correctness checks."""
2333+
2334+import sys
2335+import ast
2336+from inspect import getsource
2337+from textwrap import dedent
2338+from itertools import takewhile
2339+from unittest import TestCase, TestSuite, TestResult
2340+
2341+from twisted.trial.unittest import TestCase as TwistedTestCase
2342+
2343+
2344+def type_to_name(type_obj):
2345+ """Return a name for a type."""
2346+ package_name = getattr(type_obj, '__module__', None)
2347+ if package_name:
2348+ return "%s.%s" % (package_name, type_obj.__name__)
2349+ else:
2350+ return type_obj.__name__
2351+
2352+
2353+class Problem(AssertionError):
2354+ """An object representing a problem in a method."""
2355+
2356+ def __init__(self, method, test_class, ancestor_class):
2357+ """Initialize an instance."""
2358+ super(Problem, self).__init__()
2359+ self.method = method
2360+ self.test_class = test_class
2361+ self.ancestor_class = ancestor_class
2362+
2363+ def __eq__(self, other):
2364+ """Test equality."""
2365+ return type(self) == type(other) and self.__dict__ == other.__dict__
2366+
2367+ def __ne__(self, other):
2368+ """Test inequality."""
2369+ return not (self == other)
2370+
2371+ def __hash__(self):
2372+ """Return hash."""
2373+ member_hash = 0
2374+ for (key, value) in self.__dict__.items():
2375+ member_hash ^= hash(key) ^ hash(value)
2376+ return hash(type(self)) ^ member_hash
2377+
2378+ def __str__(self):
2379+ """Return a friendlier representation."""
2380+ if self.ancestor_class != self.test_class:
2381+ method_string = ("%s in ancestor method %s.%s" %
2382+ (type_to_name(self.test_class),
2383+ type_to_name(self.ancestor_class),
2384+ self.method))
2385+ else:
2386+ method_string = ("%s.%s" %
2387+ (type_to_name(self.test_class), self.method))
2388+ return ("%s for %s" % (type(self).__name__, method_string))
2389+
2390+ def __repr__(self):
2391+ """Return representation string."""
2392+ return "<%s %r>" % (type(self), self.__dict__)
2393+
2394+
2395+class MethodShadowed(Problem):
2396+ """Problem when trial's run method is shadowed."""
2397+
2398+
2399+class SuperResultDiscarded(Problem):
2400+ """Problem when callback chains are broken."""
2401+
2402+
2403+class SuperNotCalled(Problem):
2404+ """Problem when super isn't called."""
2405+
2406+
2407+class MissingInlineCallbacks(Problem):
2408+ """Problem when the inlineCallbacks decorator is missing."""
2409+
2410+
2411+class MissingReturnValue(Problem):
2412+ """Problem when there's no return value."""
2413+
2414+
2415+def match_type(expected_type):
2416+ """Return predicate matching nodes of given type."""
2417+ return lambda node: isinstance(node, expected_type)
2418+
2419+
2420+def match_equal(expected_value):
2421+ """Return predicate matching nodes equaling the given value."""
2422+ return lambda node: expected_value == node
2423+
2424+
2425+def match_in(expected_values):
2426+ """Return predicate matching node if in collection of expected values."""
2427+ return lambda node: node in expected_values
2428+
2429+
2430+def match_not_none():
2431+ """Returns a predicate matching nodes which are not None."""
2432+ return lambda node: node is not None
2433+
2434+
2435+def match_any(*subtests):
2436+ """Return short-circuiting predicate matching any given subpredicate."""
2437+ if len(subtests) == 1:
2438+ return subtests[0]
2439+ else:
2440+
2441+ def test(node):
2442+ """Try each subtest until we find one that passes."""
2443+ for subtest in subtests:
2444+ if subtest(node):
2445+ return True
2446+ return False
2447+
2448+ return test
2449+
2450+
2451+def match_all(*subtests):
2452+ """Return short-circuiting predicate matching all given subpredicates."""
2453+ if len(subtests) == 1:
2454+ return subtests[0]
2455+ else:
2456+
2457+ def test(node):
2458+ """Try each subtest until we find one that fails."""
2459+ for subtest in subtests:
2460+ if not subtest(node):
2461+ return False
2462+ return True
2463+
2464+ return test
2465+
2466+
2467+def match_attr(attr_name, *tests):
2468+ """Return predicate matching subpredicates against an attribute value."""
2469+ return lambda node: match_all(*tests)(getattr(node, attr_name))
2470+
2471+
2472+def match_path(initial_test, *components):
2473+ """Return predicate which recurses into the tree via given attributes."""
2474+ components = list(components)
2475+ components.reverse()
2476+
2477+ def test(node):
2478+ return True
2479+
2480+ for component in components:
2481+ attr_name = component[0]
2482+ subtests = component[1:]
2483+ test = match_attr(attr_name, match_all(match_all(*subtests), test))
2484+ return match_all(initial_test, test)
2485+
2486+
2487+def match_child(*tests):
2488+ """Return predicate which tests any child."""
2489+ subtest = match_all(*tests)
2490+
2491+ def test(node):
2492+ """Try each child until we find one that matches."""
2493+ for child in ast.iter_child_nodes(node):
2494+ if subtest(child):
2495+ return True
2496+ return False
2497+
2498+ return test
2499+
2500+
2501+def match_descendant(subtest, prune):
2502+ """Return predicate which tests a node and any descendants."""
2503+
2504+ def test(node):
2505+ """Recursively (breadth-first) search for a matching node."""
2506+ for child in ast.iter_child_nodes(node):
2507+ if prune(child):
2508+ continue
2509+ if subtest(child) or test(child):
2510+ return True
2511+ return False
2512+
2513+ return test
2514+
2515+
2516+def matches(node, *tests):
2517+ """Convenience function to try predicates on a node."""
2518+ return match_all(*tests)(node)
2519+
2520+
2521+def any_matches(nodes, *tests):
2522+ """Convenience function to try predicates on any of a sequence of nodes."""
2523+ test = match_all(*tests)
2524+ for node in nodes:
2525+ if test(node):
2526+ return True
2527+ return False
2528+
2529+
2530+def iter_matching_child_nodes(node, *tests):
2531+ """Yields every matching child node."""
2532+ test = match_all(*tests)
2533+ for child in ast.iter_child_nodes(node):
2534+ if test(child):
2535+ yield child
2536+
2537+
2538+SETUP_FUNCTION_NAMES = ('setUp', 'tearDown')
2539+SETUP_FUNCTION = match_path(match_type(ast.FunctionDef),
2540+ ('name', match_in(SETUP_FUNCTION_NAMES)))
2541+
2542+SUPER = match_path(match_type(ast.Call),
2543+ ('func', match_type(ast.Attribute)),
2544+ ('value', match_type(ast.Call)),
2545+ ('func', match_type(ast.Name)),
2546+ ('id', match_equal("super")))
2547+
2548+BARE_SUPER = match_path(match_type(ast.Expr),
2549+ ('value', SUPER))
2550+
2551+YIELD = match_type(ast.Yield)
2552+
2553+INLINE_CALLBACKS_DECORATOR = \
2554+ match_any(match_path(match_type(ast.Attribute),
2555+ ('attr', match_equal('inlineCallbacks'))),
2556+ match_path(match_type(ast.Name),
2557+ ('id', match_equal('inlineCallbacks'))))
2558+
2559+RETURN_VALUE = \
2560+ match_path(match_type(ast.Return),
2561+ ('value', match_not_none()))
2562+
2563+DEFS = match_any(match_type(ast.ClassDef),
2564+ match_type(ast.FunctionDef))
2565+
2566+
2567+def find_problems(class_to_check):
2568+ """Check twisted test setup in a given test class."""
2569+ mro = class_to_check.__mro__
2570+ if TwistedTestCase not in mro:
2571+ return set()
2572+
2573+ problems = set()
2574+
2575+ ancestry = takewhile(lambda c: c != TwistedTestCase, mro)
2576+ for ancestor_class in ancestry:
2577+ if 'run' in ancestor_class.__dict__:
2578+ problem = MethodShadowed(method='run',
2579+ test_class=class_to_check,
2580+ ancestor_class=ancestor_class)
2581+ problems.add(problem)
2582+
2583+ source = dedent(getsource(ancestor_class))
2584+ tree = ast.parse(source)
2585+ # the top level of the tree is a Module
2586+ class_node = tree.body[0]
2587+
2588+ # Check setUp/tearDown
2589+ for def_node in iter_matching_child_nodes(class_node, SETUP_FUNCTION):
2590+ if matches(def_node, match_child(BARE_SUPER)):
2591+ # Superclass method called, but its result wasn't used
2592+ problem = SuperResultDiscarded(method=def_node.name,
2593+ test_class=class_to_check,
2594+ ancestor_class=ancestor_class)
2595+ problems.add(problem)
2596+ if not matches(def_node, match_descendant(SUPER, DEFS)):
2597+ # The call to the overridden superclass method is missing
2598+ problem = SuperNotCalled(method=def_node.name,
2599+ test_class=class_to_check,
2600+ ancestor_class=ancestor_class)
2601+ problems.add(problem)
2602+
2603+ decorators = def_node.decorator_list
2604+
2605+ if matches(def_node, match_descendant(YIELD, DEFS)):
2606+ # Yield was used, making this a generator
2607+ if not any_matches(decorators, INLINE_CALLBACKS_DECORATOR):
2608+ # ...but the inlineCallbacks decorator is missing
2609+ problem = MissingInlineCallbacks(
2610+ method=def_node.name,
2611+ test_class=class_to_check,
2612+ ancestor_class=ancestor_class)
2613+ problems.add(problem)
2614+ else:
2615+ if not matches(def_node, match_descendant(RETURN_VALUE, DEFS)):
2616+ # The function fails to return a deferred
2617+ problem = MissingReturnValue(
2618+ method=def_node.name,
2619+ test_class=class_to_check,
2620+ ancestor_class=ancestor_class)
2621+ problems.add(problem)
2622+
2623+ return problems
2624+
2625+
2626+def get_test_classes(suite):
2627+ """Return all the unique test classes involved in a suite."""
2628+ classes = set()
2629+
2630+ def find_classes(suite_or_test):
2631+ """Recursively find all the test classes."""
2632+ if isinstance(suite_or_test, TestSuite):
2633+ for subtest in suite_or_test:
2634+ find_classes(subtest)
2635+ else:
2636+ classes.add(type(suite_or_test))
2637+
2638+ find_classes(suite)
2639+
2640+ return classes
2641+
2642+
2643+def make_check_testcase(tests):
2644+ """Make TestCase which checks the given twisted tests."""
2645+
2646+ class TXCheckTest(TestCase):
2647+ """Test case which checks the test classes for problems."""
2648+
2649+ def runTest(self): # pylint: disable=C0103
2650+ """Do nothing."""
2651+
2652+ def run(self, result=None):
2653+ """Check all the test classes for problems."""
2654+ if result is None:
2655+ result = TestResult()
2656+
2657+ test_classes = set()
2658+
2659+ for test_object in tests:
2660+ test_classes |= get_test_classes(test_object)
2661+
2662+ for test_class in test_classes:
2663+ problems = find_problems(test_class)
2664+ for problem in problems:
2665+ try:
2666+ raise problem
2667+ except Problem:
2668+ result.addFailure(self, sys.exc_info())
2669+
2670+ return TXCheckTest()
2671+
2672+
2673+class TXCheckSuite(TestSuite):
2674+ """Test suite which checks twisted tests."""
2675+
2676+ def __init__(self, tests=()):
2677+ """Initialize with the given tests, and add a special test."""
2678+
2679+ tests = list(tests)
2680+ tests.insert(0, make_check_testcase(self))
2681+
2682+ super(TXCheckSuite, self).__init__(tests)
2683
2684=== added file 'ubuntuone/devtools/testing/txwebserver.py'
2685--- ubuntuone/devtools/testing/txwebserver.py 1970-01-01 00:00:00 +0000
2686+++ ubuntuone/devtools/testing/txwebserver.py 2018-05-30 21:37:37 +0000
2687@@ -0,0 +1,124 @@
2688+# -*- coding: utf-8 -*-
2689+# Copyright 2012 Canonical Ltd.
2690+#
2691+# This program is free software: you can redistribute it and/or modify it
2692+# under the terms of the GNU General Public License version 3, as published
2693+# by the Free Software Foundation.
2694+#
2695+# This program is distributed in the hope that it will be useful, but
2696+# WITHOUT ANY WARRANTY; without even the implied warranties of
2697+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2698+# PURPOSE. See the GNU General Public License for more details.
2699+#
2700+# You should have received a copy of the GNU General Public License along
2701+# with this program. If not, see <http://www.gnu.org/licenses/>.
2702+#
2703+# In addition, as a special exception, the copyright holders give
2704+# permission to link the code of portions of this program with the
2705+# OpenSSL library under certain conditions as described in each
2706+# individual source file, and distribute linked combinations
2707+# including the two.
2708+# You must obey the GNU General Public License in all respects
2709+# for all of the code used other than OpenSSL. If you modify
2710+# file(s) with this exception, you may extend this exception to your
2711+# version of the file(s), but you are not obligated to do so. If you
2712+# do not wish to do so, delete this exception statement from your
2713+# version. If you delete this exception statement from all source
2714+# files in the program, then also delete it here.
2715+
2716+"""A tx based web server."""
2717+
2718+from __future__ import unicode_literals
2719+
2720+from twisted.internet import defer, reactor, ssl
2721+from twisted.protocols.policies import WrappingFactory
2722+from twisted.web import server
2723+
2724+from ubuntuone.devtools.testcases.txsocketserver import server_protocol_factory
2725+
2726+# no init method + twisted common warnings
2727+# pylint: disable=W0232, C0103, E1101
2728+
2729+
2730+class BaseWebServer(object):
2731+ """Webserver used to perform requests in tests."""
2732+
2733+ def __init__(self, root_resource, scheme):
2734+ """Create and start the instance.
2735+
2736+ The ssl_settings parameter contains a dictionary with the key and cert
2737+ that will be used to perform ssl connections. The root_resource
2738+ contains the resource with all its childre.
2739+ """
2740+ self.root = root_resource
2741+ self.scheme = scheme
2742+ self.port = None
2743+ # use an http.HTTPFactory that was modified to ensure that we have
2744+ # clean close connections
2745+ self.site = server.Site(self.root, timeout=None)
2746+ self.wrapper = WrappingFactory(self.site)
2747+ self.wrapper.testserver_on_connection_lost = defer.Deferred()
2748+ self.wrapper.protocol = server_protocol_factory(self.wrapper.protocol)
2749+ self.wrapper._disconnecting = False
2750+
2751+ def listen(self, site):
2752+ """Listen a port to allow the tests."""
2753+ raise NotImplementedError('Base abstract class.')
2754+
2755+ def get_iri(self):
2756+ """Build the iri for this mock server."""
2757+ return "{scheme}://127.0.0.1:{port}/".format(scheme=self.scheme,
2758+ port=self.get_port())
2759+
2760+ def get_port(self):
2761+ """Return the port where we are listening."""
2762+ return self.port.getHost().port
2763+
2764+ def start(self):
2765+ """Start the service."""
2766+ self.port = self.listen(self.wrapper)
2767+
2768+ def stop(self):
2769+ """Shut it down."""
2770+ if self.port:
2771+ self.wrapper._disconnecting = True
2772+ connected = self.wrapper.protocols.keys()
2773+ if connected:
2774+ for con in connected:
2775+ con.transport.loseConnection()
2776+ else:
2777+ self.wrapper.testserver_on_connection_lost = \
2778+ defer.succeed(None)
2779+ d = defer.maybeDeferred(self.port.stopListening)
2780+ return defer.gatherResults(
2781+ [d,
2782+ self.wrapper.testserver_on_connection_lost])
2783+ return defer.succeed(None)
2784+
2785+
2786+class HTTPWebServer(BaseWebServer):
2787+ """A Webserver that listens to http connections."""
2788+
2789+ def __init__(self, root_resource):
2790+ """Create a new instance."""
2791+ super(HTTPWebServer, self).__init__(root_resource, 'http')
2792+
2793+ def listen(self, site):
2794+ """Listen a port to allow the tests."""
2795+ return reactor.listenTCP(0, site)
2796+
2797+
2798+class HTTPSWebServer(BaseWebServer):
2799+ """A WebServer that listens to https connections."""
2800+
2801+ def __init__(self, root_resource, ssl_settings=None):
2802+ """Create a new instance."""
2803+ super(HTTPSWebServer, self).__init__(root_resource, 'https')
2804+ self.ssl_settings = ssl_settings
2805+
2806+ def listen(self, site):
2807+ """Listen a port to allow the tests."""
2808+ ssl_context = ssl.DefaultOpenSSLContextFactory(
2809+ self.ssl_settings['key'], self.ssl_settings['cert'])
2810+
2811+ return reactor.listenSSL(0, site, ssl_context)
2812
2813=== added file 'ubuntuone/devtools/utils.py'
2814--- ubuntuone/devtools/utils.py 1970-01-01 00:00:00 +0000
2815+++ ubuntuone/devtools/utils.py 2018-05-30 21:37:37 +0000
2816@@ -0,0 +1,180 @@
2817+# Copyright 2009-2012 Canonical Ltd.
2818+#
2819+# This program is free software: you can redistribute it and/or modify it
2820+# under the terms of the GNU General Public License version 3, as published
2821+# by the Free Software Foundation.
2822+#
2823+# This program is distributed in the hope that it will be useful, but
2824+# WITHOUT ANY WARRANTY; without even the implied warranties of
2825+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2826+# PURPOSE. See the GNU General Public License for more details.
2827+#
2828+# You should have received a copy of the GNU General Public License along
2829+# with this program. If not, see <http://www.gnu.org/licenses/>.
2830+#
2831+# In addition, as a special exception, the copyright holders give
2832+# permission to link the code of portions of this program with the
2833+# OpenSSL library under certain conditions as described in each
2834+# individual source file, and distribute linked combinations
2835+# including the two.
2836+# You must obey the GNU General Public License in all respects
2837+# for all of the code used other than OpenSSL. If you modify
2838+# file(s) with this exception, you may extend this exception to your
2839+# version of the file(s), but you are not obligated to do so. If you
2840+# do not wish to do so, delete this exception statement from your
2841+# version. If you delete this exception statement from all source
2842+# files in the program, then also delete it here.
2843+"""Utilities for Ubuntu One developer tools."""
2844+
2845+from __future__ import print_function, unicode_literals
2846+
2847+import getopt
2848+import sys
2849+
2850+from ubuntuone.devtools.errors import UsageError
2851+__all__ = ['OptionParser']
2852+
2853+
2854+def accumulate_list_attr(class_obj, attr, list_obj, base_class=None):
2855+ """Get all of the list attributes of attr from the class hierarchy,
2856+ and return a list of the lists."""
2857+ for base in class_obj.__bases__:
2858+ accumulate_list_attr(base, attr, list_obj)
2859+ if base_class is None or base_class in class_obj.__bases__:
2860+ list_obj.extend(class_obj.__dict__.get(attr, []))
2861+
2862+
2863+def unpack_padded(length, sequence, default=None):
2864+ """Pads a sequence to length with value of default.
2865+
2866+ Returns a list containing the original and padded values.
2867+ """
2868+ newlist = [default] * length
2869+ newlist[:len(sequence)] = list(sequence)
2870+ return newlist
2871+
2872+
2873+class OptionParser(dict):
2874+ """Base options for our test runner."""
2875+
2876+ def __init__(self, *args, **kwargs):
2877+ super(OptionParser, self).__init__(*args, **kwargs)
2878+
2879+ # Store info about the options and defaults
2880+ self.long_opts = []
2881+ self.short_opts = ''
2882+ self.docs = {}
2883+ self.defaults = {}
2884+ self.synonyms = {}
2885+ self.dispatch = {}
2886+
2887+ # Get the options and defaults
2888+ for _get in [self._get_flags, self._get_params]:
2889+ # We don't use variable 'syns' here. It's just to pad the result.
2890+ # pylint: disable=W0612
2891+ (long_opts, short_opts, docs, defaults, syns, dispatch) = _get()
2892+ # pylint: enable=W0612
2893+ self.long_opts.extend(long_opts)
2894+ self.short_opts = self.short_opts + short_opts
2895+ self.docs.update(docs)
2896+ self.update(defaults)
2897+ self.defaults.update(defaults)
2898+ self.synonyms.update(syns)
2899+ self.dispatch.update(dispatch)
2900+
2901+ # We use some camelcase names for trial compatibility here.
2902+ # pylint: disable=C0103
2903+ def parseOptions(self, options=None):
2904+ """Parse the options."""
2905+ if options is None:
2906+ options = sys.argv[1:]
2907+
2908+ try:
2909+ opts, args = getopt.getopt(options,
2910+ self.short_opts, self.long_opts)
2911+ except getopt.error as e:
2912+ raise UsageError(e)
2913+
2914+ for opt, arg in opts:
2915+ if opt[1] == '-':
2916+ opt = opt[2:]
2917+ else:
2918+ opt = opt[1:]
2919+
2920+ if (opt not in self.synonyms.keys()):
2921+ raise UsageError('No such options: "%s"' % opt)
2922+
2923+ opt = self.synonyms[opt]
2924+ if self.defaults[opt] is False:
2925+ self[opt] = True
2926+ else:
2927+ self.dispatch[opt](arg)
2928+
2929+ try:
2930+ self.parseArgs(*args)
2931+ except TypeError:
2932+ raise UsageError('Wrong number of arguments.')
2933+
2934+ self.postOptions()
2935+
2936+ def postOptions(self):
2937+ """Called after options are parsed."""
2938+
2939+ def parseArgs(self, *args):
2940+ """Override to handle extra arguments specially."""
2941+ # pylint: enable=C0103
2942+
2943+ def _parse_arguments(self, arg_type=None, has_default=False):
2944+ """Parse the arguments as either flags or parameters."""
2945+ long_opts, short_opts = [], ''
2946+ docs, defaults, syns, dispatch = {}, {}, {}, {}
2947+
2948+ _args = []
2949+ accumulate_list_attr(self.__class__, arg_type, _args)
2950+
2951+ for _arg in _args:
2952+ try:
2953+ if has_default:
2954+ l_opt, s_opt, default, doc, _ = unpack_padded(5, _arg)
2955+ else:
2956+ default = False
2957+ l_opt, s_opt, doc, _ = unpack_padded(4, _arg)
2958+ except ValueError:
2959+ raise ValueError('Failed to parse argument: %s' % _arg)
2960+ if not l_opt:
2961+ raise ValueError('An option must have a long name.')
2962+
2963+ opt_m_name = 'opt_' + l_opt.replace('-', '_')
2964+ opt_method = getattr(self, opt_m_name, None)
2965+ if opt_method is not None:
2966+ docs[l_opt] = getattr(opt_method, '__doc__', None)
2967+ dispatch[l_opt] = opt_method
2968+ if docs[l_opt] is None:
2969+ docs[l_opt] = doc
2970+ else:
2971+ docs[l_opt] = doc
2972+ dispatch[l_opt] = lambda arg: True
2973+
2974+ defaults[l_opt] = default
2975+ if has_default:
2976+ long_opts.append(l_opt + '=')
2977+ else:
2978+ long_opts.append(l_opt)
2979+
2980+ syns[l_opt] = l_opt
2981+ if s_opt is not None:
2982+ short_opts = short_opts + s_opt
2983+ if has_default:
2984+ short_opts = short_opts + ':'
2985+ syns[s_opt] = l_opt
2986+
2987+ return long_opts, short_opts, docs, defaults, syns, dispatch
2988+
2989+ def _get_flags(self):
2990+ """Get the flag options."""
2991+ return self._parse_arguments(arg_type='optFlags', has_default=False)
2992+
2993+ def _get_params(self):
2994+ """Get the parameters options."""
2995+ return self._parse_arguments(arg_type='optParameters',
2996+ has_default=True)

Subscribers

People subscribed via source and target branches

to all changes: