Merge lp:~cjwatson/launchpad/simplify-buildout-bin-test into lp:launchpad

Proposed by Colin Watson on 2017-05-08
Status: Merged
Merged at revision: 18374
Proposed branch: lp:~cjwatson/launchpad/simplify-buildout-bin-test
Merge into: lp:launchpad
Diff against target: 460 lines (+187/-189)
4 files modified
README (+2/-5)
doc/buildout.txt (+1/-1)
lib/lp/scripts/utilities/test.py (+183/-183)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/simplify-buildout-bin-test
Reviewer Review Type Date Requested Status
William Grant code 2017-05-08 Approve on 2017-05-11
Review via email: mp+323743@code.launchpad.net

Commit Message

Turn buildout-templates/bin/test.in into an entry point to a utility module.

Description of the Change

This brings us close to eradicating the use of z3c.recipe.filetemplate, which will simplify the upcoming conversion to pip. All that remains is buildout-templates/_pythonpath.py.in.

Considerable care is needed with imports, because the config instance needs to be set before anything interesting is done with lp.services.config. In practice this means that most imports need to be a little later in lp.scripts.utilities.test than one might normally expect, and (more subtly) that the utility module can't be under lp.testing because of the large number of non-trivial imports performed by lib/lp/testing/__init__.py.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2011-02-18 18:43:16 +0000
3+++ README 2017-05-08 12:04:30 +0000
4@@ -46,8 +46,7 @@
5 bin/, utilities/
6 Where you will find scripts intended for developers and admins. There's
7 no rhyme or reason to what goes in bin/ and what goes in utilities/, so
8- take a look in both. bin/ will be empty in a fresh checkout, the actual
9- content lives in 'buildout-templates'.
10+ take a look in both.
11
12 configs/
13 Configuration files for various kinds of Launchpad instances.
14@@ -91,9 +90,7 @@
15 of the ones that come up from time to time.
16
17 buildout-templates/
18- Templates that are generated into actual files, normally bin/ scripts,
19- when buildout is run. If you want to change the behaviour of bin/test,
20- look here.
21+ Templates that are generated into actual files when buildout is run.
22
23 bzrplugins/
24 Bazaar plugins used in running Launchpad.
25
26=== removed directory 'buildout-templates/bin'
27=== modified file 'doc/buildout.txt'
28--- doc/buildout.txt 2014-01-30 15:04:06 +0000
29+++ doc/buildout.txt 2017-05-08 12:04:30 +0000
30@@ -549,7 +549,7 @@
31 To add a file using the recipe, simply create mirrors of the source tree
32 directories that you need under ``buildout-templates/``, and create a .in file
33 template at the desired location. Take a look at
34-``buildout-templates/bin/`` for examples of what is possible.
35+``buildout-templates/_pythonpath.py.in`` for an example of what is possible.
36
37 .. _`z3c.recipe.filetemplate`: http://pypi.python.org/pypi/z3c.recipe.filetemplate
38
39
40=== renamed file 'buildout-templates/bin/test.in' => 'lib/lp/scripts/utilities/test.py'
41--- buildout-templates/bin/test.in 2017-01-20 01:46:34 +0000
42+++ lib/lp/scripts/utilities/test.py 2017-05-08 12:04:30 +0000
43@@ -1,4 +1,3 @@
44-#!${buildout:executable} -S
45 ##############################################################################
46 #
47 # Copyright (c) 2004 Zope Corporation and Contributors.
48@@ -12,209 +11,211 @@
49 # FOR A PARTICULAR PURPOSE.
50 #
51 ##############################################################################
52-"""Test script
53-"""
54-
55-# NOTE: This is a generated file. The original is in
56-# buildout-templates/bin/test.in
57-
58-import logging, os, re, sys, time, warnings
59-
60-# Initialize our paths.
61-${python-relative-path-setup}
62-
63-
64-# The working directory change is just so that the test script
65-# can be invoked from places other than the root of the source
66-# tree. This is very useful for IDE integration, so an IDE can
67-# e.g. run the test that you are currently editing.
68-BUILD_DIR = ${buildout:directory|path-repr}
69-there = os.getcwd()
70-os.chdir(BUILD_DIR)
71-
72-
73-import sys
74-sys.path.insert(0, ${scripts:parts-directory|path-repr})
75-import site
76-
77-
78-# Fix doctest so that it can handle mixed unicode and encoded output.
79+"""Test script."""
80+
81 import doctest
82-
83-_RealSpoofOut = doctest._SpoofOut
84-
85-class _SpoofOut(doctest._SpoofOut):
86-
87- def write(self, value):
88- if isinstance(value, unicode):
89- value = value.encode('utf8')
90- _RealSpoofOut.write(self, value)
91-
92-doctest._SpoofOut = _SpoofOut
93-
94-
95-CUSTOM_SITE_DIR = ${scripts:parts-directory|path-repr}
96-
97-# Make tests run in a timezone no launchpad developers live in.
98-# Our tests need to run in any timezone.
99-# (This is no longer actually required, as PQM does this.)
100-os.environ['TZ'] = 'Asia/Calcutta'
101-time.tzset()
102-
103-# Httplib2 0.7 started validating SSL certificates, and the test suite uses a
104-# self-signed certificate. So disable it with an env variable
105-os.environ['LP_DISABLE_SSL_CERTIFICATE_VALIDATION'] = '1'
106-
107-# Storm's C extensions should already be enabled from lp_sitecustomize.py,
108-# which our custom sitecustomize.py ran.
109-assert os.environ['STORM_CEXTENSIONS'] == '1'
110-
111-# Make sure our site.py is the one that subprocesses use.
112-os.environ['PYTHONPATH'] = CUSTOM_SITE_DIR
113-
114-# Set a flag if this is the main testrunner process
115-if len(sys.argv) > 1 and sys.argv[1] == '--resume-layer':
116- main_process = False
117-else:
118- main_process = True
119-
120-# Install the import fascist import hook and atexit handler.
121-from lp.scripts.utilities import importfascist
122-importfascist.install_import_fascist()
123-
124-# Install the warning handler hook and atexit handler.
125-from lp.scripts.utilities import warninghandler
126-warninghandler.install_warning_handler()
127-
128-# Ensure that atexit handlers are executed on TERM.
129+import os
130+import random
131+import re
132 import signal
133-def exit_with_atexit_handlers(*ignored):
134- sys.exit(-1 * signal.SIGTERM)
135-signal.signal(signal.SIGTERM, exit_with_atexit_handlers)
136-
137-# Tell lp.services.config to use the testrunner config instance.
138-from lp.services.config import config
139-config.setInstance('testrunner')
140-config.generate_overrides()
141-
142-# Remove this module's directory from path, so that zope.testbrowser
143-# can import pystone from test:
144-sys.path[:] = [p for p in sys.path if os.path.abspath(p) != BUILD_DIR]
145-
146-# Turn on psycopg debugging wrapper
147-#import lp.services.database.debug
148-#lp.services.database.debug.install()
149-
150-# Unset the http_proxy environment variable, because we're going to make
151-# requests to localhost and we don't wand this to be proxied.
152-try:
153- os.environ.pop('http_proxy')
154-except KeyError:
155- pass
156-
157-# Suppress accessability warning because the test runner does not have UI.
158-os.environ['GTK_MODULES'] = ''
159-
160-# Silence spurious warnings. Note that this does not propagate to subprocesses
161-# so this is not always as easy as it seems. Warnings caused by our code that
162-# need to be silenced should have an accompanied Bug reference.
163-#
164-warnings.filterwarnings(
165- 'ignore', 'PyCrypto', RuntimeWarning, 'twisted[.]conch[.]ssh',
166- )
167-warnings.filterwarnings(
168- 'ignore', 'twisted.python.plugin', DeprecationWarning,
169- )
170-warnings.filterwarnings(
171- 'ignore', 'zope.testing.doctest', DeprecationWarning,
172- )
173-warnings.filterwarnings(
174- 'ignore', 'bzrlib.*was deprecated', DeprecationWarning,
175- )
176-# The next one is caused by a lamosity in python-openid. The following change
177-# to openid/server/server.py would make the warning filter unnecessary:
178-# 978c974,974
179-# > try:
180-# > namespace = request.message.getOpenIDNamespace()
181-# > except AttributeError:
182-# > namespace = request.namespace
183-# > self.fields = Message(namespace)
184-# ---
185-# < self.fields = Message(request.namespace)
186-warnings.filterwarnings(
187- 'ignore',
188- (r'The \"namespace\" attribute of CheckIDRequest objects is deprecated.\s+'
189- r'Use \"message.getOpenIDNamespace\(\)\" instead'),
190- DeprecationWarning
191-)
192-# This warning will be triggered if the beforeTraversal hook fails. We
193-# want to ensure it is not raised as an error, as this will mask the real
194-# problem.
195-warnings.filterwarnings(
196- 'always',
197- re.escape('clear_request_started() called outside of a request'),
198- UserWarning
199- )
200-# Unicode warnings are always fatal
201-warnings.filterwarnings('error', category=UnicodeWarning)
202-
203-# shortlist() raises an error when it is misused.
204-warnings.filterwarnings('error', r'shortlist\(\)')
205-
206-from lp.testing import pgsql
207-# If this is removed, make sure lp.testing.pgsql is updated
208-# because the test harness there relies on the Connection wrapper being
209-# installed.
210-pgsql.installFakeConnect()
211+import sys
212+import time
213+import warnings
214
215 from zope.testing import testrunner
216 from zope.testing.testrunner import options
217
218+from lp.scripts.utilities import (
219+ importfascist,
220+ warninghandler,
221+ )
222+from lp.services.config import config
223+
224+
225+def fix_doctest_output():
226+ # Fix doctest so that it can handle mixed unicode and encoded output.
227+ _RealSpoofOut = doctest._SpoofOut
228+
229+ class _SpoofOut(doctest._SpoofOut):
230+
231+ def write(self, value):
232+ if isinstance(value, unicode):
233+ value = value.encode('utf8')
234+ _RealSpoofOut.write(self, value)
235+
236+ doctest._SpoofOut = _SpoofOut
237+
238+
239+def configure_environment():
240+ # Make tests run in a timezone no launchpad developers live in.
241+ # Our tests need to run in any timezone.
242+ # (This is no longer actually required, as PQM does this.)
243+ os.environ['TZ'] = 'Asia/Calcutta'
244+ time.tzset()
245+
246+ # Httplib2 0.7 started validating SSL certificates, and the test suite
247+ # uses a self-signed certificate, so disable it with an env variable.
248+ os.environ['LP_DISABLE_SSL_CERTIFICATE_VALIDATION'] = '1'
249+
250+ # Storm's C extensions should already be enabled from
251+ # lp_sitecustomize.py, which our custom sitecustomize.py ran.
252+ assert os.environ['STORM_CEXTENSIONS'] == '1'
253+
254+ # Install the import fascist import hook and atexit handler.
255+ importfascist.install_import_fascist()
256+
257+ # Install the warning handler hook and atexit handler.
258+ warninghandler.install_warning_handler()
259+
260+ # Ensure that atexit handlers are executed on TERM.
261+ def exit_with_atexit_handlers(*ignored):
262+ sys.exit(-1 * signal.SIGTERM)
263+ signal.signal(signal.SIGTERM, exit_with_atexit_handlers)
264+
265+ # Tell lp.services.config to use the testrunner config instance.
266+ config.setInstance('testrunner')
267+ config.generate_overrides()
268+
269+ # Remove this module's directory from path, so that zope.testbrowser
270+ # can import pystone from test:
271+ sys.path[:] = [p for p in sys.path if os.path.abspath(p) != config.root]
272+
273+ # Turn on psycopg debugging wrapper
274+ #import lp.services.database.debug
275+ #lp.services.database.debug.install()
276+
277+ # Unset the http_proxy environment variable, because we're going to make
278+ # requests to localhost and we don't want this to be proxied.
279+ os.environ.pop('http_proxy', None)
280+
281+ # Suppress accessibility warning because the test runner does not have UI.
282+ os.environ['GTK_MODULES'] = ''
283+
284+
285+def filter_warnings():
286+ # Silence spurious warnings. Note that this does not propagate to
287+ # subprocesses so this is not always as easy as it seems. Warnings
288+ # caused by our code that need to be silenced should have an accompanied
289+ # Bug reference.
290+ warnings.filterwarnings(
291+ 'ignore', 'PyCrypto', RuntimeWarning, 'twisted[.]conch[.]ssh',
292+ )
293+ warnings.filterwarnings(
294+ 'ignore', 'twisted.python.plugin', DeprecationWarning,
295+ )
296+ warnings.filterwarnings(
297+ 'ignore', 'zope.testing.doctest', DeprecationWarning,
298+ )
299+ warnings.filterwarnings(
300+ 'ignore', 'bzrlib.*was deprecated', DeprecationWarning,
301+ )
302+ # The next one is caused by an infelicity in python-openid. The
303+ # following change to openid/server/server.py would make the warning
304+ # filter unnecessary:
305+ # 978c974,974
306+ # > try:
307+ # > namespace = request.message.getOpenIDNamespace()
308+ # > except AttributeError:
309+ # > namespace = request.namespace
310+ # > self.fields = Message(namespace)
311+ # ---
312+ # < self.fields = Message(request.namespace)
313+ warnings.filterwarnings(
314+ 'ignore',
315+ (r'The \"namespace\" attribute of CheckIDRequest objects is '
316+ r'deprecated.\s+'
317+ r'Use \"message.getOpenIDNamespace\(\)\" instead'),
318+ DeprecationWarning
319+ )
320+ # This warning will be triggered if the beforeTraversal hook fails. We
321+ # want to ensure it is not raised as an error, as this will mask the
322+ # real problem.
323+ warnings.filterwarnings(
324+ 'always',
325+ re.escape('clear_request_started() called outside of a request'),
326+ UserWarning
327+ )
328+ # Unicode warnings are always fatal
329+ warnings.filterwarnings('error', category=UnicodeWarning)
330+
331+ # shortlist() raises an error when it is misused.
332+ warnings.filterwarnings('error', r'shortlist\(\)')
333+
334+
335+def install_fake_pgsql_connect():
336+ from lp.testing import pgsql
337+ # If this is removed, make sure lp.testing.pgsql is updated
338+ # because the test harness there relies on the Connection wrapper being
339+ # installed.
340+ pgsql.installFakeConnect()
341+
342+
343+def randomise_listdir():
344+ # Monkey-patch os.listdir to randomise the results.
345+ original_listdir = os.listdir
346+
347+ def listdir(path):
348+ """Randomise the results of os.listdir.
349+
350+ It uses random.shuffle to randomise os.listdir results. This way
351+ tests relying on unstable ordering will have a higher chance to fail
352+ in the development environment.
353+ """
354+ directory_contents = original_listdir(path)
355+ random.shuffle(directory_contents)
356+ return directory_contents
357+
358+ os.listdir = listdir
359+
360+
361 defaults = {
362 # Find tests in the tests and ftests directories
363 'tests_pattern': '^f?tests$',
364- 'test_path': [${buildout:directory/lib|path-repr}],
365+ 'test_path': [os.path.join(config.root, 'lib')],
366 'package': ['canonical', 'lp', 'devscripts', 'launchpad_loggerhead'],
367 'layer': ['!(YUIAppServerLayer)'],
368 'require_unique_ids': True,
369 }
370
371-# Monkey-patch os.listdir to randomise the results
372-original_listdir = os.listdir
373-
374-import random
375-
376-def listdir(path):
377- """Randomise the results of os.listdir.
378-
379- It uses random.suffle to randomise os.listdir results, this way tests
380- relying on unstable ordering will have a higher chance to fail in the
381- development environment.
382- """
383- directory_contents = original_listdir(path)
384- random.shuffle(directory_contents)
385- return directory_contents
386-
387-os.listdir = listdir
388-
389-
390-from lp.services.testing.customresult import filter_tests, patch_find_tests
391-
392-
393-if __name__ == '__main__':
394+
395+def main():
396+ # The working directory change is just so that the test script
397+ # can be invoked from places other than the root of the source
398+ # tree. This is very useful for IDE integration, so an IDE can
399+ # e.g. run the test that you are currently editing.
400+ there = os.getcwd()
401+ os.chdir(config.root)
402+
403+ fix_doctest_output()
404+ configure_environment()
405+ filter_warnings()
406+ install_fake_pgsql_connect()
407+ randomise_listdir()
408+
409+ # The imports at the top of this file must avoid anything that reads
410+ # from Launchpad config. Now that we've set the correct config instance,
411+ # we can safely import the rest.
412+ from lp.services.testing.customresult import (
413+ filter_tests,
414+ patch_find_tests,
415+ )
416+ from lp.services.testing import profiled
417+
418 # Extract arguments so we can see them too. We need to strip
419 # --resume-layer and --default stuff if found as get_options can't
420 # handle it.
421 if len(sys.argv) > 1 and sys.argv[1] == '--resume-layer':
422+ main_process = False
423 args = list(sys.argv)
424- args.pop(1) # --resume-layer
425- args.pop(1) # The layer name
426- args.pop(1) # The resume number
427+ args.pop(1) # --resume-layer
428+ args.pop(1) # The layer name
429+ args.pop(1) # The resume number
430 while len(args) > 1 and args[1] == '--default':
431- args.pop(1) # --default
432- args.pop(1) # The default value
433+ args.pop(1) # --default
434+ args.pop(1) # The default value
435 args.insert(0, sys.argv[0])
436 else:
437+ main_process = True
438 args = sys.argv
439
440 # thunk across to parallel support if needed.
441@@ -250,7 +251,6 @@
442 options.parser.defaults[name] = value
443
444 # Turn on Layer profiling if requested.
445- from lp.services.testing import profiled
446 if local_options.verbose >= 3 and main_process:
447 profiled.setup_profiling()
448
449
450=== modified file 'setup.py'
451--- setup.py 2017-01-21 13:42:28 +0000
452+++ setup.py 2017-05-08 12:04:30 +0000
453@@ -175,6 +175,7 @@
454 'run-testapp = lp.scripts.runlaunchpad:start_testapp',
455 'sprite-util = lp.scripts.utilities.spriteutil:main',
456 'start_librarian = lp.scripts.runlaunchpad:start_librarian',
457+ 'test = lp.scripts.utilities.test:main',
458 'twistd = twisted.scripts.twistd:run',
459 'watch_jsbuild = lp.scripts.utilities.js.watchjsbuild:main',
460 'with-xvfb = lp.scripts.utilities.withxvfb:main',