1
+ """
2
+ roadrunner (or rr)
3
+
4
+ aka. looping testrunner with environment preloading for test-driven development
5
+
6
+ preloads a standard Zope & Plone test environment compatible with
7
+ PloneTestCase.
8
+
9
+ Tests are then run in a loop. You are given a shell-like environment with
10
+ command history where you can select different tests, etc.
11
+
12
+ Limitiations
13
+ ============
14
+
15
+ Because it preloads the Plone environment you won't be able to see changes
16
+ to the Core Plone components. However, it should see all changes in your
17
+ application code which is what you will most likely be changing anyways.
18
+ Unless you are a core developer.
19
+
20
+ Theoretically this should be able to work with any test environment (eg.
21
+ Django, TG, Twisted).
22
+
23
+ I eventually plan to do this, and would accept any patches in the meantime
24
+ if anyone feels so inclined.
25
+
26
+ Author
27
+ ======
28
+
29
+
30
+
31
+ TODO
32
+ ====
33
+
34
+ - ability to preload any arbitrary layers, plus some presets like --plone or --grok
35
+ - hooks for fixture configuration?
36
+ - tests... how ironic this code has none, but it started as a proof of concept
37
+ """
38
+ from zope .testing import testrunner
39
+ import os , sys , time
40
+ import shlex
41
+
42
+ try :
43
+ import readline
44
+ HAVE_READLINE = True
45
+ except :
46
+ HAVE_READLINE = False
47
+
48
+ def run_commandloop (args ):
49
+ while 1 :
50
+ cmdline = raw_input ("rr> " ).strip ()
51
+ if cmdline :
52
+ cmdargs = shlex .split (cmdline )
53
+ if cmdargs [0 ] in ('quit' , 'exit' ):
54
+ sys .exit (0 )
55
+ if cmdargs [0 ] in ('help' , '?' ):
56
+ print HELP_MESSAGE
57
+ continue
58
+ if cmdargs [0 ] == 'test' :
59
+ # ok we have some cmdline arguments
60
+ args = cmdargs [1 :]
61
+ break
62
+ else :
63
+ print "Unknown command. Type 'help' for help."
64
+ else :
65
+ print "rr> test " + shlex_join (args )
66
+ break
67
+ return args
68
+
69
+ def main (zope_conf , args = sys .argv ):
70
+ sys .argv = ['fakepath' ] # Zope configure whines about argv stuff make it shutup
71
+ bootstrap_zope (zope_conf )
72
+ args = args [2 :]
73
+ if HAVE_READLINE :
74
+ readline .add_history ('test ' + shlex_join (args ))
75
+
76
+ ## preload test environment
77
+ t1 = time .time ()
78
+ setup_layers = preload_plone ()
79
+ t2 = time .time ()
80
+ preload_time = t2 - t1
81
+ print 'Preloading took: %0.3f seconds.' % (preload_time )
82
+
83
+ defaults = testrunner_defaults ()
84
+ defaults = setup_paths (defaults )
85
+
86
+ saved_time = 0
87
+ while 1 :
88
+ # Test Loop Start
89
+ pid = os .fork ()
90
+ if not pid :
91
+ # Run tests in child process
92
+ t1 = time .time ()
93
+ rc = testrunner .run (defaults = defaults , args = args ,
94
+ setup_layers = setup_layers )
95
+ t2 = time .time ()
96
+ print 'Testrunner took: %0.3f seconds. ' % ((t2 - t1 ))
97
+ sys .exit (rc )
98
+
99
+ else :
100
+ # In parent process
101
+ try :
102
+ status = os .wait ()
103
+ # print "\nchild process returned: ", repr(status)
104
+ except OSError :
105
+ print "\n child process was interrupted!"
106
+
107
+ args = run_commandloop (args )
108
+
109
+ # add to saved_time
110
+ # start the test loop over....
111
+
112
+ def bootstrap_zope (config_file ):
113
+ config_file = os .path .abspath (config_file )
114
+ print "Parsing %s" % config_file
115
+ import Zope2
116
+ Zope2 .configure (config_file )
117
+
118
+ def filter_warnings ():
119
+ import warnings
120
+ warnings .simplefilter ('ignore' , Warning , append = True )
121
+ filter_warnings ()
122
+
123
+ def maybe_quote_args (arg ):
124
+ if ' ' in arg :
125
+ return '"' + arg + '"'
126
+ else :
127
+ return arg
128
+
129
+ def shlex_join (args , char = ' ' ):
130
+ l = map (maybe_quote_args , args )
131
+ return char .join (args )
132
+
133
+ HELP_MESSAGE = \
134
+ """\
135
+ roadrunner help
136
+ --------------
137
+
138
+ exit
139
+ to quit
140
+
141
+ test <testrunner arguments>
142
+ run the testrunner
143
+
144
+ help
145
+ this message
146
+
147
+ Press the <return> key to run the test again with the same arguments.
148
+
149
+ If you have readline you can use that to search your history.
150
+ """
151
+
152
+ def setup_paths (defaults ):
153
+ """
154
+ this code is from Zope's test.py
155
+ """
156
+ # Put all packages found in products directories on the test-path.
157
+ import Products
158
+ products = []
159
+ softwarehome = '/Users/jbb/co/shared_plone3/parts/zope2/lib/python'
160
+
161
+ for path in Products .__path__ :
162
+ # ignore software home, as it already works
163
+ if not path .startswith (softwarehome ):
164
+ # get all folders in the current products folder and filter
165
+ # out everything that is not a directory or a VCS internal one.
166
+ folders = [f for f in os .listdir (path ) if
167
+ os .path .isdir (os .path .join (path , f )) and
168
+ not f .startswith ('.' ) and not f == 'CVS' ]
169
+ if folders :
170
+ for folder in folders :
171
+ # look into all folders and see if they have an
172
+ # __init__.py in them. This filters out non-packages
173
+ # like for example documenation folders
174
+ package = os .path .join (path , folder )
175
+ if os .path .exists (os .path .join (package , '__init__.py' )):
176
+ products .append (package )
177
+
178
+ # Put all packages onto the search path as a package. As we only deal
179
+ # with products, the package name is always prepended by 'Products.'
180
+ for product in products :
181
+ defaults += ['--package-path' , product , 'Products.%s' % os .path .split (product )[- 1 ]]
182
+
183
+ paths = sys .path
184
+ # progname = self.options.progname
185
+ buildout_root = '/Users/jordan/co/myplatform_buildout/trunk' #os.path.dirname(os.path.dirname(progname))
186
+
187
+ for path in paths :
188
+ if path != buildout_root :
189
+ defaults += ['--test-path' , path ]
190
+
191
+ return defaults
192
+
193
+ def preload_plone ():
194
+ print "Preloading Plone ..."
195
+ from Products .PloneTestCase .layer import PloneSite
196
+ from Products .PloneTestCase import PloneTestCase as ptc
197
+ ptc .setupPloneSite ()
198
+ # pre-setup Plone layer
199
+ from zope .testing .testrunner import setup_layer
200
+ setup_layers = {}
201
+ setup_layer (PloneSite , setup_layers )
202
+ # delete the plone layer registration so that the testrunner
203
+ # will re-run Plone layer setUp after deferred setups have
204
+ # been registered by the associated tests.
205
+ del setup_layers [PloneSite ]
206
+ return setup_layers
207
+
208
+ def testrunner_defaults ():
209
+ defaults = '--tests-pattern ^tests$ -v' .split ()
210
+
211
+ return defaults
212
+
213
+ def register_signal_handlers (pid ):
214
+ # propogate signals to child process
215
+ import signal
216
+ #
217
+ # def interrupt_handler(signum, frame, pid=pid):
218
+ # try:
219
+ # print "received interrupt, killing pid %s" % pid
220
+ # os.kill(pid, signal.SIGKILL)
221
+ # except OSError, e:
222
+ # print e, pid
223
+ #
224
+ # signal.signal(signal.SIGINT, interrupt_handler)
225
+ # # restore signal handler
226
+ # signal.signal(signal.SIGINT, signal.SIG_DFL)
227
+
228
+ if __name__ == '__main__' :
229
+ main ()
0 commit comments