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
=== modified file 'Makefile'
--- Makefile 2018-04-14 23:34:20 +0000
+++ Makefile 2018-05-30 21:37:37 +0000
@@ -43,7 +43,7 @@
43 cat dependencies-devel.txt | xargs apt-get install -y --no-install-recommends43 cat dependencies-devel.txt | xargs apt-get install -y --no-install-recommends
4444
45venv: 45venv:
46 virtualenv --system-site-packages $(ENV)46 virtualenv $(ENV)
47 $(ENV)/bin/pip install -r requirements.txt -r requirements-devel.txt47 $(ENV)/bin/pip install -r requirements.txt -r requirements-devel.txt
4848
49lint:49lint:
5050
=== added directory 'contrib/dirspec'
=== added file 'contrib/dirspec/__init__.py'
--- contrib/dirspec/__init__.py 1970-01-01 00:00:00 +0000
+++ contrib/dirspec/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,16 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16"""dirspec package."""
017
=== added file 'contrib/dirspec/basedir.py'
--- contrib/dirspec/basedir.py 1970-01-01 00:00:00 +0000
+++ contrib/dirspec/basedir.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,159 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011-2012 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16"""XDG Base Directory paths."""
17
18from __future__ import unicode_literals, print_function
19
20import os
21
22from dirspec.utils import (default_cache_home,
23 default_config_path, default_config_home,
24 default_data_path, default_data_home,
25 get_env_path, unicode_path)
26
27__all__ = ['xdg_cache_home',
28 'xdg_config_home',
29 'xdg_data_home',
30 'xdg_config_dirs',
31 'xdg_data_dirs',
32 'load_config_paths',
33 'load_data_paths',
34 'load_first_config',
35 'save_config_path',
36 'save_data_path',
37 ]
38
39
40def get_xdg_cache_home():
41 """Get the path for XDG cache directory in user's HOME."""
42 return get_env_path('XDG_CACHE_HOME', default_cache_home)
43
44
45def get_xdg_config_home():
46 """Get the path for XDG config directory in user's HOME."""
47 return get_env_path('XDG_CONFIG_HOME', default_config_home)
48
49
50def get_xdg_data_home():
51 """Get the path for XDG data directory in user's HOME."""
52 return get_env_path('XDG_DATA_HOME', default_data_home)
53
54
55def get_xdg_config_dirs():
56 """Get the paths for the XDG config directories."""
57 result = [get_xdg_config_home()]
58 result.extend([x.encode('utf-8') for x in get_env_path(
59 'XDG_CONFIG_DIRS',
60 default_config_path).decode('utf-8').split(os.pathsep)])
61 return result
62
63
64def get_xdg_data_dirs():
65 """Get the paths for the XDG data directories."""
66 result = [get_xdg_data_home()]
67 result.extend([x.encode('utf-8') for x in get_env_path(
68 'XDG_DATA_DIRS',
69 default_data_path).decode('utf-8').split(os.pathsep)])
70 return result
71
72
73def load_config_paths(*resource):
74 """Iterator of configuration paths.
75
76 Return an iterator which gives each directory named 'resource' in
77 the configuration search path. Information provided by earlier
78 directories should take precedence over later ones (ie, the user's
79 config dir comes first).
80 """
81 resource = os.path.join(*resource)
82 assert not resource.startswith('/')
83 for config_dir in get_xdg_config_dirs():
84 path = os.path.join(config_dir, resource.encode('utf-8'))
85 # access the file system always with unicode
86 # to properly behave in some operating systems
87 if os.path.exists(unicode_path(path)):
88 yield path
89
90
91def load_data_paths(*resource):
92 """Iterator of data paths.
93
94 Return an iterator which gives each directory named 'resource' in
95 the stored data search path. Information provided by earlier
96 directories should take precedence over later ones.
97 """
98 resource = os.path.join(*resource)
99 assert not resource.startswith('/')
100 for data_dir in get_xdg_data_dirs():
101 path = os.path.join(data_dir, resource.encode('utf-8'))
102 # access the file system always with unicode
103 # to properly behave in some operating systems
104 if os.path.exists(unicode_path(path)):
105 yield path
106
107
108def load_first_config(*resource):
109 """Returns the first result from load_config_paths, or None if nothing
110 is found to load.
111 """
112 for path in load_config_paths(*resource):
113 return path
114 return None
115
116
117def save_config_path(*resource):
118 """Path to save configuration.
119
120 Ensure $XDG_CONFIG_HOME/<resource>/ exists, and return its path.
121 'resource' should normally be the name of your application. Use this
122 when SAVING configuration settings. Use the xdg_config_dirs variable
123 for loading.
124 """
125 resource = os.path.join(*resource)
126 assert not resource.startswith('/')
127 path = os.path.join(get_xdg_config_home(), resource.encode('utf-8'))
128 # access the file system always with unicode
129 # to properly behave in some operating systems
130 if not os.path.isdir(unicode_path(path)):
131 os.makedirs(unicode_path(path), 0o700)
132 return path
133
134
135def save_data_path(*resource):
136 """Path to save data.
137
138 Ensure $XDG_DATA_HOME/<resource>/ exists, and return its path.
139 'resource' should normally be the name of your application. Use this
140 when STORING a resource. Use the xdg_data_dirs variable for loading.
141 """
142 resource = os.path.join(*resource)
143 assert not resource.startswith('/')
144 path = os.path.join(get_xdg_data_home(), resource.encode('utf-8'))
145 # access the file system always with unicode
146 # to properly behave in some operating systems
147 if not os.path.isdir(unicode_path(path)):
148 os.makedirs(unicode_path(path), 0o700)
149 return path
150
151
152# pylint: disable=C0103
153xdg_cache_home = get_xdg_cache_home()
154xdg_config_home = get_xdg_config_home()
155xdg_data_home = get_xdg_data_home()
156
157xdg_config_dirs = [x for x in get_xdg_config_dirs() if x]
158xdg_data_dirs = [x for x in get_xdg_data_dirs() if x]
159# pylint: disable=C0103
0160
=== added file 'contrib/dirspec/utils.py'
--- contrib/dirspec/utils.py 1970-01-01 00:00:00 +0000
+++ contrib/dirspec/utils.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,188 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011-2012 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16"""Utilities for multiplatform support of XDG directory handling."""
17
18from __future__ import unicode_literals, print_function
19
20import errno
21import os
22import sys
23
24__all__ = ['user_home',
25 'default_cache_home',
26 'default_config_home',
27 'default_config_path',
28 'default_data_home',
29 'default_data_path',
30 'get_env_path',
31 'get_program_path',
32 'unicode_path',
33 ]
34
35
36def _get_exe_path_frozen_win32(exe_name):
37 """Get path to the helper .exe on packaged windows."""
38 # all the .exes are in the same place on windows:
39 cur_exec_path = os.path.abspath(sys.executable)
40 exe_dir = os.path.dirname(cur_exec_path)
41 return os.path.join(exe_dir, exe_name + ".exe")
42
43
44def _get_exe_path_frozen_darwin(exe_name, app_names):
45 """Get path to the sub-app executable on packaged darwin."""
46
47 sub_app_name = app_names[exe_name]
48 main_app_dir = "".join(__file__.partition(".app")[:-1])
49 main_app_resources_dir = os.path.join(main_app_dir,
50 "Contents",
51 "Resources")
52 exe_bin = os.path.join(main_app_resources_dir,
53 sub_app_name,
54 "Contents", "MacOS",
55 exe_name)
56 return exe_bin
57
58
59def get_program_path(program_name, *args, **kwargs):
60 """Given a program name, returns the path to run that program.
61
62 Raises OSError if the program is not found.
63
64 :param program_name: The name of the program to find. For darwin and win32
65 platforms, the behavior is changed slightly, when sys.frozen is set,
66 to look in the packaged program locations for the program.
67 :param search_dirs: A list of directories to look for the program in. This
68 is only available as a keyword argument.
69 :param app_names: A dict of program names mapped to sub-app names. Used
70 for discovering paths in embedded .app bundles on the darwin platform.
71 This is only available as a keyword argument.
72 :return: The path to the discovered program.
73 """
74 search_dirs = kwargs.get('fallback_dirs', None)
75 app_names = kwargs.get('app_names', None)
76
77 if getattr(sys, "frozen", None) is not None:
78 if sys.platform == 'win32':
79 program_path = _get_exe_path_frozen_win32(program_name)
80 elif sys.platform == 'darwin':
81 program_path = _get_exe_path_frozen_darwin(program_name,
82 app_names)
83 else:
84 raise Exception("Unsupported platform for frozen execution: %r" %
85 sys.platform)
86 else:
87 if search_dirs is not None:
88 for dirname in search_dirs:
89 program_path = os.path.join(dirname, program_name)
90 if os.path.exists(program_path):
91 return program_path
92 else:
93 # Check in normal system $PATH, if no fallback dirs specified
94 from distutils.spawn import find_executable
95 program_path = find_executable(program_name)
96
97 if program_path is None or not os.path.exists(program_path):
98 raise OSError(errno.ENOENT,
99 "Could not find executable %r" % program_name)
100
101 return program_path
102
103
104def get_env_path(key, default):
105 """Get a UTF-8 encoded path from an environment variable."""
106 if key in os.environ:
107 # on windows, environment variables are mbcs bytes
108 # so we must turn them into utf-8 Syncdaemon paths
109 try:
110 path = os.environb.get(key.encode('utf-8'))
111 except AttributeError:
112 path = os.environ[key]
113 return path.decode(sys.getfilesystemencoding()).encode('utf-8')
114 else:
115 if not isinstance(default, bytes):
116 return default.encode('utf-8')
117 return default
118
119
120def unicode_path(utf8path):
121 """Turn an utf8 path into a unicode path."""
122 if isinstance(utf8path, bytes):
123 return utf8path.decode("utf-8")
124 return utf8path
125
126
127def get_special_folders():
128 """ Routine to grab all the Windows Special Folders locations.
129
130 If successful, returns dictionary
131 of shell folder locations indexed on Windows keyword for each;
132 otherwise, returns an empty dictionary.
133 """
134 # pylint: disable=W0621, F0401, E0611
135 special_folders = {}
136
137 if sys.platform == 'win32':
138 from win32com.shell import shell, shellcon
139 # CSIDL_LOCAL_APPDATA = C:\Users\<username>\AppData\Local
140 # CSIDL_PROFILE = C:\Users\<username>
141 # CSIDL_COMMON_APPDATA = C:\ProgramData
142 # More information on these constants at
143 # http://msdn.microsoft.com/en-us/library/bb762494
144
145 # per http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181,
146 # SHGetFolderPath is deprecated, replaced by SHGetKnownFolderPath
147 # (http://msdn.microsoft.com/en-us/library/windows/desktop/bb762188)
148 def get_path(name):
149 return shell.SHGetFolderPath(0, getattr(shellcon, name),
150 None, 0).encode('utf8')
151 special_folders['Personal'] = get_path("CSIDL_PROFILE")
152 special_folders['Local AppData'] = get_path("CSIDL_LOCAL_APPDATA")
153 special_folders['AppData'] = os.path.dirname(
154 special_folders['Local AppData'])
155 special_folders['Common AppData'] = get_path("CSIDL_COMMON_APPDATA")
156
157 return special_folders
158
159
160# pylint: disable=C0103
161if sys.platform == 'win32':
162 special_folders = get_special_folders()
163 user_home = special_folders['Personal']
164 default_config_path = special_folders['Common AppData']
165 default_config_home = special_folders['Local AppData']
166 default_data_path = os.path.join(default_config_path, b'xdg')
167 default_data_home = os.path.join(default_config_home, b'xdg')
168 default_cache_home = os.path.join(default_data_home, b'cache')
169elif sys.platform == 'darwin':
170 user_home = os.path.expanduser(b'~')
171 default_cache_home = os.path.join(user_home, b'Library', b'Caches')
172 default_config_path = b'/Library/Preferences:/etc/xdg'
173 default_config_home = os.path.join(user_home, b'Library', b'Preferences')
174 default_data_path = b':'.join([b'/Library/Application Support',
175 b'/usr/local/share',
176 b'/usr/share'])
177 default_data_home = os.path.join(user_home, b'Library',
178 b'Application Support')
179else:
180 user_home = os.path.expanduser(b'~')
181 default_cache_home = os.path.join(user_home,
182 b'.cache')
183 default_config_path = b'/etc/xdg'
184 default_config_home = os.path.join(user_home,
185 b'.config')
186 default_data_path = b'/usr/local/share:/usr/share'
187 default_data_home = os.path.join(user_home,
188 b'.local', b'share')
0189
=== added file 'contrib/u1trial'
--- contrib/u1trial 1970-01-01 00:00:00 +0000
+++ contrib/u1trial 2018-05-30 21:37:37 +0000
@@ -0,0 +1,40 @@
1#! /usr/bin/python
2#
3# Copyright 2009-2012 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16#
17# In addition, as a special exception, the copyright holders give
18# permission to link the code of portions of this program with the
19# OpenSSL library under certain conditions as described in each
20# individual source file, and distribute linked combinations
21# including the two.
22# You must obey the GNU General Public License in all respects
23# for all of the code used other than OpenSSL. If you modify
24# file(s) with this exception, you may extend this exception to your
25# version of the file(s), but you are not obligated to do so. If you
26# do not wish to do so, delete this exception statement from your
27# version. If you delete this exception statement from all source
28# files in the program, then also delete it here.
29"""Test runner which works with special services and main loops."""
30
31import os
32import sys
33
34sys.path.insert(0, os.path.abspath("."))
35
36from ubuntuone.devtools.runners import main # noqa
37
38
39if __name__ == '__main__':
40 main()
041
=== added file 'data/dbus-session.conf.in'
--- data/dbus-session.conf.in 1970-01-01 00:00:00 +0000
+++ data/dbus-session.conf.in 2018-05-30 21:37:37 +0000
@@ -0,0 +1,63 @@
1<!-- This configuration file controls our test-only session bus -->
2
3<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
4 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
5<busconfig>
6 <!-- We only use a session bus -->
7 <type>session</type>
8
9 <listen>@ADDRESS@</listen>
10
11 <!-- Load our own services.
12 To make other dbus service in this session bus, just add another servicedir entry. -->
13 <servicedir>dbus-session</servicedir>
14 <!-- Load the standard session services -->
15 <!--standard_session_servicedirs /-->
16
17 <policy context="default">
18 <!-- Allow everything to be sent -->
19 <allow send_destination="*" eavesdrop="true"/>
20 <!-- Allow everything to be received -->
21 <allow eavesdrop="true"/>
22 <!-- Allow anyone to own anything -->
23 <allow own="*"/>
24 </policy>
25
26 <!-- Config files are placed here that among other things,
27 further restrict the above policy for specific services. -->
28 <includedir>/etc/dbus-1/session.d</includedir>
29
30 <!-- raise the service start timeout to 40 seconds as it can timeout
31 on the live cd on slow machines -->
32 <limit name="service_start_timeout">60000</limit>
33
34 <!-- This is included last so local configuration can override what's
35 in this standard file -->
36 <include ignore_missing="yes">session-local.conf</include>
37
38 <include ignore_missing="yes" if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
39
40 <!-- For the session bus, override the default relatively-low limits
41 with essentially infinite limits, since the bus is just running
42 as the user anyway, using up bus resources is not something we need
43 to worry about. In some cases, we do set the limits lower than
44 "all available memory" if exceeding the limit is almost certainly a bug,
45 having the bus enforce a limit is nicer than a huge memory leak. But the
46 intent is that these limits should never be hit. -->
47
48 <!-- the memory limits are 1G instead of say 4G because they can't exceed 32-bit signed int max -->
49 <limit name="max_incoming_bytes">1000000000</limit>
50 <limit name="max_outgoing_bytes">1000000000</limit>
51 <limit name="max_message_size">1000000000</limit>
52 <limit name="service_start_timeout">120000</limit>
53 <limit name="auth_timeout">240000</limit>
54 <limit name="max_completed_connections">100000</limit>
55 <limit name="max_incomplete_connections">10000</limit>
56 <limit name="max_connections_per_user">100000</limit>
57 <limit name="max_pending_service_starts">10000</limit>
58 <limit name="max_names_per_connection">50000</limit>
59 <limit name="max_match_rules_per_connection">50000</limit>
60 <limit name="max_replies_per_connection">50000</limit>
61 <limit name="reply_timeout">300000</limit>
62
63</busconfig>
064
=== modified file 'dependencies-devel.txt'
--- dependencies-devel.txt 2018-03-18 11:59:08 +0000
+++ dependencies-devel.txt 2018-05-30 21:37:37 +0000
@@ -1,5 +1,2 @@
1bzr1bzr
2make2make
3python-mocker
4ubuntuone-dev-tools
5virtualenv
63
=== modified file 'dependencies.txt'
--- dependencies.txt 2018-04-14 23:34:20 +0000
+++ dependencies.txt 2018-05-30 21:37:37 +0000
@@ -1,8 +1,5 @@
1gir1.2-soup-2.41libgirepository1.0-dev
2python-configglue2libgtk2.0-dev
3python-dirspec3pkg-config
4python-distutils-extra4python-dev
5python-gi5virtualenv
6python-protobuf
7python-pyinotify
8python-twisted
96
=== modified file 'requirements-devel.txt'
--- requirements-devel.txt 2018-03-18 15:03:09 +0000
+++ requirements-devel.txt 2018-05-30 21:37:37 +0000
@@ -1,1 +1,3 @@
1coverage==3.7.1
1flake8==3.5.02flake8==3.5.0
3mocker==1.1.1
24
=== modified file 'requirements.txt'
--- requirements.txt 2018-04-14 23:34:20 +0000
+++ requirements.txt 2018-05-30 21:37:37 +0000
@@ -1,2 +1,7 @@
1configglue==1.1.3.post0
2dbus-python==1.2.8
3magicicadaprotocol==2.0
4PyGObject==3.28.2
5pyinotify==0.9.6
1Send2Trash==1.5.06Send2Trash==1.5.0
2magicicadaprotocol==2.07Twisted==18.4.0
38
=== modified file 'run-tests'
--- run-tests 2018-03-18 11:59:08 +0000
+++ run-tests 2018-05-30 21:37:37 +0000
@@ -49,5 +49,5 @@
4949
50echo "*** Running test suite for ""$MODULE"" ***"50echo "*** Running test suite for ""$MODULE"" ***"
51export SSL_CERTIFICATES_DIR=/etc/ssl/certs51export SSL_CERTIFICATES_DIR=/etc/ssl/certs
52.env/bin/python /usr/bin/u1trial -i "$IGNORE_FILES" -p "$IGNORE_PATHS" $MODULE52.env/bin/python contrib/u1trial -i "$IGNORE_FILES" -p "$IGNORE_PATHS" $MODULE
53rm -rf _trial_temp53rm -rf _trial_temp
5454
=== modified file 'setup.py'
--- setup.py 2018-04-23 02:21:03 +0000
+++ setup.py 2018-05-30 21:37:37 +0000
@@ -32,15 +32,9 @@
32import os32import os
33import sys33import sys
3434
35try:35from setuptools import setup
36 from DistUtilsExtra.command import build_extra, build_i18n36from setuptools.command.install import install
37 import DistUtilsExtra.auto37from distutils.command import build, clean
38except ImportError:
39 print >> sys.stderr, 'To build this program you need '\
40 'https://launchpad.net/python-distutils-extra'
41 raise
42assert DistUtilsExtra.auto.__version__ >= '2.18', \
43 'needs DistUtilsExtra.auto >= 2.18'
4438
4539
46PROJECT_NAME = 'magicicada-client'40PROJECT_NAME = 'magicicada-client'
@@ -83,7 +77,7 @@
83 out_file.write(content)77 out_file.write(content)
8478
8579
86class Install(DistUtilsExtra.auto.install_auto):80class Install(install):
87 """Class to install proper files."""81 """Class to install proper files."""
8882
89 def run(self):83 def run(self):
@@ -110,7 +104,8 @@
110 prefix = self.install_data.replace(104 prefix = self.install_data.replace(
111 self.root if self.root is not None else '', '')105 self.root if self.root is not None else '', '')
112 replace_variables(SERVICE_FILES, prefix)106 replace_variables(SERVICE_FILES, prefix)
113 DistUtilsExtra.auto.install_auto.run(self)107 install.run(self)
108
114 # Replace the CLIENTDEFS paths here, so that we can do it directly in109 # Replace the CLIENTDEFS paths here, so that we can do it directly in
115 # the installed copy, rather than the lcoal copy. This allows us to110 # the installed copy, rather than the lcoal copy. This allows us to
116 # have a semi-generated version for use in tests, and a full version111 # have a semi-generated version for use in tests, and a full version
@@ -127,7 +122,7 @@
127 out_file.write(content)122 out_file.write(content)
128123
129124
130class Build(build_extra.build_extra):125class Build(build.build):
131 """Build PyQt (.ui) files and resources."""126 """Build PyQt (.ui) files and resources."""
132127
133 description = "build PyQt GUIs (.ui) and resources (.qrc)"128 description = "build PyQt GUIs (.ui) and resources (.qrc)"
@@ -135,10 +130,10 @@
135 def run(self):130 def run(self):
136 """Execute the command."""131 """Execute the command."""
137 replace_variables(BUILD_FILES)132 replace_variables(BUILD_FILES)
138 build_extra.build_extra.run(self)133 build.build.run(self)
139134
140135
141class Clean(DistUtilsExtra.auto.clean_build_tree):136class Clean(clean.clean):
142 """Class to clean up after the build."""137 """Class to clean up after the build."""
143138
144 def run(self):139 def run(self):
@@ -147,24 +142,7 @@
147 if os.path.exists(built_file):142 if os.path.exists(built_file):
148 os.unlink(built_file)143 os.unlink(built_file)
149144
150 DistUtilsExtra.auto.clean_build_tree.run(self)145 clean.clean.run(self)
151
152
153class BuildLocale(build_i18n.build_i18n):
154 """Work around a bug in DistUtilsExtra."""
155
156 def run(self):
157 """Magic."""
158 build_i18n.build_i18n.run(self)
159 i = 0
160 for df in self.distribution.data_files:
161 if df[0].startswith('etc/xdg/'):
162 if sys.platform not in ('darwin', 'win32'):
163 new_df = (df[0].replace('etc/xdg/', '/etc/xdg/'), df[1])
164 self.distribution.data_files[i] = new_df
165 else:
166 self.distribution.data_files.pop(i)
167 i += 1
168146
169147
170def set_py2exe_paths():148def set_py2exe_paths():
@@ -191,10 +169,9 @@
191169
192170
193cmdclass = {171cmdclass = {
194 'install': Install,
195 'build': Build,172 'build': Build,
196 'clean': Clean,173 'clean': Clean,
197 'build_i18n': BuildLocale,174 'install': Install,
198}175}
199176
200bin_scripts = [177bin_scripts = [
@@ -236,7 +213,7 @@
236 scripts.extend(bin_scripts)213 scripts.extend(bin_scripts)
237 extra = {}214 extra = {}
238215
239DistUtilsExtra.auto.setup(216setup(
240 name=PROJECT_NAME,217 name=PROJECT_NAME,
241 version=VERSION,218 version=VERSION,
242 license='GPL v3',219 license='GPL v3',
243220
=== added directory 'ubuntuone/devtools'
=== added file 'ubuntuone/devtools/__init__.py'
--- ubuntuone/devtools/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,1 @@
1"""Testing utilities for Ubuntu One client code."""
02
=== added file 'ubuntuone/devtools/compat.py'
--- ubuntuone/devtools/compat.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/compat.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,49 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2012 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16#
17# In addition, as a special exception, the copyright holders give
18# permission to link the code of portions of this program with the
19# OpenSSL library under certain conditions as described in each
20# individual source file, and distribute linked combinations
21# including the two.
22# You must obey the GNU General Public License in all respects
23# for all of the code used other than OpenSSL. If you modify
24# file(s) with this exception, you may extend this exception to your
25# version of the file(s), but you are not obligated to do so. If you
26# do not wish to do so, delete this exception statement from your
27# version. If you delete this exception statement from all source
28# files in the program, then also delete it here.
29"""Python 2 and 3 compatibility."""
30
31from __future__ import unicode_literals
32
33# The following approach was outlined in Lennart Regebro's
34# "Porting to Python 3" book.
35# http://python3porting.com/noconv.html#more-bytes-strings-and-unicode
36
37import sys
38
39# Disable redefined builtin, invalid name warning
40# pylint: disable=W0622,C0103
41
42if sys.version_info < (3,):
43 text_type = unicode
44 binary_type = str
45 basestring = basestring
46else:
47 text_type = str
48 binary_type = bytes
49 basestring = str
050
=== added file 'ubuntuone/devtools/errors.py'
--- ubuntuone/devtools/errors.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/errors.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,35 @@
1# Copyright 2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""Custom error types for Ubuntu One developer tools."""
28
29
30class TestError(Exception):
31 """An error occurred in attempting to load or start the tests."""
32
33
34class UsageError(Exception):
35 """An error occurred in parsing the command line arguments."""
036
=== added file 'ubuntuone/devtools/handlers.py'
--- ubuntuone/devtools/handlers.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/handlers.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,97 @@
1# -*- coding: utf-8 -*-
2
3# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
4# Author: Facundo Batista <facundo@canonical.com>
5#
6# Copyright 2009-2012 Canonical Ltd.
7#
8# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published
10# by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranties of
14# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15# PURPOSE. See the GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20# In addition, as a special exception, the copyright holders give
21# permission to link the code of portions of this program with the
22# OpenSSL library under certain conditions as described in each
23# individual source file, and distribute linked combinations
24# including the two.
25# You must obey the GNU General Public License in all respects
26# for all of the code used other than OpenSSL. If you modify
27# file(s) with this exception, you may extend this exception to your
28# version of the file(s), but you are not obligated to do so. If you
29# do not wish to do so, delete this exception statement from your
30# version. If you delete this exception statement from all source
31# files in the program, then also delete it here.
32"""Set of helpers handlers."""
33
34from __future__ import print_function
35
36import logging
37
38
39class MementoHandler(logging.Handler):
40 """ A handler class which store logging records in a list """
41
42 def __init__(self, *args, **kwargs):
43 """ Create the instance, and add a records attribute. """
44 logging.Handler.__init__(self, *args, **kwargs)
45 self.records = []
46 self.debug = False
47
48 def emit(self, record):
49 """ Just add the record to self.records. """
50 self.format(record)
51 self.records.append(record)
52
53 def dump_contents(self, msgs):
54 """Dumps the contents of the MementoHandler."""
55 if self.debug:
56 print("Expecting:")
57 for msg in msgs:
58 print("\t", msg)
59 print("MementoHandler contents:")
60 for rec in self.records:
61 print("\t", rec.exc_info)
62 print("\t", logging.getLevelName(rec.levelno))
63 print("\t\t", rec.message)
64 print("\t\t", rec.exc_text)
65
66 def check(self, level, *msgs):
67 """Verifies that the msgs are logged in the specified level"""
68 for rec in self.records:
69 if rec.levelno == level and all(m in rec.message for m in msgs):
70 return rec
71 self.dump_contents(msgs)
72 return False
73
74 def check_debug(self, *msgs):
75 """Shortcut for checking in DEBUG."""
76 return self.check(logging.DEBUG, *msgs)
77
78 def check_info(self, *msgs):
79 """Shortcut for checking in INFO."""
80 return self.check(logging.INFO, *msgs)
81
82 def check_warning(self, *msgs):
83 """Shortcut for checking in WARNING."""
84 return self.check(logging.WARNING, *msgs)
85
86 def check_error(self, *msgs):
87 """Shortcut for checking in ERROR."""
88 return self.check(logging.ERROR, *msgs)
89
90 def check_exception(self, exception_info, *msgs):
91 """Shortcut for checking exceptions."""
92 for rec in self.records:
93 if rec.levelno == logging.ERROR and \
94 all(m in rec.exc_text + rec.message for m in msgs) and \
95 exception_info in rec.exc_info:
96 return True
97 return False
098
=== added directory 'ubuntuone/devtools/reactors'
=== added file 'ubuntuone/devtools/reactors/__init__.py'
--- ubuntuone/devtools/reactors/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/reactors/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,1 @@
1"""Twisted reactors for testing."""
02
=== added file 'ubuntuone/devtools/reactors/gi.py'
--- ubuntuone/devtools/reactors/gi.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/reactors/gi.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,53 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""The introspection based main loop integration reactor for testing."""
28
29REACTOR_URL = 'http://twistedmatrix.com/trac/ticket/4558'
30
31
32def load_reactor(reactor_name=None):
33 """Load the reactor module and return it."""
34 return __import__(reactor_name, None, None, [''])
35
36
37def install(options=None):
38 """Install the reactor and parse any options we might need."""
39 reactor = None
40 if options is not None and options['gui']:
41 try:
42 reactor = load_reactor('twisted.internet.gtk3reactor')
43 except ImportError:
44 print('Falling back to gtk2reactor module.')
45 reactor = load_reactor('twisted.internet.gtk2reactor')
46 else:
47 try:
48 reactor = load_reactor('twisted.internet.gireactor')
49 except ImportError:
50 print('Falling back to glib2reactor module.')
51 reactor = load_reactor('twisted.internet.glib2reactor')
52
53 reactor.install()
054
=== added directory 'ubuntuone/devtools/runners'
=== added file 'ubuntuone/devtools/runners/__init__.py'
--- ubuntuone/devtools/runners/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/runners/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,304 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""The base test runner object."""
28
29from __future__ import print_function, unicode_literals
30
31import coverage
32import gc
33import inspect
34import os
35import re
36import sys
37import unittest
38
39from ubuntuone.devtools.errors import TestError, UsageError
40from ubuntuone.devtools.testing.txcheck import TXCheckSuite
41from ubuntuone.devtools.utils import OptionParser
42from ubuntuone.devtools.compat import text_type
43
44__all__ = ['BaseTestOptions', 'BaseTestRunner', 'main']
45
46
47def _is_in_ignored_path(testcase, paths):
48 """Return if the testcase is in one of the ignored paths."""
49 for ignored_path in paths:
50 if testcase.startswith(ignored_path):
51 return True
52 return False
53
54
55class BaseTestRunner(object):
56 """The base test runner type. Does not actually run tests."""
57
58 def __init__(self, options=None, *args, **kwargs):
59 super(BaseTestRunner, self).__init__(*args, **kwargs)
60
61 # set $HOME to the _trial_temp dir, to avoid breaking user files
62 trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())
63 homedir = os.path.join(trial_temp_dir, options['temp-directory'])
64 os.environ['HOME'] = homedir
65
66 # setup $XDG_*_HOME variables and create the directories
67 xdg_cache = os.path.join(homedir, 'xdg_cache')
68 xdg_config = os.path.join(homedir, 'xdg_config')
69 xdg_data = os.path.join(homedir, 'xdg_data')
70 os.environ['XDG_CACHE_HOME'] = xdg_cache
71 os.environ['XDG_CONFIG_HOME'] = xdg_config
72 os.environ['XDG_DATA_HOME'] = xdg_data
73
74 if not os.path.exists(xdg_cache):
75 os.makedirs(xdg_cache)
76 if not os.path.exists(xdg_config):
77 os.makedirs(xdg_config)
78 if not os.path.exists(xdg_data):
79 os.makedirs(xdg_data)
80
81 # setup the ROOTDIR env var
82 os.environ['ROOTDIR'] = os.getcwd()
83
84 # Need an attribute for tempdir so we can use it later
85 self.tempdir = homedir
86 self.working_dir = os.path.join(self.tempdir, 'trial')
87
88 self.source_files = []
89 self.required_services = []
90
91 def _load_unittest(self, relpath):
92 """Load unit tests from a Python module with the given 'relpath'."""
93 assert relpath.endswith(".py"), (
94 "%s does not appear to be a Python module" % relpath)
95 if not os.path.basename(relpath).startswith('test_'):
96 return
97 modpath = relpath.replace(os.path.sep, ".")[:-3]
98 module = __import__(modpath, None, None, [""])
99
100 # If the module specifies required_services, make sure we get them
101 members = [x[1] for x in inspect.getmembers(module, inspect.isclass)]
102 for member_type in members:
103 if hasattr(member_type, 'required_services'):
104 member = member_type()
105 for service in member.required_services():
106 if service not in self.required_services:
107 self.required_services.append(service)
108 del member
109 gc.collect()
110
111 # If the module has a 'suite' or 'test_suite' function, use that
112 # to load the tests.
113 if hasattr(module, "suite"):
114 return module.suite()
115 elif hasattr(module, "test_suite"):
116 return module.test_suite()
117 else:
118 return unittest.defaultTestLoader.loadTestsFromModule(module)
119
120 def _collect_tests(self, path, test_pattern, ignored_modules,
121 ignored_paths):
122 """Return the set of unittests."""
123 suite = TXCheckSuite()
124 if test_pattern:
125 pattern = re.compile('.*%s.*' % test_pattern)
126 else:
127 pattern = None
128
129 # Disable this lint warning as we need to access _tests in the
130 # test suites, to collect the tests
131 # pylint: disable=W0212
132 if path:
133 try:
134 module_suite = self._load_unittest(path)
135 if pattern:
136 for inner_suite in module_suite._tests:
137 for test in inner_suite._tests:
138 if pattern.match(test.id()):
139 suite.addTest(test)
140 else:
141 suite.addTests(module_suite)
142 return suite
143 except AssertionError:
144 pass
145 else:
146 raise TestError('Path should be defined.')
147
148 # We don't use the dirs variable, so ignore the warning
149 # pylint: disable=W0612
150 for root, dirs, files in os.walk(path):
151 for test in files:
152 filepath = os.path.join(root, test)
153 if test.endswith(".py") and test not in ignored_modules and \
154 not _is_in_ignored_path(filepath, ignored_paths):
155 self.source_files.append(filepath)
156 if test.startswith("test_"):
157 module_suite = self._load_unittest(filepath)
158 if pattern:
159 for inner_suite in module_suite._tests:
160 for test in inner_suite._tests:
161 if pattern.match(test.id()):
162 suite.addTest(test)
163 else:
164 suite.addTests(module_suite)
165 return suite
166
167 def get_suite(self, config):
168 """Get the test suite to use."""
169 suite = unittest.TestSuite()
170 for path in config['tests']:
171 suite.addTest(self._collect_tests(path, config['test'],
172 config['ignore-modules'],
173 config['ignore-paths']))
174 if config['loop']:
175 old_suite = suite
176 suite = unittest.TestSuite()
177 for _ in range(config['loop']):
178 suite.addTest(old_suite)
179
180 return suite
181
182 def run_tests(self, suite):
183 """Run the test suite."""
184 return False
185
186
187class BaseTestOptions(OptionParser):
188 """Base options for our test runner."""
189
190 optFlags = [['coverage', 'c', 'Generate a coverage report for the tests.'],
191 ['gui', None, 'Use the GUI mode of some runners.'],
192 ['help', 'h', ''],
193 ['help-runners', None, 'List information about test runners.'],
194 ]
195
196 optParameters = [['test', 't', None, None],
197 ['loop', None, 1, None],
198 ['ignore-modules', 'i', '', None],
199 ['ignore-paths', 'p', '', None],
200 ['runner', None, 'txrunner', None],
201 ['temp-directory', None, b'_trial_temp', None],
202 ]
203
204 def __init__(self, *args, **kwargs):
205 super(BaseTestOptions, self).__init__(*args, **kwargs)
206
207 def opt_help_runners(self):
208 """List the runners which are supported."""
209 sys.exit(0)
210
211 def opt_ignore_modules(self, option):
212 """Comma-separate list of test modules to ignore,
213 e.g: test_gtk.py, test_account.py
214 """
215 self['ignore-modules'] = list(map(text_type.strip, option.split(',')))
216
217 def opt_ignore_paths(self, option):
218 """Comma-separated list of relative paths to ignore,
219 e.g: tests/platform/windows, tests/platform/macosx
220 """
221 self['ignore-paths'] = list(map(text_type.strip, option.split(',')))
222
223 def opt_loop(self, option):
224 """Loop tests the specified number of times."""
225 try:
226 self['loop'] = int(option)
227 except ValueError:
228 raise UsageError('A positive integer value must be specified.')
229
230 def opt_temp_directory(self, option):
231 """Path to use as a working directory for tests.
232 [default: _trial_temp]
233 """
234 self['temp-directory'] = option
235
236 def opt_test(self, option):
237 """Run specific tests, e.g: className.methodName"""
238 self['test'] = option
239
240 # We use some camelcase names for trial compatibility here.
241 def parseArgs(self, *args):
242 """Handle the extra arguments."""
243 if isinstance(self.tests, set):
244 self['tests'].update(args)
245 elif isinstance(self.tests, list):
246 self['tests'].extend(args)
247
248
249def _get_runner_options(runner_name):
250 """Return the test runner module, and its options object."""
251 module_name = 'ubuntuone.devtools.runners.%s' % runner_name
252 runner = __import__(module_name, None, None, [''])
253 options = None
254 if getattr(runner, 'TestOptions', None) is not None:
255 options = runner.TestOptions()
256 if options is None:
257 options = BaseTestOptions()
258 return (runner, options)
259
260
261def main():
262 """Do the deed."""
263 if len(sys.argv) == 1:
264 sys.argv.append('--help')
265
266 try:
267 pos = sys.argv.index('--runner')
268 runner_name = sys.argv.pop(pos + 1)
269 sys.argv.pop(pos)
270 except ValueError:
271 runner_name = 'txrunner'
272 finally:
273 runner, options = _get_runner_options(runner_name)
274 options.parseOptions()
275
276 test_runner = runner.TestRunner(options=options)
277 suite = test_runner.get_suite(options)
278
279 if options['coverage']:
280 coverage.erase()
281 coverage.start()
282
283 running_services = []
284
285 succeeded = False
286 try:
287 # Start any required services
288 for service_obj in test_runner.required_services:
289 service = service_obj()
290 service.start_service(tempdir=test_runner.tempdir)
291 running_services.append(service)
292
293 succeeded = test_runner.run_tests(suite)
294 finally:
295 # Stop all the running services
296 for service in running_services:
297 service.stop_service()
298
299 if options['coverage']:
300 coverage.stop()
301 coverage.report(test_runner.source_files, ignore_errors=True,
302 show_missing=False)
303
304 sys.exit(not succeeded)
0305
=== added file 'ubuntuone/devtools/runners/txrunner.py'
--- ubuntuone/devtools/runners/txrunner.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/runners/txrunner.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,133 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""The twisted test runner and options."""
28
29from __future__ import print_function, unicode_literals
30
31import sys
32
33from twisted.scripts import trial
34from twisted.trial.runner import TrialRunner
35
36from ubuntuone.devtools.errors import TestError
37from ubuntuone.devtools.runners import BaseTestOptions, BaseTestRunner
38
39__all__ = ['TestRunner', 'TestOptions']
40
41
42class TestRunner(BaseTestRunner, TrialRunner):
43 """The twisted test runner implementation."""
44
45 def __init__(self, options=None):
46 # Handle running trial in debug or dry-run mode
47 self.config = options
48
49 try:
50 reactor_name = ('ubuntuone.devtools.reactors.%s' %
51 self.config['reactor'])
52 reactor = __import__(reactor_name, None, None, [''])
53 except ImportError:
54 raise TestError('The specified reactor is not supported.')
55 else:
56 try:
57 reactor.install(options=self.config)
58 except ImportError:
59 raise TestError(
60 'The Python package providing the requested reactor is '
61 'not installed. You can find it here: %s' %
62 reactor.REACTOR_URL)
63
64 mode = None
65 if self.config['debug']:
66 mode = TrialRunner.DEBUG
67 if self.config['dry-run']:
68 mode = TrialRunner.DRY_RUN
69
70 # Hook up to the parent test runner
71 super(TestRunner, self).__init__(
72 options=options,
73 reporterFactory=self.config['reporter'],
74 mode=mode,
75 profile=self.config['profile'],
76 logfile=self.config['logfile'],
77 tracebackFormat=self.config['tbformat'],
78 realTimeErrors=self.config['rterrors'],
79 uncleanWarnings=self.config['unclean-warnings'],
80 forceGarbageCollection=self.config['force-gc'])
81 # Named for trial compatibility.
82 # pylint: disable=C0103
83 self.workingDirectory = self.working_dir
84 # pylint: enable=C0103
85
86 def run_tests(self, suite):
87 """Run the twisted test suite."""
88 if self.config['until-failure']:
89 result = self.runUntilFailure(suite)
90 else:
91 result = self.run(suite)
92 return result.wasSuccessful()
93
94
95def _get_default_reactor():
96 """Return the platform-dependent default reactor to use."""
97 default_reactor = 'gi'
98 if sys.platform in ['darwin', 'win32']:
99 default_reactor = 'twisted'
100 return default_reactor
101
102
103class TestOptions(trial.Options, BaseTestOptions):
104 """Class for twisted options handling."""
105
106 optFlags = [["help-reactors", None],
107 ]
108
109 optParameters = [["reactor", "r", _get_default_reactor()],
110 ]
111
112 def __init__(self, *args, **kwargs):
113 super(TestOptions, self).__init__(*args, **kwargs)
114 self['rterrors'] = True
115
116 def opt_coverage(self, option):
117 """Handle special flags."""
118 self['coverage'] = True
119 opt_c = opt_coverage
120
121 def opt_help_reactors(self):
122 """Help on available reactors for use with tests"""
123 synopsis = ('')
124 print(synopsis)
125 print('Need to get list of reactors and print them here.\n')
126 sys.exit(0)
127
128 def opt_reactor(self, option):
129 """Which reactor to use (see --help-reactors for a list
130 of possibilities)
131 """
132 self['reactor'] = option
133 opt_r = opt_reactor
0134
=== added directory 'ubuntuone/devtools/services'
=== added file 'ubuntuone/devtools/services/__init__.py'
--- ubuntuone/devtools/services/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/services/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,66 @@
1#
2# Copyright 2011-2012 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program. If not, see <http://www.gnu.org/licenses/>.
15#
16# In addition, as a special exception, the copyright holders give
17# permission to link the code of portions of this program with the
18# OpenSSL library under certain conditions as described in each
19# individual source file, and distribute linked combinations
20# including the two.
21# You must obey the GNU General Public License in all respects
22# for all of the code used other than OpenSSL. If you modify
23# file(s) with this exception, you may extend this exception to your
24# version of the file(s), but you are not obligated to do so. If you
25# do not wish to do so, delete this exception statement from your
26# version. If you delete this exception statement from all source
27# files in the program, then also delete it here.
28"""Service runners for testing."""
29
30import os
31import socket
32
33from dirspec.basedir import load_data_paths
34
35
36def find_config_file(in_config_file):
37 """Find the first appropriate conf to use."""
38 # In case we're running from within the source tree
39 path = os.path.abspath(os.path.join(os.path.dirname(__file__),
40 os.path.pardir, os.path.pardir,
41 os.path.pardir,
42 "data", in_config_file))
43 if not os.path.exists(path):
44 # Use the installed file in $pkgdatadir as source
45 for path in load_data_paths("ubuntuone-dev-tools", in_config_file):
46 if os.path.exists(path):
47 break
48
49 # Check to make sure we didn't just fall out of the loop
50 if not os.path.exists(path):
51 raise IOError('Could not locate suitable %s' % in_config_file)
52 return path
53
54
55def get_arbitrary_port():
56 """
57 Find an unused port, and return it.
58
59 There might be a small race condition here, but we aren't
60 worried about it.
61 """
62 sock = socket.socket()
63 sock.bind(('localhost', 0))
64 _, port = sock.getsockname()
65 sock.close()
66 return port
067
=== added file 'ubuntuone/devtools/services/dbus.py'
--- ubuntuone/devtools/services/dbus.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/services/dbus.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,121 @@
1#
2# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
3#
4# Copyright 2009-2012 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published
8# by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranties of
12# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
13# PURPOSE. See the GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program. If not, see <http://www.gnu.org/licenses/>.
17#
18# In addition, as a special exception, the copyright holders give
19# permission to link the code of portions of this program with the
20# OpenSSL library under certain conditions as described in each
21# individual source file, and distribute linked combinations
22# including the two.
23# You must obey the GNU General Public License in all respects
24# for all of the code used other than OpenSSL. If you modify
25# file(s) with this exception, you may extend this exception to your
26# version of the file(s), but you are not obligated to do so. If you
27# do not wish to do so, delete this exception statement from your
28# version. If you delete this exception statement from all source
29# files in the program, then also delete it here.
30"""Utilities for finding and running a dbus session bus for testing."""
31
32from __future__ import unicode_literals
33
34import os
35import signal
36import subprocess
37
38from distutils.spawn import find_executable
39
40# pylint: disable=F0401,E0611
41try:
42 from urllib.parse import quote
43except ImportError:
44 from urllib import quote
45# pylint: enable=F0401,E0611
46
47from ubuntuone.devtools.services import find_config_file
48DBUS_CONFIG_FILE = 'dbus-session.conf.in'
49
50
51class DBusLaunchError(Exception):
52 """Error while launching dbus-daemon"""
53 pass
54
55
56class NotFoundError(Exception):
57 """Not found error"""
58 pass
59
60
61class DBusRunner(object):
62 """Class for running dbus-daemon with a private session."""
63
64 def __init__(self):
65 self.dbus_address = None
66 self.dbus_pid = None
67 self.running = False
68 self.config_file = None
69
70 def _generate_config_file(self, tempdir=None):
71 """Find the first appropriate dbus-session.conf to use."""
72 # load the config file
73 path = find_config_file(DBUS_CONFIG_FILE)
74 # replace config settings
75 self.config_file = os.path.join(tempdir, 'dbus-session.conf')
76 dbus_address = 'unix:tmpdir=%s' % quote(tempdir)
77 with open(path) as in_file:
78 content = in_file.read()
79 with open(self.config_file, 'w') as out_file:
80 out_file.write(content.replace('@ADDRESS@', dbus_address))
81
82 def start_service(self, tempdir=None):
83 """Start our own session bus daemon for testing."""
84 dbus = find_executable("dbus-daemon")
85 if not dbus:
86 raise NotFoundError("dbus-daemon was not found.")
87
88 self._generate_config_file(tempdir)
89
90 dbus_args = ["--fork",
91 "--config-file=" + self.config_file,
92 "--print-address=1",
93 "--print-pid=2"]
94 sp = subprocess.Popen([dbus] + dbus_args,
95 bufsize=4096, stdout=subprocess.PIPE,
96 stderr=subprocess.PIPE)
97
98 # Call wait here as under the qt4 reactor we get an error about
99 # interrupted system call if we don't.
100 sp.wait()
101 self.dbus_address = b"".join(sp.stdout.readlines()).strip()
102 self.dbus_pid = int(b"".join(sp.stderr.readlines()).strip())
103
104 if self.dbus_address != "":
105 os.environ["DBUS_SESSION_BUS_ADDRESS"] = \
106 self.dbus_address.decode("utf8")
107 else:
108 os.kill(self.dbus_pid, signal.SIGKILL)
109 raise DBusLaunchError("There was a problem launching dbus-daemon.")
110 self.running = True
111
112 def stop_service(self):
113 """Stop our DBus session bus daemon."""
114 try:
115 del os.environ["DBUS_SESSION_BUS_ADDRESS"]
116 except KeyError:
117 pass
118 os.kill(self.dbus_pid, signal.SIGKILL)
119 self.running = False
120 os.unlink(self.config_file)
121 self.config_file = None
0122
=== added directory 'ubuntuone/devtools/testcases'
=== added file 'ubuntuone/devtools/testcases/__init__.py'
--- ubuntuone/devtools/testcases/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testcases/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,187 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2009-2012 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16#
17# In addition, as a special exception, the copyright holders give
18# permission to link the code of portions of this program with the
19# OpenSSL library under certain conditions as described in each
20# individual source file, and distribute linked combinations
21# including the two.
22# You must obey the GNU General Public License in all respects
23# for all of the code used other than OpenSSL. If you modify
24# file(s) with this exception, you may extend this exception to your
25# version of the file(s), but you are not obligated to do so. If you
26# do not wish to do so, delete this exception statement from your
27# version. If you delete this exception statement from all source
28# files in the program, then also delete it here.
29"""Base tests cases and test utilities."""
30
31from __future__ import with_statement
32
33import contextlib
34import os
35import shutil
36import sys
37
38from functools import wraps
39
40from twisted.trial.unittest import TestCase, SkipTest
41
42
43@contextlib.contextmanager
44def environ(env_var, new_value):
45 """context manager to replace/add an environ value"""
46 old_value = os.environ.get(env_var, None)
47 os.environ[env_var] = new_value
48 yield
49 if old_value is None:
50 os.environ.pop(env_var)
51 else:
52 os.environ[env_var] = old_value
53
54
55def _id(obj):
56 """Return the obj calling the funct."""
57 return obj
58
59
60# pylint: disable=C0103
61def skipTest(reason):
62 """Unconditionally skip a test."""
63
64 def decorator(test_item):
65 """Decorate the test so that it is skipped."""
66 if not (isinstance(test_item, type) and
67 issubclass(test_item, TestCase)):
68
69 @wraps(test_item)
70 def skip_wrapper(*args, **kwargs):
71 """Skip a test method raising an exception."""
72 raise SkipTest(reason)
73 test_item = skip_wrapper
74
75 # tell twisted.trial.unittest to skip the test, pylint will complain
76 # since it thinks we are redefining a name out of the scope
77 # pylint: disable=W0621,W0612
78 test_item.skip = reason
79 # pylint: enable=W0621,W0612
80 # because the item was skipped, we will make sure that no
81 # services are started for it
82 if hasattr(test_item, "required_services"):
83 # pylint: disable=W0612
84 test_item.required_services = lambda *args, **kwargs: []
85 # pylint: enable=W0612
86 return test_item
87 return decorator
88
89
90def skipIf(condition, reason):
91 """Skip a test if the condition is true."""
92 if condition:
93 return skipTest(reason)
94 return _id
95
96
97def skipIfOS(current_os, reason):
98 """Skip test for a particular os or lists of them."""
99 if os:
100 if sys.platform in current_os or sys.platform == current_os:
101 return skipTest(reason)
102 return _id
103 return _id
104
105
106def skipIfNotOS(current_os, reason):
107 """Skip test we are not in a particular os."""
108 if os:
109 if sys.platform not in current_os or \
110 sys.platform != current_os:
111 return skipTest(reason)
112 return _id
113 return _id
114
115
116def skipIfJenkins(current_os, reason):
117 """Skip test for a particular os or lists of them
118 when running on Jenkins."""
119 if os.getenv("JENKINS", False) and (sys.platform in current_os or
120 sys.platform == current_os):
121 return skipTest(reason)
122 return _id
123
124
125# pylint: enable=C0103
126
127
128class BaseTestCase(TestCase):
129 """Base TestCase with helper methods to handle temp dir.
130
131 This class provides:
132 mktemp(name): helper to create temporary dirs
133 rmtree(path): support read-only shares
134 makedirs(path): support read-only shares
135
136 """
137
138 def required_services(self):
139 """Return the list of required services for DBusTestCase."""
140 return []
141
142 def mktemp(self, name='temp'):
143 """Customized mktemp that accepts an optional name argument."""
144 tempdir = os.path.join(self.tmpdir, name)
145 if os.path.exists(tempdir):
146 self.rmtree(tempdir)
147 self.makedirs(tempdir)
148 return tempdir
149
150 @property
151 def tmpdir(self):
152 """Default tmpdir: module/class/test_method."""
153 # check if we already generated the root path
154 root_dir = getattr(self, '__root', None)
155 if root_dir:
156 return root_dir
157 max_filename = 32 # some platforms limit lengths of filenames
158 base = os.path.join(self.__class__.__module__[:max_filename],
159 self.__class__.__name__[:max_filename],
160 self._testMethodName[:max_filename])
161 # use _trial_temp dir, it should be os.gwtcwd()
162 # define the root temp dir of the testcase, pylint: disable=W0201
163 self.__root = os.path.join(os.getcwd(), base)
164 return self.__root
165
166 def rmtree(self, path):
167 """Custom rmtree that handle ro parent(s) and childs."""
168 if not os.path.exists(path):
169 return
170 # change perms to rw, so we can delete the temp dir
171 if path != getattr(self, '__root', None):
172 os.chmod(os.path.dirname(path), 0o755)
173 if not os.access(path, os.W_OK):
174 os.chmod(path, 0o755)
175 # pylint: disable=W0612
176 for dirpath, dirs, files in os.walk(path):
177 for dirname in dirs:
178 if not os.access(os.path.join(dirpath, dirname), os.W_OK):
179 os.chmod(os.path.join(dirpath, dirname), 0o777)
180 shutil.rmtree(path)
181
182 def makedirs(self, path):
183 """Custom makedirs that handle ro parent."""
184 parent = os.path.dirname(path)
185 if os.path.exists(parent):
186 os.chmod(parent, 0o755)
187 os.makedirs(path)
0188
=== added file 'ubuntuone/devtools/testcases/dbus.py'
--- ubuntuone/devtools/testcases/dbus.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testcases/dbus.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,138 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2009-2012 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16#
17# In addition, as a special exception, the copyright holders give
18# permission to link the code of portions of this program with the
19# OpenSSL library under certain conditions as described in each
20# individual source file, and distribute linked combinations
21# including the two.
22# You must obey the GNU General Public License in all respects
23# for all of the code used other than OpenSSL. If you modify
24# file(s) with this exception, you may extend this exception to your
25# version of the file(s), but you are not obligated to do so. If you
26# do not wish to do so, delete this exception statement from your
27# version. If you delete this exception statement from all source
28# files in the program, then also delete it here.
29"""Base dbus tests cases and test utilities."""
30
31from __future__ import absolute_import, with_statement
32
33import os
34
35from twisted.internet import defer
36
37from ubuntuone.devtools.testcases import BaseTestCase, skipIf
38
39# DBusRunner for DBusTestCase using tests
40from ubuntuone.devtools.services.dbus import DBusRunner
41
42
43# pylint: disable=F0401,C0103,W0406,E0611
44try:
45 import dbus
46except ImportError as e:
47 dbus = None
48
49try:
50 import dbus.service as service
51except ImportError:
52 service = None
53
54try:
55 from dbus.mainloop.glib import DBusGMainLoop
56except ImportError:
57 DBusGMainLoop = None
58
59# pylint: enable=F0401,C0103,W0406,E0611
60
61
62class InvalidSessionBus(Exception):
63 """Error when we are connected to the wrong session bus in tests."""
64
65
66class FakeDBusInterface(object):
67 """A fake DBusInterface..."""
68
69 def shutdown(self, with_restart=False):
70 """...that only knows how to go away"""
71
72
73@skipIf(dbus is None or service is None or DBusGMainLoop is None,
74 "The test requires dbus.")
75class DBusTestCase(BaseTestCase):
76 """Test the DBus event handling."""
77
78 def required_services(self):
79 """Return the list of required services for DBusTestCase."""
80 services = super(DBusTestCase, self).required_services()
81 services.extend([DBusRunner])
82 return services
83
84 @defer.inlineCallbacks
85 def setUp(self):
86 """Setup the infrastructure fo the test (dbus service)."""
87 # Class 'BaseTestCase' has no 'setUp' member
88 # pylint: disable=E1101
89 # dbus modules will be imported by the decorator
90 # pylint: disable=E0602
91 yield super(DBusTestCase, self).setUp()
92
93 # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here
94 # pylint: disable=F0401,E0611
95 try:
96 from urllib.parse import unquote
97 except ImportError:
98 from urllib import unquote
99 # pylint: enable=F0401,E0611
100
101 bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
102 if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
103 != os.path.dirname(os.getcwd()):
104 raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRESS is wrong.')
105
106 # Set up the main loop and bus connection
107 self.loop = DBusGMainLoop(set_as_default=True)
108
109 # NOTE: The address_or_type value must remain explicitly as
110 # str instead of anything from ubuntuone.devtools.compat. dbus
111 # expects this to be str regardless of version.
112 self.bus = dbus.bus.BusConnection(address_or_type=str(bus_address),
113 mainloop=self.loop)
114
115 # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we
116 # always point at our own private bus instance.
117 self.patch(dbus, 'SessionBus', lambda: self.bus)
118 self.patch(dbus, 'SystemBus', lambda: self.bus)
119
120 # Check that we are on the correct bus for real
121# Disable this for now, because our tests are extremely broken :(
122# bus_names = self.bus.list_names()
123# if len(bus_names) > 2:
124# raise InvalidSessionBus('Too many bus connections: %s (%r)' %
125# (len(bus_names), bus_names))
126
127 # monkeypatch busName.__del__ to avoid errors on gc
128 # we take care of releasing the name in shutdown
129 service.BusName.__del__ = lambda _: None
130 yield self.bus.set_exit_on_disconnect(False)
131 self.signal_receivers = set()
132
133 @defer.inlineCallbacks
134 def tearDown(self):
135 """Cleanup the test."""
136 yield self.bus.flush()
137 yield self.bus.close()
138 yield super(DBusTestCase, self).tearDown()
0139
=== added file 'ubuntuone/devtools/testcases/txsocketserver.py'
--- ubuntuone/devtools/testcases/txsocketserver.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testcases/txsocketserver.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,357 @@
1# -*- coding: utf-8 -*-
2# Copyright 2012 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program. If not, see <http://www.gnu.org/licenses/>.
15#
16# In addition, as a special exception, the copyright holders give
17# permission to link the code of portions of this program with the
18# OpenSSL library under certain conditions as described in each
19# individual source file, and distribute linked combinations
20# including the two.
21# You must obey the GNU General Public License in all respects
22# for all of the code used other than OpenSSL. If you modify
23# file(s) with this exception, you may extend this exception to your
24# version of the file(s), but you are not obligated to do so. If you
25# do not wish to do so, delete this exception statement from your
26# version. If you delete this exception statement from all source
27# files in the program, then also delete it here.
28
29"""Base test case for twisted servers."""
30
31import os
32import shutil
33import tempfile
34
35from twisted.internet import defer, endpoints, protocol
36from twisted.spread import pb
37
38from ubuntuone.devtools.testcases import BaseTestCase
39
40# no init method + twisted common warnings
41# pylint: disable=W0232, C0103, E1101
42
43
44def server_protocol_factory(cls):
45 """Factory to create tidy protocols."""
46
47 if cls is None:
48 cls = protocol.Protocol
49
50 class ServerTidyProtocol(cls):
51 """A tidy protocol."""
52
53 def connectionLost(self, *args):
54 """Lost the connection."""
55 cls.connectionLost(self, *args)
56 # lets tell everyone
57 # pylint: disable=W0212
58 if (self.factory._disconnecting and
59 self.factory.testserver_on_connection_lost is not None and
60 not self.factory.testserver_on_connection_lost.called):
61 self.factory.testserver_on_connection_lost.callback(self)
62 # pylint: enable=W0212
63
64 return ServerTidyProtocol
65
66
67def server_factory_factory(cls):
68 """Factory that creates special types of factories for tests."""
69
70 if cls is None:
71 cls = protocol.ServerFactory
72
73 class TidyServerFactory(cls):
74 """A tidy factory."""
75
76 testserver_on_connection_lost = None
77
78 def buildProtocol(self, addr):
79 prot = cls.buildProtocol(self, addr)
80 self.testserver_on_connection_lost = defer.Deferred()
81 return prot
82
83 return TidyServerFactory
84
85
86def client_protocol_factory(cls):
87 """Factory to create tidy protocols."""
88
89 if cls is None:
90 cls = protocol.Protocol
91
92 class ClientTidyProtocol(cls):
93 """A tidy protocol."""
94
95 def connectionLost(self, *a):
96 """Connection list."""
97 cls.connectionLost(self, *a)
98 # pylint: disable=W0212
99 if (self.factory._disconnecting and
100 self.factory.testserver_on_connection_lost is not None and
101 not self.factory.testserver_on_connection_lost.called):
102 self.factory.testserver_on_connection_lost.callback(self)
103 # pylint: enable=W0212
104
105 return ClientTidyProtocol
106
107
108class TidySocketServer(object):
109 """Ensure that twisted servers are correctly managed in tests.
110
111 Closing a twisted server is a complicated matter. In order to do so you
112 have to ensure that three different deferreds are fired:
113
114 1. The server must stop listening.
115 2. The client connection must disconnect.
116 3. The server connection must disconnect.
117
118 This class allows to create a server and a client that will ensure that
119 the reactor is left clean by following the pattern described at
120 http://mumak.net/stuff/twisted-disconnect.html
121 """
122 def __init__(self):
123 """Create a new instance."""
124 self.listener = None
125 self.server_factory = None
126
127 self.connector = None
128 self.client_factory = None
129
130 def get_server_endpoint(self):
131 """Return the server endpoint description."""
132 raise NotImplementedError('To be implemented by child classes.')
133
134 def get_client_endpoint(self):
135 """Return the client endpoint description."""
136 raise NotImplementedError('To be implemented by child classes.')
137
138 @defer.inlineCallbacks
139 def listen_server(self, server_class, *args, **kwargs):
140 """Start a server in a random port."""
141 from twisted.internet import reactor
142 tidy_class = server_factory_factory(server_class)
143 self.server_factory = tidy_class(*args, **kwargs)
144 self.server_factory._disconnecting = False
145 self.server_factory.protocol = server_protocol_factory(
146 self.server_factory.protocol)
147 endpoint = endpoints.serverFromString(reactor,
148 self.get_server_endpoint())
149 self.listener = yield endpoint.listen(self.server_factory)
150 defer.returnValue(self.server_factory)
151
152 @defer.inlineCallbacks
153 def connect_client(self, client_class, *args, **kwargs):
154 """Conect a client to a given server."""
155 from twisted.internet import reactor
156
157 if self.server_factory is None:
158 raise ValueError('Server Factory was not provided.')
159 if self.listener is None:
160 raise ValueError('%s has not started listening.',
161 self.server_factory)
162
163 self.client_factory = client_class(*args, **kwargs)
164 self.client_factory._disconnecting = False
165 self.client_factory.protocol = client_protocol_factory(
166 self.client_factory.protocol)
167 self.client_factory.testserver_on_connection_lost = defer.Deferred()
168 endpoint = endpoints.clientFromString(reactor,
169 self.get_client_endpoint())
170 self.connector = yield endpoint.connect(self.client_factory)
171 defer.returnValue(self.client_factory)
172
173 def clean_up(self):
174 """Action to be performed for clean up."""
175 if self.server_factory is None or self.listener is None:
176 # nothing to clean
177 return defer.succeed(None)
178
179 if self.listener and self.connector:
180 # clean client and server
181 self.server_factory._disconnecting = True
182 self.client_factory._disconnecting = True
183 d = defer.maybeDeferred(self.listener.stopListening)
184 self.connector.transport.loseConnection()
185 if self.server_factory.testserver_on_connection_lost:
186 return defer.gatherResults(
187 [d,
188 self.client_factory.testserver_on_connection_lost,
189 self.server_factory.testserver_on_connection_lost])
190 else:
191 return defer.gatherResults(
192 [d,
193 self.client_factory.testserver_on_connection_lost])
194 if self.listener:
195 # just clean the server since there is no client
196 # pylint: disable=W0201
197 self.server_factory._disconnecting = True
198 return defer.maybeDeferred(self.listener.stopListening)
199 # pylint: enable=W0201
200
201
202class TidyTCPServer(TidySocketServer):
203 """A tidy tcp domain sockets server."""
204
205 client_endpoint_pattern = 'tcp:host=127.0.0.1:port=%s'
206 server_endpoint_pattern = 'tcp:0:interface=127.0.0.1'
207
208 def get_server_endpoint(self):
209 """Return the server endpoint description."""
210 return self.server_endpoint_pattern
211
212 def get_client_endpoint(self):
213 """Return the client endpoint description."""
214 if self.server_factory is None:
215 raise ValueError('Server Factory was not provided.')
216 if self.listener is None:
217 raise ValueError('%s has not started listening.',
218 self.server_factory)
219 return self.client_endpoint_pattern % self.listener.getHost().port
220
221
222class TidyUnixServer(TidySocketServer):
223 """A tidy unix domain sockets server."""
224
225 client_endpoint_pattern = 'unix:path=%s'
226 server_endpoint_pattern = 'unix:%s'
227
228 def __init__(self):
229 """Create a new instance."""
230 super(TidyUnixServer, self).__init__()
231 self.temp_dir = tempfile.mkdtemp()
232 self.path = os.path.join(self.temp_dir, 'tidy_unix_server')
233
234 def get_server_endpoint(self):
235 """Return the server endpoint description."""
236 return self.server_endpoint_pattern % self.path
237
238 def get_client_endpoint(self):
239 """Return the client endpoint description."""
240 return self.client_endpoint_pattern % self.path
241
242 def clean_up(self):
243 """Action to be performed for clean up."""
244 result = super(TidyUnixServer, self).clean_up()
245 # remove the dir once we are disconnected
246 result.addCallback(lambda _: shutil.rmtree(self.temp_dir))
247 return result
248
249
250class ServerTestCase(BaseTestCase):
251 """Base test case for tidy servers."""
252
253 @defer.inlineCallbacks
254 def setUp(self):
255 """Set the diff tests."""
256 yield super(ServerTestCase, self).setUp()
257
258 try:
259 self.server_runner = self.get_server()
260 except NotImplementedError:
261 self.server_runner = None
262
263 self.server_factory = None
264 self.client_factory = None
265 self.server_disconnected = None
266 self.client_connected = None
267 self.client_disconnected = None
268 self.listener = None
269 self.connector = None
270 self.addCleanup(self.tear_down_server_client)
271
272 def get_server(self):
273 """Return the server to be used to run the tests."""
274 raise NotImplementedError('To be implemented by child classes.')
275
276 @defer.inlineCallbacks
277 def listen_server(self, server_class, *args, **kwargs):
278 """Listen a server.
279
280 The method takes the server class and the arguments that should be
281 passed to the server constructor.
282 """
283 self.server_factory = yield self.server_runner.listen_server(
284 server_class, *args, **kwargs)
285 self.server_disconnected = \
286 self.server_factory.testserver_on_connection_lost
287 self.listener = self.server_runner.listener
288
289 @defer.inlineCallbacks
290 def connect_client(self, client_class, *args, **kwargs):
291 """Connect the client.
292
293 The method takes the client factory class and the arguments that
294 should be passed to the client constructor.
295 """
296 self.client_factory = yield self.server_runner.connect_client(
297 client_class, *args, **kwargs)
298 self.client_disconnected = \
299 self.client_factory.testserver_on_connection_lost
300 self.connector = self.server_runner.connector
301
302 def tear_down_server_client(self):
303 """Clean the server and client."""
304 if self.server_runner:
305 return self.server_runner.clean_up()
306
307
308class TCPServerTestCase(ServerTestCase):
309 """Test that uses a single twisted server."""
310
311 def get_server(self):
312 """Return the server to be used to run the tests."""
313 return TidyTCPServer()
314
315
316class UnixServerTestCase(ServerTestCase):
317 """Test that uses a single twisted server."""
318
319 def get_server(self):
320 """Return the server to be used to run the tests."""
321 return TidyUnixServer()
322
323
324class PbServerTestCase(ServerTestCase):
325 """Test a pb server."""
326
327 def get_server(self):
328 """Return the server to be used to run the tests."""
329 raise NotImplementedError('To be implemented by child classes.')
330
331 @defer.inlineCallbacks
332 def listen_server(self, *args, **kwargs):
333 """Listen a pb server."""
334 yield super(PbServerTestCase, self).listen_server(pb.PBServerFactory,
335 *args, **kwargs)
336
337 @defer.inlineCallbacks
338 def connect_client(self, *args, **kwargs):
339 """Connect a pb client."""
340 yield super(PbServerTestCase, self).connect_client(pb.PBClientFactory,
341 *args, **kwargs)
342
343
344class TCPPbServerTestCase(PbServerTestCase):
345 """Test a pb server over TCP."""
346
347 def get_server(self):
348 """Return the server to be used to run the tests."""
349 return TidyTCPServer()
350
351
352class UnixPbServerTestCase(PbServerTestCase):
353 """Test a pb server over Unix domain sockets."""
354
355 def get_server(self):
356 """Return the server to be used to run the tests."""
357 return TidyUnixServer()
0358
=== added directory 'ubuntuone/devtools/testing'
=== added file 'ubuntuone/devtools/testing/__init__.py'
--- ubuntuone/devtools/testing/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testing/__init__.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,1 @@
1"""Testing helpers."""
02
=== added file 'ubuntuone/devtools/testing/txcheck.py'
--- ubuntuone/devtools/testing/txcheck.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testing/txcheck.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,381 @@
1# -*- coding: utf-8 -*-
2
3# Author: Tim Cole <tim.cole@canonical.com>
4#
5# Copyright 2011-2012 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19# In addition, as a special exception, the copyright holders give
20# permission to link the code of portions of this program with the
21# OpenSSL library under certain conditions as described in each
22# individual source file, and distribute linked combinations
23# including the two.
24# You must obey the GNU General Public License in all respects
25# for all of the code used other than OpenSSL. If you modify
26# file(s) with this exception, you may extend this exception to your
27# version of the file(s), but you are not obligated to do so. If you
28# do not wish to do so, delete this exception statement from your
29# version. If you delete this exception statement from all source
30# files in the program, then also delete it here.
31"""Utilities for performing correctness checks."""
32
33import sys
34import ast
35from inspect import getsource
36from textwrap import dedent
37from itertools import takewhile
38from unittest import TestCase, TestSuite, TestResult
39
40from twisted.trial.unittest import TestCase as TwistedTestCase
41
42
43def type_to_name(type_obj):
44 """Return a name for a type."""
45 package_name = getattr(type_obj, '__module__', None)
46 if package_name:
47 return "%s.%s" % (package_name, type_obj.__name__)
48 else:
49 return type_obj.__name__
50
51
52class Problem(AssertionError):
53 """An object representing a problem in a method."""
54
55 def __init__(self, method, test_class, ancestor_class):
56 """Initialize an instance."""
57 super(Problem, self).__init__()
58 self.method = method
59 self.test_class = test_class
60 self.ancestor_class = ancestor_class
61
62 def __eq__(self, other):
63 """Test equality."""
64 return type(self) == type(other) and self.__dict__ == other.__dict__
65
66 def __ne__(self, other):
67 """Test inequality."""
68 return not (self == other)
69
70 def __hash__(self):
71 """Return hash."""
72 member_hash = 0
73 for (key, value) in self.__dict__.items():
74 member_hash ^= hash(key) ^ hash(value)
75 return hash(type(self)) ^ member_hash
76
77 def __str__(self):
78 """Return a friendlier representation."""
79 if self.ancestor_class != self.test_class:
80 method_string = ("%s in ancestor method %s.%s" %
81 (type_to_name(self.test_class),
82 type_to_name(self.ancestor_class),
83 self.method))
84 else:
85 method_string = ("%s.%s" %
86 (type_to_name(self.test_class), self.method))
87 return ("%s for %s" % (type(self).__name__, method_string))
88
89 def __repr__(self):
90 """Return representation string."""
91 return "<%s %r>" % (type(self), self.__dict__)
92
93
94class MethodShadowed(Problem):
95 """Problem when trial's run method is shadowed."""
96
97
98class SuperResultDiscarded(Problem):
99 """Problem when callback chains are broken."""
100
101
102class SuperNotCalled(Problem):
103 """Problem when super isn't called."""
104
105
106class MissingInlineCallbacks(Problem):
107 """Problem when the inlineCallbacks decorator is missing."""
108
109
110class MissingReturnValue(Problem):
111 """Problem when there's no return value."""
112
113
114def match_type(expected_type):
115 """Return predicate matching nodes of given type."""
116 return lambda node: isinstance(node, expected_type)
117
118
119def match_equal(expected_value):
120 """Return predicate matching nodes equaling the given value."""
121 return lambda node: expected_value == node
122
123
124def match_in(expected_values):
125 """Return predicate matching node if in collection of expected values."""
126 return lambda node: node in expected_values
127
128
129def match_not_none():
130 """Returns a predicate matching nodes which are not None."""
131 return lambda node: node is not None
132
133
134def match_any(*subtests):
135 """Return short-circuiting predicate matching any given subpredicate."""
136 if len(subtests) == 1:
137 return subtests[0]
138 else:
139
140 def test(node):
141 """Try each subtest until we find one that passes."""
142 for subtest in subtests:
143 if subtest(node):
144 return True
145 return False
146
147 return test
148
149
150def match_all(*subtests):
151 """Return short-circuiting predicate matching all given subpredicates."""
152 if len(subtests) == 1:
153 return subtests[0]
154 else:
155
156 def test(node):
157 """Try each subtest until we find one that fails."""
158 for subtest in subtests:
159 if not subtest(node):
160 return False
161 return True
162
163 return test
164
165
166def match_attr(attr_name, *tests):
167 """Return predicate matching subpredicates against an attribute value."""
168 return lambda node: match_all(*tests)(getattr(node, attr_name))
169
170
171def match_path(initial_test, *components):
172 """Return predicate which recurses into the tree via given attributes."""
173 components = list(components)
174 components.reverse()
175
176 def test(node):
177 return True
178
179 for component in components:
180 attr_name = component[0]
181 subtests = component[1:]
182 test = match_attr(attr_name, match_all(match_all(*subtests), test))
183 return match_all(initial_test, test)
184
185
186def match_child(*tests):
187 """Return predicate which tests any child."""
188 subtest = match_all(*tests)
189
190 def test(node):
191 """Try each child until we find one that matches."""
192 for child in ast.iter_child_nodes(node):
193 if subtest(child):
194 return True
195 return False
196
197 return test
198
199
200def match_descendant(subtest, prune):
201 """Return predicate which tests a node and any descendants."""
202
203 def test(node):
204 """Recursively (breadth-first) search for a matching node."""
205 for child in ast.iter_child_nodes(node):
206 if prune(child):
207 continue
208 if subtest(child) or test(child):
209 return True
210 return False
211
212 return test
213
214
215def matches(node, *tests):
216 """Convenience function to try predicates on a node."""
217 return match_all(*tests)(node)
218
219
220def any_matches(nodes, *tests):
221 """Convenience function to try predicates on any of a sequence of nodes."""
222 test = match_all(*tests)
223 for node in nodes:
224 if test(node):
225 return True
226 return False
227
228
229def iter_matching_child_nodes(node, *tests):
230 """Yields every matching child node."""
231 test = match_all(*tests)
232 for child in ast.iter_child_nodes(node):
233 if test(child):
234 yield child
235
236
237SETUP_FUNCTION_NAMES = ('setUp', 'tearDown')
238SETUP_FUNCTION = match_path(match_type(ast.FunctionDef),
239 ('name', match_in(SETUP_FUNCTION_NAMES)))
240
241SUPER = match_path(match_type(ast.Call),
242 ('func', match_type(ast.Attribute)),
243 ('value', match_type(ast.Call)),
244 ('func', match_type(ast.Name)),
245 ('id', match_equal("super")))
246
247BARE_SUPER = match_path(match_type(ast.Expr),
248 ('value', SUPER))
249
250YIELD = match_type(ast.Yield)
251
252INLINE_CALLBACKS_DECORATOR = \
253 match_any(match_path(match_type(ast.Attribute),
254 ('attr', match_equal('inlineCallbacks'))),
255 match_path(match_type(ast.Name),
256 ('id', match_equal('inlineCallbacks'))))
257
258RETURN_VALUE = \
259 match_path(match_type(ast.Return),
260 ('value', match_not_none()))
261
262DEFS = match_any(match_type(ast.ClassDef),
263 match_type(ast.FunctionDef))
264
265
266def find_problems(class_to_check):
267 """Check twisted test setup in a given test class."""
268 mro = class_to_check.__mro__
269 if TwistedTestCase not in mro:
270 return set()
271
272 problems = set()
273
274 ancestry = takewhile(lambda c: c != TwistedTestCase, mro)
275 for ancestor_class in ancestry:
276 if 'run' in ancestor_class.__dict__:
277 problem = MethodShadowed(method='run',
278 test_class=class_to_check,
279 ancestor_class=ancestor_class)
280 problems.add(problem)
281
282 source = dedent(getsource(ancestor_class))
283 tree = ast.parse(source)
284 # the top level of the tree is a Module
285 class_node = tree.body[0]
286
287 # Check setUp/tearDown
288 for def_node in iter_matching_child_nodes(class_node, SETUP_FUNCTION):
289 if matches(def_node, match_child(BARE_SUPER)):
290 # Superclass method called, but its result wasn't used
291 problem = SuperResultDiscarded(method=def_node.name,
292 test_class=class_to_check,
293 ancestor_class=ancestor_class)
294 problems.add(problem)
295 if not matches(def_node, match_descendant(SUPER, DEFS)):
296 # The call to the overridden superclass method is missing
297 problem = SuperNotCalled(method=def_node.name,
298 test_class=class_to_check,
299 ancestor_class=ancestor_class)
300 problems.add(problem)
301
302 decorators = def_node.decorator_list
303
304 if matches(def_node, match_descendant(YIELD, DEFS)):
305 # Yield was used, making this a generator
306 if not any_matches(decorators, INLINE_CALLBACKS_DECORATOR):
307 # ...but the inlineCallbacks decorator is missing
308 problem = MissingInlineCallbacks(
309 method=def_node.name,
310 test_class=class_to_check,
311 ancestor_class=ancestor_class)
312 problems.add(problem)
313 else:
314 if not matches(def_node, match_descendant(RETURN_VALUE, DEFS)):
315 # The function fails to return a deferred
316 problem = MissingReturnValue(
317 method=def_node.name,
318 test_class=class_to_check,
319 ancestor_class=ancestor_class)
320 problems.add(problem)
321
322 return problems
323
324
325def get_test_classes(suite):
326 """Return all the unique test classes involved in a suite."""
327 classes = set()
328
329 def find_classes(suite_or_test):
330 """Recursively find all the test classes."""
331 if isinstance(suite_or_test, TestSuite):
332 for subtest in suite_or_test:
333 find_classes(subtest)
334 else:
335 classes.add(type(suite_or_test))
336
337 find_classes(suite)
338
339 return classes
340
341
342def make_check_testcase(tests):
343 """Make TestCase which checks the given twisted tests."""
344
345 class TXCheckTest(TestCase):
346 """Test case which checks the test classes for problems."""
347
348 def runTest(self): # pylint: disable=C0103
349 """Do nothing."""
350
351 def run(self, result=None):
352 """Check all the test classes for problems."""
353 if result is None:
354 result = TestResult()
355
356 test_classes = set()
357
358 for test_object in tests:
359 test_classes |= get_test_classes(test_object)
360
361 for test_class in test_classes:
362 problems = find_problems(test_class)
363 for problem in problems:
364 try:
365 raise problem
366 except Problem:
367 result.addFailure(self, sys.exc_info())
368
369 return TXCheckTest()
370
371
372class TXCheckSuite(TestSuite):
373 """Test suite which checks twisted tests."""
374
375 def __init__(self, tests=()):
376 """Initialize with the given tests, and add a special test."""
377
378 tests = list(tests)
379 tests.insert(0, make_check_testcase(self))
380
381 super(TXCheckSuite, self).__init__(tests)
0382
=== added file 'ubuntuone/devtools/testing/txwebserver.py'
--- ubuntuone/devtools/testing/txwebserver.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testing/txwebserver.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,124 @@
1# -*- coding: utf-8 -*-
2# Copyright 2012 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program. If not, see <http://www.gnu.org/licenses/>.
15#
16# In addition, as a special exception, the copyright holders give
17# permission to link the code of portions of this program with the
18# OpenSSL library under certain conditions as described in each
19# individual source file, and distribute linked combinations
20# including the two.
21# You must obey the GNU General Public License in all respects
22# for all of the code used other than OpenSSL. If you modify
23# file(s) with this exception, you may extend this exception to your
24# version of the file(s), but you are not obligated to do so. If you
25# do not wish to do so, delete this exception statement from your
26# version. If you delete this exception statement from all source
27# files in the program, then also delete it here.
28
29"""A tx based web server."""
30
31from __future__ import unicode_literals
32
33from twisted.internet import defer, reactor, ssl
34from twisted.protocols.policies import WrappingFactory
35from twisted.web import server
36
37from ubuntuone.devtools.testcases.txsocketserver import server_protocol_factory
38
39# no init method + twisted common warnings
40# pylint: disable=W0232, C0103, E1101
41
42
43class BaseWebServer(object):
44 """Webserver used to perform requests in tests."""
45
46 def __init__(self, root_resource, scheme):
47 """Create and start the instance.
48
49 The ssl_settings parameter contains a dictionary with the key and cert
50 that will be used to perform ssl connections. The root_resource
51 contains the resource with all its childre.
52 """
53 self.root = root_resource
54 self.scheme = scheme
55 self.port = None
56 # use an http.HTTPFactory that was modified to ensure that we have
57 # clean close connections
58 self.site = server.Site(self.root, timeout=None)
59 self.wrapper = WrappingFactory(self.site)
60 self.wrapper.testserver_on_connection_lost = defer.Deferred()
61 self.wrapper.protocol = server_protocol_factory(self.wrapper.protocol)
62 self.wrapper._disconnecting = False
63
64 def listen(self, site):
65 """Listen a port to allow the tests."""
66 raise NotImplementedError('Base abstract class.')
67
68 def get_iri(self):
69 """Build the iri for this mock server."""
70 return "{scheme}://127.0.0.1:{port}/".format(scheme=self.scheme,
71 port=self.get_port())
72
73 def get_port(self):
74 """Return the port where we are listening."""
75 return self.port.getHost().port
76
77 def start(self):
78 """Start the service."""
79 self.port = self.listen(self.wrapper)
80
81 def stop(self):
82 """Shut it down."""
83 if self.port:
84 self.wrapper._disconnecting = True
85 connected = self.wrapper.protocols.keys()
86 if connected:
87 for con in connected:
88 con.transport.loseConnection()
89 else:
90 self.wrapper.testserver_on_connection_lost = \
91 defer.succeed(None)
92 d = defer.maybeDeferred(self.port.stopListening)
93 return defer.gatherResults(
94 [d,
95 self.wrapper.testserver_on_connection_lost])
96 return defer.succeed(None)
97
98
99class HTTPWebServer(BaseWebServer):
100 """A Webserver that listens to http connections."""
101
102 def __init__(self, root_resource):
103 """Create a new instance."""
104 super(HTTPWebServer, self).__init__(root_resource, 'http')
105
106 def listen(self, site):
107 """Listen a port to allow the tests."""
108 return reactor.listenTCP(0, site)
109
110
111class HTTPSWebServer(BaseWebServer):
112 """A WebServer that listens to https connections."""
113
114 def __init__(self, root_resource, ssl_settings=None):
115 """Create a new instance."""
116 super(HTTPSWebServer, self).__init__(root_resource, 'https')
117 self.ssl_settings = ssl_settings
118
119 def listen(self, site):
120 """Listen a port to allow the tests."""
121 ssl_context = ssl.DefaultOpenSSLContextFactory(
122 self.ssl_settings['key'], self.ssl_settings['cert'])
123
124 return reactor.listenSSL(0, site, ssl_context)
0125
=== added file 'ubuntuone/devtools/utils.py'
--- ubuntuone/devtools/utils.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/utils.py 2018-05-30 21:37:37 +0000
@@ -0,0 +1,180 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""Utilities for Ubuntu One developer tools."""
28
29from __future__ import print_function, unicode_literals
30
31import getopt
32import sys
33
34from ubuntuone.devtools.errors import UsageError
35__all__ = ['OptionParser']
36
37
38def accumulate_list_attr(class_obj, attr, list_obj, base_class=None):
39 """Get all of the list attributes of attr from the class hierarchy,
40 and return a list of the lists."""
41 for base in class_obj.__bases__:
42 accumulate_list_attr(base, attr, list_obj)
43 if base_class is None or base_class in class_obj.__bases__:
44 list_obj.extend(class_obj.__dict__.get(attr, []))
45
46
47def unpack_padded(length, sequence, default=None):
48 """Pads a sequence to length with value of default.
49
50 Returns a list containing the original and padded values.
51 """
52 newlist = [default] * length
53 newlist[:len(sequence)] = list(sequence)
54 return newlist
55
56
57class OptionParser(dict):
58 """Base options for our test runner."""
59
60 def __init__(self, *args, **kwargs):
61 super(OptionParser, self).__init__(*args, **kwargs)
62
63 # Store info about the options and defaults
64 self.long_opts = []
65 self.short_opts = ''
66 self.docs = {}
67 self.defaults = {}
68 self.synonyms = {}
69 self.dispatch = {}
70
71 # Get the options and defaults
72 for _get in [self._get_flags, self._get_params]:
73 # We don't use variable 'syns' here. It's just to pad the result.
74 # pylint: disable=W0612
75 (long_opts, short_opts, docs, defaults, syns, dispatch) = _get()
76 # pylint: enable=W0612
77 self.long_opts.extend(long_opts)
78 self.short_opts = self.short_opts + short_opts
79 self.docs.update(docs)
80 self.update(defaults)
81 self.defaults.update(defaults)
82 self.synonyms.update(syns)
83 self.dispatch.update(dispatch)
84
85 # We use some camelcase names for trial compatibility here.
86 # pylint: disable=C0103
87 def parseOptions(self, options=None):
88 """Parse the options."""
89 if options is None:
90 options = sys.argv[1:]
91
92 try:
93 opts, args = getopt.getopt(options,
94 self.short_opts, self.long_opts)
95 except getopt.error as e:
96 raise UsageError(e)
97
98 for opt, arg in opts:
99 if opt[1] == '-':
100 opt = opt[2:]
101 else:
102 opt = opt[1:]
103
104 if (opt not in self.synonyms.keys()):
105 raise UsageError('No such options: "%s"' % opt)
106
107 opt = self.synonyms[opt]
108 if self.defaults[opt] is False:
109 self[opt] = True
110 else:
111 self.dispatch[opt](arg)
112
113 try:
114 self.parseArgs(*args)
115 except TypeError:
116 raise UsageError('Wrong number of arguments.')
117
118 self.postOptions()
119
120 def postOptions(self):
121 """Called after options are parsed."""
122
123 def parseArgs(self, *args):
124 """Override to handle extra arguments specially."""
125 # pylint: enable=C0103
126
127 def _parse_arguments(self, arg_type=None, has_default=False):
128 """Parse the arguments as either flags or parameters."""
129 long_opts, short_opts = [], ''
130 docs, defaults, syns, dispatch = {}, {}, {}, {}
131
132 _args = []
133 accumulate_list_attr(self.__class__, arg_type, _args)
134
135 for _arg in _args:
136 try:
137 if has_default:
138 l_opt, s_opt, default, doc, _ = unpack_padded(5, _arg)
139 else:
140 default = False
141 l_opt, s_opt, doc, _ = unpack_padded(4, _arg)
142 except ValueError:
143 raise ValueError('Failed to parse argument: %s' % _arg)
144 if not l_opt:
145 raise ValueError('An option must have a long name.')
146
147 opt_m_name = 'opt_' + l_opt.replace('-', '_')
148 opt_method = getattr(self, opt_m_name, None)
149 if opt_method is not None:
150 docs[l_opt] = getattr(opt_method, '__doc__', None)
151 dispatch[l_opt] = opt_method
152 if docs[l_opt] is None:
153 docs[l_opt] = doc
154 else:
155 docs[l_opt] = doc
156 dispatch[l_opt] = lambda arg: True
157
158 defaults[l_opt] = default
159 if has_default:
160 long_opts.append(l_opt + '=')
161 else:
162 long_opts.append(l_opt)
163
164 syns[l_opt] = l_opt
165 if s_opt is not None:
166 short_opts = short_opts + s_opt
167 if has_default:
168 short_opts = short_opts + ':'
169 syns[s_opt] = l_opt
170
171 return long_opts, short_opts, docs, defaults, syns, dispatch
172
173 def _get_flags(self):
174 """Get the flag options."""
175 return self._parse_arguments(arg_type='optFlags', has_default=False)
176
177 def _get_params(self):
178 """Get the parameters options."""
179 return self._parse_arguments(arg_type='optParameters',
180 has_default=True)

Subscribers

People subscribed via source and target branches

to all changes: