Updates and slight refactor in fix
[scons.git] / bin / time-scons.py
blob272283a08bc0cd4406ea2baac8572253b182be86
1 #!/usr/bin/env python
3 # time-scons.py: a wrapper script for running SCons timings
5 # This script exists to:
7 # 1) Wrap the invocation of runtest.py to run the actual TimeSCons
8 # timings consistently. It does this specifically by building
9 # SCons first, so .pyc compilation is not part of the timing.
11 # 2) Provide an interface for running TimeSCons timings against
12 # earlier revisions, before the whole TimeSCons infrastructure
13 # was "frozen" to provide consistent timings. This is done
14 # by updating the specific pieces containing the TimeSCons
15 # infrastructure to the earliest revision at which those pieces
16 # were "stable enough."
18 # By encapsulating all the logic in this script, our Buildbot
19 # infrastructure only needs to call this script, and we should be able
20 # to change what we need to in this script and have it affect the build
21 # automatically when the source code is updated, without having to
22 # restart either master or slave.
24 import optparse
25 import os
26 import shutil
27 import subprocess
28 import sys
29 import tempfile
30 import xml.sax.handler
33 SubversionURL = 'http://scons.tigris.org/svn/scons'
36 # This is the baseline revision when the TimeSCons scripts first
37 # stabilized and collected "real," consistent timings. If we're timing
38 # a revision prior to this, we'll forcibly update the TimeSCons pieces
39 # of the tree to this revision to collect consistent timings for earlier
40 # revisions.
41 TimeSCons_revision = 4569
43 # The pieces of the TimeSCons infrastructure that are necessary to
44 # produce consistent timings, even when the rest of the tree is from
45 # an earlier revision that doesn't have these pieces.
46 TimeSCons_pieces = ['testing/framework', 'timings', 'runtest.py']
49 class CommandRunner:
50 """
51 Executor class for commands, including "commands" implemented by
52 Python functions.
53 """
54 verbose = True
55 active = True
57 def __init__(self, dictionary={}):
58 self.subst_dictionary(dictionary)
60 def subst_dictionary(self, dictionary):
61 self._subst_dictionary = dictionary
63 def subst(self, string, dictionary=None):
64 """
65 Substitutes (via the format operator) the values in the specified
66 dictionary into the specified command.
68 The command can be an (action, string) tuple. In all cases, we
69 perform substitution on strings and don't worry if something isn't
70 a string. (It's probably a Python function to be executed.)
71 """
72 if dictionary is None:
73 dictionary = self._subst_dictionary
74 if dictionary:
75 try:
76 string = string % dictionary
77 except TypeError:
78 pass
79 return string
81 def display(self, command, stdout=None, stderr=None):
82 if not self.verbose:
83 return
84 if isinstance(command, tuple):
85 func = command[0]
86 args = command[1:]
87 s = '%s(%s)' % (func.__name__, ', '.join(map(repr, args)))
88 if isinstance(command, list):
89 # TODO: quote arguments containing spaces
90 # TODO: handle meta characters?
91 s = ' '.join(command)
92 else:
93 s = self.subst(command)
94 if not s.endswith('\n'):
95 s += '\n'
96 sys.stdout.write(s)
97 sys.stdout.flush()
99 def execute(self, command, stdout=None, stderr=None):
101 Executes a single command.
103 if not self.active:
104 return 0
105 if isinstance(command, str):
106 command = self.subst(command)
107 cmdargs = shlex.split(command)
108 if cmdargs[0] == 'cd':
109 command = (os.chdir,) + tuple(cmdargs[1:])
110 if isinstance(command, tuple):
111 func = command[0]
112 args = command[1:]
113 return func(*args)
114 else:
115 if stdout is sys.stdout:
116 # Same as passing sys.stdout, except works with python2.4.
117 subout = None
118 elif stdout is None:
119 # Open pipe for anything else so Popen works on python2.4.
120 subout = subprocess.PIPE
121 else:
122 subout = stdout
123 if stderr is sys.stderr:
124 # Same as passing sys.stdout, except works with python2.4.
125 suberr = None
126 elif stderr is None:
127 # Merge with stdout if stderr isn't specified.
128 suberr = subprocess.STDOUT
129 else:
130 suberr = stderr
131 p = subprocess.Popen(command,
132 shell=(sys.platform == 'win32'),
133 stdout=subout,
134 stderr=suberr)
135 p.wait()
136 return p.returncode
138 def run(self, command, display=None, stdout=None, stderr=None):
140 Runs a single command, displaying it first.
142 if display is None:
143 display = command
144 self.display(display)
145 return self.execute(command, stdout, stderr)
147 def run_list(self, command_list, **kw):
149 Runs a list of commands, stopping with the first error.
151 Returns the exit status of the first failed command, or 0 on success.
153 status = 0
154 for command in command_list:
155 s = self.run(command, **kw)
156 if s and status == 0:
157 status = s
158 return 0
161 def get_svn_revisions(branch, revisions=None):
163 Fetch the actual SVN revisions for the given branch querying
164 "svn log." A string specifying a range of revisions can be
165 supplied to restrict the output to a subset of the entire log.
167 command = ['svn', 'log', '--xml']
168 if revisions:
169 command.extend(['-r', revisions])
170 command.append(branch)
171 p = subprocess.Popen(command, stdout=subprocess.PIPE)
173 class SVNLogHandler(xml.sax.handler.ContentHandler):
174 def __init__(self):
175 self.revisions = []
176 def startElement(self, name, attributes):
177 if name == 'logentry':
178 self.revisions.append(int(attributes['revision']))
180 parser = xml.sax.make_parser()
181 handler = SVNLogHandler()
182 parser.setContentHandler(handler)
183 parser.parse(p.stdout)
184 return sorted(handler.revisions)
187 def prepare_commands():
189 Returns a list of the commands to be executed to prepare the tree
190 for testing. This involves building SCons, specifically the
191 build/scons subdirectory where our packaging build is staged,
192 and then running setup.py to create a local installed copy
193 with compiled *.pyc files. The build directory gets removed
194 first.
196 commands = []
197 if os.path.exists('build'):
198 commands.extend([
199 ['mv', 'build', 'build.OLD'],
200 ['rm', '-rf', 'build.OLD'],
202 commands.append([sys.executable, 'bootstrap.py', 'build/scons'])
203 commands.append([sys.executable,
204 'build/scons/setup.py',
205 'install',
206 '--prefix=' + os.path.abspath('build/usr')])
207 return commands
209 def script_command(script):
210 """Returns the command to actually invoke the specified timing
211 script using our "built" scons."""
212 return [sys.executable, 'runtest.py', '-x', 'build/usr/bin/scons', script]
214 def do_revisions(cr, opts, branch, revisions, scripts):
216 Time the SCons branch specified scripts through a list of revisions.
218 We assume we're in a (temporary) directory in which we can check
219 out the source for the specified revisions.
221 stdout = sys.stdout
222 stderr = sys.stderr
224 status = 0
226 if opts.logsdir and not opts.no_exec and len(scripts) > 1:
227 for script in scripts:
228 subdir = os.path.basename(os.path.dirname(script))
229 logsubdir = os.path.join(opts.origin, opts.logsdir, subdir)
230 if not os.path.exists(logsubdir):
231 os.makedirs(logsubdir)
233 for this_revision in revisions:
235 if opts.logsdir and not opts.no_exec:
236 log_name = '%s.log' % this_revision
237 log_file = os.path.join(opts.origin, opts.logsdir, log_name)
238 stdout = open(log_file, 'w')
239 stderr = None
241 commands = [
242 ['svn', 'co', '-q', '-r', str(this_revision), branch, '.'],
245 if int(this_revision) < int(TimeSCons_revision):
246 commands.append(['svn', 'up', '-q', '-r', str(TimeSCons_revision)]
247 + TimeSCons_pieces)
249 commands.extend(prepare_commands())
251 s = cr.run_list(commands, stdout=stdout, stderr=stderr)
252 if s:
253 if status == 0:
254 status = s
255 continue
257 for script in scripts:
258 if opts.logsdir and not opts.no_exec and len(scripts) > 1:
259 subdir = os.path.basename(os.path.dirname(script))
260 lf = os.path.join(opts.origin, opts.logsdir, subdir, log_name)
261 out = open(lf, 'w')
262 err = None
263 close_out = True
264 else:
265 out = stdout
266 err = stderr
267 close_out = False
268 s = cr.run(script_command(script), stdout=out, stderr=err)
269 if s and status == 0:
270 status = s
271 if close_out:
272 out.close()
273 out = None
275 if int(this_revision) < int(TimeSCons_revision):
276 # "Revert" the pieces that we previously updated to the
277 # TimeSCons_revision, so the update to the next revision
278 # works cleanly.
279 command = (['svn', 'up', '-q', '-r', str(this_revision)]
280 + TimeSCons_pieces)
281 s = cr.run(command, stdout=stdout, stderr=stderr)
282 if s:
283 if status == 0:
284 status = s
285 continue
287 if stdout not in (sys.stdout, None):
288 stdout.close()
289 stdout = None
291 return status
293 Usage = """\
294 time-scons.py [-hnq] [-r REVISION ...] [--branch BRANCH]
295 [--logsdir DIR] [--svn] SCRIPT ..."""
297 def main(argv=None):
298 if argv is None:
299 argv = sys.argv
301 parser = optparse.OptionParser(usage=Usage)
302 parser.add_option("--branch", metavar="BRANCH", default="trunk",
303 help="time revision on BRANCH")
304 parser.add_option("--logsdir", metavar="DIR", default='.',
305 help="generate separate log files for each revision")
306 parser.add_option("-n", "--no-exec", action="store_true",
307 help="no execute, just print the command line")
308 parser.add_option("-q", "--quiet", action="store_true",
309 help="quiet, don't print the command line")
310 parser.add_option("-r", "--revision", metavar="REVISION",
311 help="time specified revisions")
312 parser.add_option("--svn", action="store_true",
313 help="fetch actual revisions for BRANCH")
314 opts, scripts = parser.parse_args(argv[1:])
316 if not scripts:
317 sys.stderr.write('No scripts specified.\n')
318 sys.exit(1)
320 CommandRunner.verbose = not opts.quiet
321 CommandRunner.active = not opts.no_exec
322 cr = CommandRunner()
324 os.environ['TESTSCONS_SCONSFLAGS'] = ''
326 branch = SubversionURL + '/' + opts.branch
328 if opts.svn:
329 revisions = get_svn_revisions(branch, opts.revision)
330 elif opts.revision:
331 # TODO(sgk): parse this for SVN-style revision strings
332 revisions = [opts.revision]
333 else:
334 revisions = None
336 if opts.logsdir and not os.path.exists(opts.logsdir):
337 os.makedirs(opts.logsdir)
339 if revisions:
340 opts.origin = os.getcwd()
341 tempdir = tempfile.mkdtemp(prefix='time-scons-')
342 try:
343 os.chdir(tempdir)
344 status = do_revisions(cr, opts, branch, revisions, scripts)
345 finally:
346 os.chdir(opts.origin)
347 shutil.rmtree(tempdir)
348 else:
349 commands = prepare_commands()
350 commands.extend([ script_command(script) for script in scripts ])
351 status = cr.run_list(commands, stdout=sys.stdout, stderr=sys.stderr)
353 return status
356 if __name__ == "__main__":
357 sys.exit(main())