summaryrefslogtreecommitdiffstats
path: root/bin/time-scons.py
blob: 10684e09a5caafad4eaeb551eae0bf9f732a549d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#!/usr/bin/env python
#
# time-scons.py:  a wrapper script for running SCons timings
#
# This script exists to:
#
#     1)  Wrap the invocation of runtest.py to run the actual TimeSCons
#         timings consistently.  It does this specifically by building
#         SCons first, so .pyc compilation is not part of the timing.
#
#     2)  Provide an interface for running TimeSCons timings against
#         earlier revisions, before the whole TimeSCons infrastructure
#         was "frozen" to provide consistent timings.  This is done
#         by updating the specific pieces containing the TimeSCons
#         infrastructure to the earliest revision at which those pieces
#         were "stable enough."
#
# By encapsulating all the logic in this script, our Buildbot
# infrastructure only needs to call this script, and we should be able
# to change what we need to in this script and have it affect the build
# automatically when the source code is updated, without having to
# restart either master or slave.

import optparse
import os
import shutil
import subprocess
import sys
import tempfile
import xml.sax.handler


SubversionURL = 'http://scons.tigris.org/svn/scons'


# This is the earliest revision when the TimeSCons scripts collected
# "real," stable timings.  If we're timing a revision prior to this,
# we'll forcibly update the TimeSCons pieces of the tree to this revision
# to collect consistent timings for earlier revisions.
TimeSCons_revision = 4547

# The pieces of the TimeSCons infrastructure that are necessary to
# produce consistent timings, even when the rest of the tree is from
# an earlier revision that doesn't have these pieces.
TimeSCons_pieces = ['QMTest', 'timings', 'runtest.py']


class CommandRunner:
    """
    Executor class for commands, including "commands" implemented by
    Python functions.
    """
    verbose = True
    active = True

    def __init__(self, dictionary={}):
        self.subst_dictionary(dictionary)

    def subst_dictionary(self, dictionary):
        self._subst_dictionary = dictionary

    def subst(self, string, dictionary=None):
        """
        Substitutes (via the format operator) the values in the specified
        dictionary into the specified command.

        The command can be an (action, string) tuple.    In all cases, we
        perform substitution on strings and don't worry if something isn't
        a string.    (It's probably a Python function to be executed.)
        """
        if dictionary is None:
            dictionary = self._subst_dictionary
        if dictionary:
            try:
                string = string % dictionary
            except TypeError:
                pass
        return string

    def display(self, command, stdout=None, stderr=None):
        if not self.verbose:
            return
        if type(command) == type(()):
            func = command[0]
            args = command[1:]
            s = '%s(%s)' % (func.__name__, ', '.join(map(repr, args)))
        if type(command) == type([]):
            # TODO:    quote arguments containing spaces
            # TODO:    handle meta characters?
            s = ' '.join(command)
        else:
            s = self.subst(command)
        if not s.endswith('\n'):
            s += '\n'
        sys.stdout.write(s)
        sys.stdout.flush()

    def execute(self, command, stdout=None, stderr=None):
        """
        Executes a single command.
        """
        if not self.active:
            return 0
        if type(command) == type(''):
            command = self.subst(command)
            cmdargs = shlex.split(command)
            if cmdargs[0] == 'cd':
                 command = (os.chdir,) + tuple(cmdargs[1:])
        if type(command) == type(()):
            func = command[0]
            args = command[1:]
            return func(*args)
        else:
            if stdout is sys.stdout:
                # Same as passing sys.stdout, except works with python2.4.
                subout = None
            else:
                # Open pipe for anything else so Popen works on python2.4.
                subout = subprocess.PIPE
            if stderr is sys.stderr:
                # Same as passing sys.stdout, except works with python2.4.
                suberr = None
            elif stderr is None:
                # Merge with stdout if stderr isn't specified.
                suberr = subprocess.STDOUT
            else:
                # Open pipe for anything else so Popen works on python2.4.
                suberr = subprocess.PIPE
            p = subprocess.Popen(command,
                                 shell=(sys.platform == 'win32'),
                                 stdout=subout,
                                 stderr=suberr)
            p.wait()
            if stdout is None:
                self.stdout = p.stdout.read()
            elif stdout is not sys.stdout:
                stdout.write(p.stdout.read())
            if stderr not in (None, sys.stderr):
                stderr.write(p.stderr.read())
            return p.returncode

    def run(self, command, display=None, stdout=None, stderr=None):
        """
        Runs a single command, displaying it first.
        """
        if display is None:
            display = command
        self.display(display)
        return self.execute(command, stdout, stderr)

    def run_list(self, command_list, **kw):
        """
        Runs a list of commands, stopping with the first error.

        Returns the exit status of the first failed command, or 0 on success.
        """
        for command in command_list:
            status = self.run(command, **kw)
            if status:
                return status
        return 0


def get_svn_revisions(branch, revisions=None):
    """
    Fetch the actual SVN revisions for the given branch querying
    "svn log."  A string specifying a range of revisions can be
    supplied to restrict the output to a subset of the entire log.
    """
    command = ['svn', 'log', '--xml']
    if revisions:
        command.extend(['-r', revisions])
    command.append(branch)
    p = subprocess.Popen(command, stdout=subprocess.PIPE)

    class SVNLogHandler(xml.sax.handler.ContentHandler):
        def __init__(self):
            self.revisions = []
        def startElement(self, name, attributes):
            if name == 'logentry':
                self.revisions.append(int(attributes['revision']))

    parser = xml.sax.make_parser()
    handler = SVNLogHandler()
    parser.setContentHandler(handler)
    parser.parse(p.stdout)
    return sorted(handler.revisions)


def script_commands(script):
    """
    Returns a list of the commands to be executed to test the specified
    TimeSCons script.  This involves building SCons (specifically the
    'tar-gz' Alias that creates and unpacks a SCons .tar.gz package,
    in order to have the *.py files compiled to *.pyc) after first
    removing the build directory, and then actually calling runtest.py
    to run the timing script.
    """
    commands = []
    if os.path.exists('build'):
        commands.extend([
            ['mv', 'build', 'build.OLD'],
            ['rm', '-rf', 'build.OLD'],
        ])
    commands.extend([
        [sys.executable, 'bootstrap.py', 'tar-gz'],
        # --noqmtest is necessary for the log to contain the
        # actual scons output (which qmtest normally swallows).
        [sys.executable, 'runtest.py', '--noqmtest', '-p', 'tar-gz', script],
    ])
    return commands

def do_revisions(cr, opts, branch, revisions, scripts):
    """
    Time the SCons branch specified scripts through a list of revisions.

    We assume we're in a (temporary) directory in which we can check
    out the source for the specified revisions.
    """
    stdout = sys.stdout
    stderr = sys.stderr

    for this_revision in revisions:

        if opts.logfiles:
            log_file = os.path.join(opts.origin, '%s.log' % this_revision)
            stdout = open(log_file, 'w')
            stderr = None

        commands = [
            ['svn', 'co', '-q', '-r', str(this_revision), branch, '.'],
        ]

        if int(this_revision) < int(TimeSCons_revision):
            commands.append(['svn', 'up', '-q', '-r', str(TimeSCons_revision)]
                            + TimeSCons_pieces)

        for script in scripts:
            commands.extend(script_commands(script))

        if int(this_revision) < int(TimeSCons_revision):
            # "Revert" the pieces that we previously updated to the
            # TimeSCons_revision, so the update to the next revision
            # works cleanly.
            commands.append(['svn', 'up', '-q', '-r', str(this_revision)]
                            + TimeSCons_pieces)

        status = cr.run_list(commands, stdout=stdout, stderr=stderr)
        if status:
            return status

    return 0

def main(argv=None):
    if argv is None:
        argv = sys.argv

    parser = optparse.OptionParser(usage="time-scons.py [-hnq] [-r REVISION ...] [--branch BRANCH] [--svn] SCRIPT ...")
    parser.add_option("--branch", metavar="BRANCH", default="trunk",
                      help="time revision on BRANCH")
    parser.add_option("--logfiles", action="store_true",
                      help="generate separate log files for each revision")
    parser.add_option("-n", "--no-exec", action="store_true",
                      help="no execute, just print the command line")
    parser.add_option("-q", "--quiet", action="store_true",
                      help="quiet, don't print the command line")
    parser.add_option("-r", "--revision", metavar="REVISION",
                      help="time specified revisions")
    parser.add_option("--svn", action="store_true",
                      help="fetch actual revisions for BRANCH")
    opts, scripts = parser.parse_args(argv[1:])

    if not scripts:
        sys.stderr.write('No scripts specified.\n')
        sys.exit(1)

    CommandRunner.verbose = not opts.quiet
    CommandRunner.active = not opts.no_exec
    cr = CommandRunner()

    os.environ['TESTSCONS_SCONSFLAGS'] = ''

    branch = SubversionURL + '/' + opts.branch

    if opts.svn:
        revisions = get_svn_revisions(branch, opts.revision)
    else:
        # TODO(sgk):  parse this for SVN-style revision strings
        revisions = opts.revision

    if revisions:
        opts.origin = os.getcwd()
        tempdir = tempfile.mkdtemp(prefix='time-scons-')
        try:
            os.chdir(tempdir)
            status = do_revisions(cr, opts, branch, revisions, scripts)
        finally:
            os.chdir(opts.origin)
            shutil.rmtree(tempdir)
    else:
        for script in scripts:
            commands = script_commands(script)
            status = cr.run_list(commands, stdout=sys.stdout, stderr=sys.stderr)
            if status:
                return status

    return status


if __name__ == "__main__":
    sys.exit(main())