Merge lp:~frankban/charms/precise/juju-gui/testing-improvements into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- testing-improvements
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 68 |
Proposed branch: | lp:~frankban/charms/precise/juju-gui/testing-improvements |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
440 lines (+155/-58) 10 files modified
.lbox.check (+3/-0) HACKING.md (+0/-5) Makefile (+1/-1) revision (+1/-1) tests/20-functional.test (+6/-6) tests/deploy.py (+10/-8) tests/helpers.py (+33/-9) tests/requirements.pip (+2/-0) tests/test_deploy.py (+30/-13) tests/test_helpers.py (+69/-15) |
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/testing-improvements |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+168673@code.launchpad.net |
Commit message
Description of the change
Charm test improvements.
This branch includes some improvements to
the charm testing infrastructure:
Strengthened the Juju implementation check by
introducing a new juju_version helper function.
Do not require the branch to be named "juju-gui"
anymore: the charm can be tested and deployed from
an arbitrary branch.
Updated documentation.
Added the .lbox.check file: now, before
proposing/
and the unit tests must pass.
Francesco Banconi (frankban) wrote : | # |
- 76. By Francesco Banconi
-
Bump revision up.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
Nicola Larosa (teknico) wrote : | # |
LGTM, thanks for this. Nice tests as usual. A few trivials below.
https:/
File tests/20-
https:/
tests/20-
timeout=30):
Maybe a constant at top of file for all three default timeout values?
https:/
File tests/helpers.py (right):
https:/
tests/helpers.
This means it's goJuju, right? A comment to that effect?
https:/
tests/helpers.
This looks like a mispelling of "taint". :-) "to_int" maybe?
https:/
File tests/requireme
https:/
tests/requireme
Oh, you can put comments in here? Good to know. :-)
https:/
File tests/test_
https:/
tests/test_
mock_check_output):
Maybe a more explicit "test_not_
- 77. By Francesco Banconi
-
Fix make lint.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
j.c.sackett (jcsackett) wrote : | # |
On 2013/06/11 14:58:05, frankban wrote:
> Please take a look.
LGTM.
I have nothing to add, though I do agree wtih teknico's suggestion of
using a constant for the default timeout value in 20-functional.test
- 78. By Francesco Banconi
-
Changes as per review.
- 79. By Francesco Banconi
-
Merged trunk and resolved conflicts.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Charm test improvements.
This branch includes some improvements to
the charm testing infrastructure:
Strengthened the Juju implementation check by
introducing a new juju_version helper function.
Do not require the branch to be named "juju-gui"
anymore: the charm can be tested and deployed from
an arbitrary branch.
Updated documentation.
Added the .lbox.check file: now, before
proposing/
and the unit tests must pass.
R=teknico, j.c.sackett
CC=
https:/
https:/
File tests/20-
https:/
tests/20-
timeout=30):
On 2013/06/11 14:38:40, teknico wrote:
> Maybe a constant at top of file for all three default timeout values?
As discussed on IRC, those values are only incidentally the same, so
making a module level constant doesn't give us a lot.
https:/
File tests/helpers.py (right):
https:/
tests/helpers.
On 2013/06/11 14:38:40, teknico wrote:
> This means it's goJuju, right? A comment to that effect?
Done.
https:/
tests/helpers.
On 2013/06/11 14:38:40, teknico wrote:
> This looks like a mispelling of "taint". :-) "to_int" maybe?
Done.
https:/
File tests/test_
https:/
tests/test_
mock_check_output):
On 2013/06/11 14:38:40, teknico wrote:
> Maybe a more explicit "test_not_
Done.
Francesco Banconi (frankban) wrote : | # |
Thank you both for the reviews!
Preview Diff
1 | === added file '.lbox.check' |
2 | --- .lbox.check 1970-01-01 00:00:00 +0000 |
3 | +++ .lbox.check 2013-06-12 09:11:27 +0000 |
4 | @@ -0,0 +1,3 @@ |
5 | +#!/bin/sh |
6 | + |
7 | +make lint unittest |
8 | |
9 | === modified file 'HACKING.md' |
10 | --- HACKING.md 2013-06-11 14:04:04 +0000 |
11 | +++ HACKING.md 2013-06-12 09:11:27 +0000 |
12 | @@ -43,11 +43,6 @@ |
13 | At this point, link "juju-plugins/plugins/juju_test.py" as "juju-test" |
14 | somewhere in your PATH, so that it is possible to execute "juju-test". |
15 | |
16 | -Our testing infrastructure requires the current directory name to match the |
17 | -charm name, so you must check out the charm into a directory named "juju-gui": |
18 | - |
19 | - bzr branch lp:~juju-gui/charms/precise/juju-gui/trunk juju-gui |
20 | - |
21 | Before being able to run the suite, test requirements need to be installed |
22 | running the command: |
23 | |
24 | |
25 | === modified file 'Makefile' |
26 | --- Makefile 2013-06-11 14:13:45 +0000 |
27 | +++ Makefile 2013-06-12 09:11:27 +0000 |
28 | @@ -36,7 +36,7 @@ |
29 | $(JUJUTEST) |
30 | |
31 | lint: setup |
32 | - @flake8 --show-source --exclude=.venv ./hooks/ ./tests/ |
33 | + @$(VENV)/bin/flake8 --show-source --exclude=.venv ./hooks/ ./tests/ |
34 | |
35 | clean: |
36 | find . -name '*.pyc' -delete |
37 | |
38 | === modified file 'revision' |
39 | --- revision 2013-06-12 08:59:15 +0000 |
40 | +++ revision 2013-06-12 09:11:27 +0000 |
41 | @@ -1,1 +1,1 @@ |
42 | -52 |
43 | +53 |
44 | |
45 | === modified file 'tests/20-functional.test' |
46 | --- tests/20-functional.test 2013-06-11 14:04:04 +0000 |
47 | +++ tests/20-functional.test 2013-06-12 09:11:27 +0000 |
48 | @@ -26,14 +26,14 @@ |
49 | from deploy import juju_deploy |
50 | from helpers import ( |
51 | juju_destroy_service, |
52 | - legacy_juju, |
53 | + juju_version, |
54 | ssh, |
55 | ) |
56 | |
57 | |
58 | JUJU_GUI_TEST_BRANCH = 'lp:~juju-gui/juju-gui/charm-tests-branch' |
59 | STAGING_SERVICES = ('haproxy', 'mediawiki', 'memcached', 'mysql', 'wordpress') |
60 | -is_legacy_juju = legacy_juju() |
61 | +is_legacy_juju = juju_version().major == 0 |
62 | |
63 | |
64 | class DeployTestMixin(object): |
65 | @@ -56,7 +56,7 @@ |
66 | def assertEnvironmentIsConnected(self): |
67 | """Assert the GUI environment is connected to the Juju API agent.""" |
68 | self.wait_for_script( |
69 | - 'return app.env.get("connected");', |
70 | + 'return app && app.env && app.env.get("connected");', |
71 | error='Environment not connected.') |
72 | |
73 | def handle_browser_warning(self): |
74 | @@ -87,7 +87,7 @@ |
75 | return driver.title == 'Juju Admin' |
76 | self.wait_for(page_ready, error='Juju GUI not found.') |
77 | |
78 | - def wait_for(self, condition, error=None, timeout=20): |
79 | + def wait_for(self, condition, error=None, timeout=30): |
80 | """Wait for condition to be True. |
81 | |
82 | The argument condition is a callable accepting a driver object. |
83 | @@ -97,7 +97,7 @@ |
84 | wait = ui.WebDriverWait(self.selenium, timeout) |
85 | return wait.until(condition, error) |
86 | |
87 | - def wait_for_css_selector(self, selector, error=None, timeout=20): |
88 | + def wait_for_css_selector(self, selector, error=None, timeout=30): |
89 | """Wait until the provided CSS selector is found. |
90 | |
91 | Fail printing the provided error if timeout is exceeded. |
92 | @@ -108,7 +108,7 @@ |
93 | elements = self.wait_for(condition, error=error, timeout=timeout) |
94 | return elements[0] |
95 | |
96 | - def wait_for_script(self, script, error=None, timeout=20): |
97 | + def wait_for_script(self, script, error=None, timeout=30): |
98 | """Wait for the given JavaScript snippet to return a True value. |
99 | |
100 | Fail printing the provided error if timeout is exceeded. |
101 | |
102 | === modified file 'tests/deploy.py' |
103 | --- tests/deploy.py 2013-06-11 14:04:04 +0000 |
104 | +++ tests/deploy.py 2013-06-12 09:11:27 +0000 |
105 | @@ -33,23 +33,25 @@ |
106 | rsync = command('rsync', '-a', '--exclude', '.bzr', '--exclude', '.venv') |
107 | |
108 | |
109 | -def setup_repository(source, series='precise'): |
110 | +def setup_repository(name, source, series='precise'): |
111 | """Create a temporary Juju repository to use for charm deployment. |
112 | |
113 | - Copy the charm files in source in the precise repository section, excluding |
114 | - the virtualenv and Bazaar directories. |
115 | + Copy the charm files in source in the precise repository section, using the |
116 | + provided charm name and excluding the virtualenv and Bazaar directories. |
117 | |
118 | Return the repository path. |
119 | """ |
120 | - source = os.path.abspath(source) |
121 | + source = os.path.abspath(source) + os.path.sep |
122 | repo = tempfile.mkdtemp() |
123 | - destination = os.path.join(repo, series) |
124 | + destination = os.path.join(repo, series, name) |
125 | os.makedirs(destination) |
126 | rsync(source, destination) |
127 | return repo |
128 | |
129 | |
130 | -def juju_deploy(charm, options=None, force_machine=None, charm_source=None): |
131 | +def juju_deploy( |
132 | + charm, options=None, force_machine=None, charm_source=None, |
133 | + series='precise'): |
134 | """Deploy and expose the charm. Return the first unit's public address. |
135 | |
136 | Also wait until the service is exposed and the first unit started. |
137 | @@ -60,14 +62,14 @@ |
138 | if charm_source is None: |
139 | # Dynamically retrieve the charm source based on the path of this file. |
140 | charm_source = os.path.join(os.path.dirname(__file__), '..') |
141 | - repo = setup_repository(charm_source) |
142 | + repo = setup_repository(charm, charm_source, series=series) |
143 | args = ['deploy', '--repository', repo] |
144 | if options is not None: |
145 | config_file = make_charm_config_file({charm: options}) |
146 | args.extend(['--config', config_file.name]) |
147 | if force_machine is not None: |
148 | args.extend(['--force-machine', str(force_machine)]) |
149 | - args.append('local:{0}'.format(charm)) |
150 | + args.append('local:{}/{}'.format(series, charm)) |
151 | juju(*args) |
152 | juju('expose', charm) |
153 | return wait_for_unit(charm) |
154 | |
155 | === modified file 'tests/helpers.py' |
156 | --- tests/helpers.py 2013-06-11 14:04:04 +0000 |
157 | +++ tests/helpers.py 2013-06-12 09:11:27 +0000 |
158 | @@ -16,9 +16,11 @@ |
159 | |
160 | """Juju GUI test helpers.""" |
161 | |
162 | +from collections import namedtuple |
163 | from functools import wraps |
164 | import json |
165 | import os |
166 | +import re |
167 | import subprocess |
168 | import time |
169 | |
170 | @@ -71,15 +73,7 @@ |
171 | juju_command = command('juju') |
172 | juju_env = lambda: os.getenv('JUJU_ENV') # This is propagated by juju-test. |
173 | ssh = command('ssh') |
174 | - |
175 | - |
176 | -def legacy_juju(): |
177 | - """Return True if pyJuju is being used, False otherwise.""" |
178 | - try: |
179 | - juju_command('--version') |
180 | - except ProcessError: |
181 | - return False |
182 | - return True |
183 | +Version = namedtuple('Version', 'major minor patch') |
184 | |
185 | |
186 | def retry(exception, tries=10, delay=1): |
187 | @@ -136,6 +130,36 @@ |
188 | return json.loads(status) |
189 | |
190 | |
191 | +_juju_version_expression = re.compile(r""" |
192 | + ^ # Beginning of line. |
193 | + (?:juju\s+)? # Optional juju prefix. |
194 | + (\d+)\.(\d+) # Major and minor versions. |
195 | + (?:\.(\d+))? # Optional patch version. |
196 | + .* # Optional suffix. |
197 | + $ # End of line. |
198 | +""", re.VERBOSE) |
199 | + |
200 | + |
201 | +def juju_version(): |
202 | + """Return the currently used Juju version. |
203 | + |
204 | + The version is returned as a named tuple (major, minor, patch). |
205 | + If the patch number is missing, it is set to zero. |
206 | + """ |
207 | + try: |
208 | + # In pyJuju, version info is printed to stderr. |
209 | + output = subprocess.check_output( |
210 | + ['juju', '--version'], stderr=subprocess.STDOUT) |
211 | + except subprocess.CalledProcessError: |
212 | + # Current juju-core exposes a version subcommand. |
213 | + output = subprocess.check_output(['juju', 'version']) |
214 | + match = _juju_version_expression.match(output) |
215 | + if match is None: |
216 | + raise ValueError('invalid juju version: {!r}'.format(output)) |
217 | + to_int = lambda num: 0 if num is None else int(num) |
218 | + return Version._make(map(to_int, match.groups())) |
219 | + |
220 | + |
221 | def wait_for_unit(sevice): |
222 | """Wait for the first unit of the given service to be started. |
223 | |
224 | |
225 | === modified file 'tests/requirements.pip' |
226 | --- tests/requirements.pip 2013-06-11 14:43:19 +0000 |
227 | +++ tests/requirements.pip 2013-06-12 09:11:27 +0000 |
228 | @@ -1,3 +1,5 @@ |
229 | +# Juju GUI test requirements. |
230 | + |
231 | # This file is part of the Juju GUI, which lets users view and manage Juju |
232 | # environments within a graphical interface (https://launchpad.net/juju-gui). |
233 | # Copyright (C) 2012-2013 Canonical Ltd. |
234 | |
235 | === modified file 'tests/test_deploy.py' |
236 | --- tests/test_deploy.py 2013-06-11 14:04:04 +0000 |
237 | +++ tests/test_deploy.py 2013-06-12 09:11:27 +0000 |
238 | @@ -31,10 +31,11 @@ |
239 | |
240 | class TestSetupRepository(unittest.TestCase): |
241 | |
242 | + name = 'test-charm' |
243 | + |
244 | def setUp(self): |
245 | # Create a directory structure for the charm source. |
246 | self.source = tempfile.mkdtemp() |
247 | - self.charm_name = os.path.basename(self.source) |
248 | self.addCleanup(shutil.rmtree, self.source) |
249 | # Create a file in the source dir. |
250 | _, self.root_file = tempfile.mkstemp(dir=self.source) |
251 | @@ -75,24 +76,24 @@ |
252 | series_dir = os.path.join(repo, series) |
253 | self.assert_dir_exists(series_dir) |
254 | # The series directory only contains our charm. |
255 | - self.assertEqual([self.charm_name], os.listdir(series_dir)) |
256 | - self.assert_dir_exists(os.path.join(series_dir, self.charm_name)) |
257 | + self.assertEqual([self.name], os.listdir(series_dir)) |
258 | + self.assert_dir_exists(os.path.join(series_dir, self.name)) |
259 | |
260 | def test_repository(self): |
261 | # The charm repository is correctly created with the default series. |
262 | - repo = setup_repository(self.source) |
263 | + repo = setup_repository(self.name, self.source) |
264 | self.check_repository(repo, 'precise') |
265 | |
266 | def test_series(self): |
267 | # The charm repository is created with the given series. |
268 | - repo = setup_repository(self.source, series='raring') |
269 | + repo = setup_repository(self.name, self.source, series='raring') |
270 | self.check_repository(repo, 'raring') |
271 | |
272 | def test_charm_files(self): |
273 | # The charm files are correctly copied inside the repository, excluding |
274 | # unwanted directories. |
275 | - repo = setup_repository(self.source) |
276 | - charm_dir = os.path.join(repo, 'precise', self.charm_name) |
277 | + repo = setup_repository(self.name, self.source) |
278 | + charm_dir = os.path.join(repo, 'precise', self.name) |
279 | test_dir_name = os.path.basename(self.tests_dir) |
280 | expected = set([ |
281 | os.path.basename(self.root_file), |
282 | @@ -107,7 +108,7 @@ |
283 | unit_info = {'public-address': 'unit.example.com'} |
284 | charm = 'test-charm' |
285 | expose_call = mock.call('expose', charm) |
286 | - local_charm = 'local:{}'.format(charm) |
287 | + local_charm = 'local:precise/{}'.format(charm) |
288 | repo = '/tmp/repo/' |
289 | |
290 | @mock.patch('deploy.juju') |
291 | @@ -115,13 +116,19 @@ |
292 | @mock.patch('deploy.setup_repository') |
293 | def call_deploy( |
294 | self, mock_setup_repository, mock_wait_for_unit, mock_juju, |
295 | - **kwargs): |
296 | + options=None, force_machine=None, charm_source=None, |
297 | + series='precise'): |
298 | mock_setup_repository.return_value = self.repo |
299 | mock_wait_for_unit.return_value = self.unit_info |
300 | - charm_source = kwargs.setdefault( |
301 | - 'charm_source', os.path.join(os.path.dirname(__file__), '..')) |
302 | - unit_info = juju_deploy(self.charm, **kwargs) |
303 | - mock_setup_repository.assert_called_once_with(charm_source) |
304 | + if charm_source is None: |
305 | + expected_source = os.path.join(os.path.dirname(__file__), '..') |
306 | + else: |
307 | + expected_source = charm_source |
308 | + unit_info = juju_deploy( |
309 | + self.charm, options=options, force_machine=force_machine, |
310 | + charm_source=charm_source, series=series) |
311 | + mock_setup_repository.assert_called_once_with( |
312 | + self.charm, expected_source, series=series) |
313 | # The unit address is correctly returned. |
314 | self.assertEqual(self.unit_info, unit_info) |
315 | self.assertEqual(1, mock_wait_for_unit.call_count) |
316 | @@ -177,3 +184,13 @@ |
317 | ) |
318 | deploy_call = self.call_deploy(charm_source='/tmp/source/') |
319 | self.assertEqual(expected_deploy_call, deploy_call) |
320 | + |
321 | + def test_series(self): |
322 | + # The function can deploy a charm from a specific series. |
323 | + expected_deploy_call = mock.call( |
324 | + 'deploy', |
325 | + '--repository', self.repo, |
326 | + 'local:raring/{}'.format(self.charm) |
327 | + ) |
328 | + deploy_call = self.call_deploy(series='raring') |
329 | + self.assertEqual(expected_deploy_call, deploy_call) |
330 | |
331 | === modified file 'tests/test_helpers.py' |
332 | --- tests/test_helpers.py 2013-06-11 14:04:04 +0000 |
333 | +++ tests/test_helpers.py 2013-06-12 09:11:27 +0000 |
334 | @@ -17,6 +17,7 @@ |
335 | """Juju GUI helpers tests.""" |
336 | |
337 | import json |
338 | +import subprocess |
339 | import unittest |
340 | |
341 | import mock |
342 | @@ -27,9 +28,10 @@ |
343 | juju_destroy_service, |
344 | juju_env, |
345 | juju_status, |
346 | - legacy_juju, |
347 | + juju_version, |
348 | ProcessError, |
349 | retry, |
350 | + Version, |
351 | wait_for_unit, |
352 | ) |
353 | |
354 | @@ -193,20 +195,72 @@ |
355 | mock_juju.assert_called_once_with('status', '--format', 'json') |
356 | |
357 | |
358 | -@mock.patch('helpers.juju_command') |
359 | -class TestLegacyJuju(unittest.TestCase): |
360 | - |
361 | - def test_pyjuju(self, mock_juju_command): |
362 | - # Legacy Juju is correctly recognized. |
363 | - mock_juju_command.return_value = '0.7.0' |
364 | - self.assertTrue(legacy_juju()) |
365 | - mock_juju_command.assert_called_once_with('--version') |
366 | - |
367 | - def test_juju_core(self, mock_juju_command): |
368 | - # juju-core is correctly recognized. |
369 | - mock_juju_command.side_effect = ProcessError(1, 'failed', '', '') |
370 | - self.assertFalse(legacy_juju()) |
371 | - mock_juju_command.assert_called_once_with('--version') |
372 | +@mock.patch('subprocess.check_output') |
373 | +class TestJujuVersion(unittest.TestCase): |
374 | + |
375 | + error = subprocess.CalledProcessError(2, 'invalid flag', 'output') |
376 | + |
377 | + def test_pyjuju(self, mock_check_output): |
378 | + # The pyJuju version is correctly retrieved. |
379 | + mock_check_output.return_value = '0.7.2' |
380 | + version = juju_version() |
381 | + self.assertEqual(Version(0, 7, 2), version) |
382 | + mock_check_output.assert_called_once_with( |
383 | + ['juju', '--version'], stderr=subprocess.STDOUT, |
384 | + ) |
385 | + |
386 | + def test_juju_core(self, mock_check_output): |
387 | + # The juju-core version is correctly retrieved. |
388 | + mock_check_output.side_effect = (self.error, '1.12.3') |
389 | + version = juju_version() |
390 | + self.assertEqual(Version(1, 12, 3), version) |
391 | + self.assertEqual(2, mock_check_output.call_count) |
392 | + first_call, second_call = mock_check_output.call_args_list |
393 | + self.assertEqual( |
394 | + mock.call(['juju', '--version'], stderr=subprocess.STDOUT), |
395 | + first_call, |
396 | + ) |
397 | + self.assertEqual(mock.call(['juju', 'version']), second_call) |
398 | + |
399 | + def test_not_semantic_versioning(self, mock_check_output): |
400 | + # If the patch number is missing, it is set to zero. |
401 | + mock_check_output.return_value = '0.7' |
402 | + version = juju_version() |
403 | + self.assertEqual(Version(0, 7, 0), version) |
404 | + |
405 | + def test_prefix(self, mock_check_output): |
406 | + # The function handles versions returned as "juju x.y.z". |
407 | + mock_check_output.return_value = 'juju 0.8.3' |
408 | + version = juju_version() |
409 | + self.assertEqual(Version(0, 8, 3), version) |
410 | + |
411 | + def test_suffix(self, mock_check_output): |
412 | + # The function handles versions returned as "x.y.z-series-arch". |
413 | + mock_check_output.return_value = '1.10.3-raring-amd64' |
414 | + version = juju_version() |
415 | + self.assertEqual(Version(1, 10, 3), version) |
416 | + |
417 | + def test_all(self, mock_check_output): |
418 | + # Additional information is correctly handled. |
419 | + mock_check_output.side_effect = (self.error, 'juju 1.234-precise-i386') |
420 | + version = juju_version() |
421 | + self.assertEqual(Version(1, 234, 0), version) |
422 | + self.assertEqual(2, mock_check_output.call_count) |
423 | + |
424 | + def test_invalid_version(self, mock_check_output): |
425 | + # A ValueError is raised if the returned version is not valid. |
426 | + mock_check_output.return_value = '42' |
427 | + with self.assertRaises(ValueError) as info: |
428 | + juju_version() |
429 | + self.assertEqual("invalid juju version: '42'", str(info.exception)) |
430 | + |
431 | + def test_failure(self, mock_check_output): |
432 | + # A CalledProcessError is raised if the Juju version cannot be found. |
433 | + mock_check_output.side_effect = (self.error, self.error) |
434 | + with self.assertRaises(subprocess.CalledProcessError) as info: |
435 | + juju_version() |
436 | + self.assertIs(self.error, info.exception) |
437 | + self.assertEqual(2, mock_check_output.call_count) |
438 | |
439 | |
440 | class TestProcessError(unittest.TestCase): |
Reviewers: mp+168673_ code.launchpad. net,
Message:
Please take a look.
Description:
Charm test improvements.
This branch includes some improvements to
the charm testing infrastructure:
Strengthened the Juju implementation check by
introducing a new juju_version helper function.
Do not require the branch to be named "juju-gui"
anymore: the charm can be tested and deployed from
an arbitrary branch.
Updated documentation.
Added the .lbox.check file: now, before submitting at least the lint check
proposing/
and the unit tests must pass.
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ testing- improvements/ +merge/ 168673
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/9714047/
Affected files: functional. test nts.pip deploy. py helpers. py
A .lbox.check
M HACKING.md
A [revision details]
M tests/20-
M tests/deploy.py
M tests/helpers.py
M tests/requireme
M tests/test_
M tests/test_