~ubuntu-branches/ubuntu/raring/qtwebkit-source/raring-proposed

« back to all changes in this revision

Viewing changes to Tools/Scripts/run-qtwebkit-tests

  • Committer: Package Import Robot
  • Author(s): Jonathan Riddell
  • Date: 2013-02-18 14:24:18 UTC
  • Revision ID: package-import@ubuntu.com-20130218142418-eon0jmjg3nj438uy
Tags: upstream-2.3
ImportĀ upstreamĀ versionĀ 2.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# -*- coding: utf-8 -*-
 
3
 
 
4
#Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
 
5
 
 
6
#This library is free software; you can redistribute it and/or
 
7
#modify it under the terms of the GNU Library General Public
 
8
#License as published by the Free Software Foundation; either
 
9
#version 2 of the License, or (at your option) any later version.
 
10
 
 
11
#This library is distributed in the hope that it will be useful,
 
12
#but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
14
#Library General Public License for more details.
 
15
 
 
16
#You should have received a copy of the GNU Library General Public License
 
17
#along with this library; see the file COPYING.LIB.  If not, write to
 
18
#the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 
19
#Boston, MA 02110-1301, USA.
 
20
 
 
21
from __future__ import with_statement
 
22
 
 
23
import sys
 
24
import os
 
25
import re
 
26
import logging
 
27
from subprocess import Popen, PIPE, STDOUT
 
28
from optparse import OptionParser
 
29
 
 
30
 
 
31
class Log(object):
 
32
    def __init__(self, name):
 
33
        self._log = logging.getLogger(name)
 
34
        self.debug = self._log.debug
 
35
        self.warn = self._log.warn
 
36
        self.error = self._log.error
 
37
        self.exception = self._log.exception
 
38
        self.info = self._log.info
 
39
 
 
40
 
 
41
class Options(Log):
 
42
    """ Option manager. It parses and checks script's parameters, sets an internal variable. """
 
43
 
 
44
    def __init__(self, args):
 
45
        Log.__init__(self, "Options")
 
46
        log = self._log
 
47
        opt = OptionParser("%prog [options] PathToSearch.\nTry -h or --help.")
 
48
        opt.add_option("-j", "--parallel-level", action="store", type="int",
 
49
              dest="parallel_level", default=None,
 
50
              help="Number of parallel processes executing the Qt's tests. Default: cpu count.")
 
51
        opt.add_option("-v", "--verbose", action="store", type="int",
 
52
              dest="verbose", default=2,
 
53
              help="Verbose level (0 - quiet, 1 - errors only, 2 - infos and warnings, 3 - debug information). Default: %default.")
 
54
        opt.add_option("", "--tests-options", action="store", type="string",
 
55
              dest="tests_options", default="",
 
56
              help="Parameters passed to Qt's tests (for example '-eventdelay 123').")
 
57
        opt.add_option("-o", "--output-file", action="store", type="string",
 
58
              dest="output_file", default="/tmp/qtwebkittests.html",
 
59
              help="File where results will be stored. The file will be overwritten. Default: %default.")
 
60
        opt.add_option("-b", "--browser", action="store", dest="browser",
 
61
              default="xdg-open",
 
62
              help="Browser in which results will be opened. Default %default.")
 
63
        opt.add_option("", "--do-not-open-results", action="store_false",
 
64
              dest="open_results", default=True,
 
65
              help="The results shouldn't pop-up in a browser automatically")
 
66
        opt.add_option("-d", "--developer-mode", action="store_true",
 
67
              dest="developer", default=False,
 
68
              help="Special mode for debugging. In general it simulates human behavior, running all autotests. In the mode everything is executed synchronously, no html output will be generated, no changes or transformation will be applied to stderr or stdout. In this mode options; parallel-level, output-file, browser and do-not-open-results will be ignored.")
 
69
        opt.add_option("-t", "--timeout", action="store", type="int",
 
70
              dest="timeout", default=0,
 
71
              help="Timeout in seconds for each testsuite. Zero value means that there is not timeout. Default: %default.")
 
72
 
 
73
        self._o, self._a = opt.parse_args(args)
 
74
        verbose = self._o.verbose
 
75
        if verbose == 0:
 
76
            logging.basicConfig(level=logging.CRITICAL,)
 
77
        elif verbose == 1:
 
78
            logging.basicConfig(level=logging.ERROR,)
 
79
        elif verbose == 2:
 
80
            logging.basicConfig(level=logging.INFO,)
 
81
        elif verbose == 3:
 
82
            logging.basicConfig(level=logging.DEBUG,)
 
83
        else:
 
84
            logging.basicConfig(level=logging.INFO,)
 
85
            log.warn("Bad verbose level, switching to default.")
 
86
        try:
 
87
            if not os.path.exists(self._a[0]):
 
88
                raise Exception("Given path doesn't exist.")
 
89
            if len(self._a) > 1:
 
90
                raise IndexError("Only one directory could be provided.")
 
91
            self._o.path = self._a[0]
 
92
        except IndexError:
 
93
            log.error("Bad usage. Please try -h or --help.")
 
94
            sys.exit(1)
 
95
        except Exception:
 
96
            log.error("Path '%s' doesn't exist", self._a[0])
 
97
            sys.exit(2)
 
98
        if self._o.developer:
 
99
            if not self._o.parallel_level is None:
 
100
                log.warn("Developer mode sets parallel-level option to one.")
 
101
            self._o.parallel_level = 1
 
102
            self._o.open_results = False
 
103
 
 
104
    def __getattr__(self, attr):
 
105
        """ Maps all options properties into this object (remove one level of indirection). """
 
106
        return getattr(self._o, attr)
 
107
 
 
108
 
 
109
def run_test(args):
 
110
    """ Runs one given test.
 
111
    args should contain a tuple with 3 elements;
 
112
      TestSuiteResult containing full file name of an autotest executable.
 
113
      str with options that should be passed to the autotest executable
 
114
      bool if true then the stdout will be buffered and separated from the stderr, if it is false
 
115
        then the stdout and the stderr will be merged together and left unbuffered (the TestSuiteResult output will be None).
 
116
      int time after which the autotest executable would be killed
 
117
    """
 
118
    log = logging.getLogger("Exec")
 
119
    test_suite, options, buffered, timeout = args
 
120
    timer = None
 
121
    try:
 
122
        log.info("Running... %s", test_suite.test_file_name())
 
123
        if buffered:
 
124
            tst = Popen([test_suite.test_file_name()] + options.split(), stdout=PIPE, stderr=None)
 
125
        else:
 
126
            tst = Popen([test_suite.test_file_name()] + options.split(), stdout=None, stderr=STDOUT)
 
127
        if timeout:
 
128
            from threading import Timer
 
129
            log.debug("Setting timeout timer %i sec on %s (process %s)", timeout, test_suite.test_file_name(), tst.pid)
 
130
            def process_killer():
 
131
                try:
 
132
                    try:
 
133
                        tst.terminate()
 
134
                    except AttributeError:
 
135
                        # Workaround for python version < 2.6 it can be removed as soon as we drop support for python2.5
 
136
                        try:
 
137
                            import ctypes
 
138
                            PROCESS_TERMINATE = 1
 
139
                            handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, tst.pid)
 
140
                            ctypes.windll.kernel32.TerminateProcess(handle, -1)
 
141
                            ctypes.windll.kernel32.CloseHandle(handle)
 
142
                        except AttributeError:
 
143
                            # windll is not accessible so we are on *nix like system
 
144
                            import signal
 
145
                            os.kill(tst.pid, signal.SIGTERM)
 
146
                    log.error("Timeout, process '%s' (%i) was terminated", test_suite.test_file_name(), tst.pid)
 
147
                except OSError, e:
 
148
                    # the process was finished before got killed
 
149
                    pass
 
150
            timer = Timer(timeout, process_killer)
 
151
            timer.start()
 
152
    except OSError, e:
 
153
        log.exception("Can't open an autotest file: '%s'. Skipping the test...", e.filename)
 
154
    else:
 
155
        test_suite.set_output(tst.communicate()[0])  # takes stdout only, in developer mode it would be None.
 
156
    log.info("Finished %s", test_suite.test_file_name())
 
157
    if timeout:
 
158
        timer.cancel()
 
159
    return test_suite
 
160
 
 
161
 
 
162
class TestSuiteResult(object):
 
163
    """ Keeps information about a test. """
 
164
 
 
165
    def __init__(self):
 
166
        self._output = None
 
167
        self._test_file_name = None
 
168
 
 
169
    def set_output(self, xml):
 
170
        if xml:
 
171
            self._output = xml.strip()
 
172
 
 
173
    def output(self):
 
174
        return self._output
 
175
 
 
176
    def set_test_file_name(self, file_name):
 
177
        self._test_file_name = file_name
 
178
 
 
179
    def test_file_name(self):
 
180
        return self._test_file_name
 
181
 
 
182
 
 
183
class Main(Log):
 
184
    """ The main script. All real work is done in run() method. """
 
185
 
 
186
    def __init__(self, options):
 
187
        Log.__init__(self, "Main")
 
188
        self._options = options
 
189
        if options.parallel_level > 1 or options.parallel_level is None:
 
190
            try:
 
191
                from multiprocessing import Pool
 
192
            except ImportError:
 
193
                self.warn("Import Error: the multiprocessing module couldn't be loaded (may be lack of python-multiprocessing package?). The Qt autotests will be executed one by one.")
 
194
                options.parallel_level = 1
 
195
        if options.parallel_level == 1:
 
196
 
 
197
            class Pool(object):
 
198
                """ A hack, created to avoid problems with multiprocessing module, this class is single thread replacement for the multiprocessing.Pool class. """
 
199
                def __init__(self, processes):
 
200
                    pass
 
201
 
 
202
                def imap_unordered(self, func, files):
 
203
                    return map(func, files)
 
204
 
 
205
                def map(self, func, files):
 
206
                    return map(func, files)
 
207
 
 
208
        self._Pool = Pool
 
209
 
 
210
    def run(self):
 
211
        """ Find && execute && publish results of all test. "All in one" function. """
 
212
        # This is needed for Qt finding our QML modules. The current code makes our
 
213
        # two existing API tests (WK1 API and WK2 UI process API) work correctly.
 
214
        qml_import_path = self._options.path + "../../../../imports"
 
215
        qml_import_path += ":" + self._options.path + "../../../../../../imports"
 
216
        os.putenv("QML_IMPORT_PATH", qml_import_path)
 
217
        path = os.getenv("PATH")
 
218
        path += ":" + self._options.path + "../../../../../../bin"
 
219
        os.putenv("PATH", path)
 
220
        self.debug("Searching executables...")
 
221
        tests_executables = self.find_tests_paths(self._options.path)
 
222
        self.debug("Found: %s", len(tests_executables))
 
223
        self.debug("Executing tests...")
 
224
        results = self.run_tests(tests_executables)
 
225
        if not self._options.developer:
 
226
            self.debug("Transforming...")
 
227
            transformed_results = self.transform(results)
 
228
            self.debug("Publishing...")
 
229
            self.announce_results(transformed_results)
 
230
 
 
231
    def find_tests_paths(self, path):
 
232
        """ Finds all tests executables inside the given path. """
 
233
        executables = []
 
234
        for root, dirs, files in os.walk(path):
 
235
            # Check only for a file that name starts from 'tst_' and that we can execute.
 
236
            filtered_path = filter(lambda w: w.startswith('tst_') and os.access(os.path.join(root, w), os.X_OK), files)
 
237
            filtered_path = map(lambda w: os.path.join(root, w), filtered_path)
 
238
            for file_name in filtered_path:
 
239
                r = TestSuiteResult()
 
240
                r.set_test_file_name(file_name)
 
241
                executables.append(r)
 
242
        return executables
 
243
 
 
244
    def run_tests(self, files):
 
245
        """ Executes given files by using a pool of workers. """
 
246
        workers = self._Pool(processes=self._options.parallel_level)
 
247
        # to each file add options.
 
248
        self.debug("Using %s the workers pool, number of workers %i", repr(workers), self._options.parallel_level)
 
249
        package = map(lambda w: [w, self._options.tests_options, not self._options.developer, self._options.timeout], files)
 
250
        self.debug("Generated packages for workers: %s", repr(package))
 
251
        results = workers.map(run_test, package)  # Collects results.
 
252
        return results
 
253
 
 
254
    def transform(self, results):
 
255
        """ Transforms list of the results to specialized versions. """
 
256
        stdout = self.convert_to_stdout(results)
 
257
        html = self.convert_to_html(results)
 
258
        return {"stdout": stdout, "html": html}
 
259
 
 
260
    def announce_results(self, results):
 
261
        """ Shows the results. """
 
262
        self.announce_results_stdout(results['stdout'])
 
263
        self.announce_results_html(results['html'])
 
264
 
 
265
    def announce_results_stdout(self, results):
 
266
        """ Show the results by printing to the stdout."""
 
267
        print(results)
 
268
 
 
269
    def announce_results_html(self, results):
 
270
        """ Shows the result by creating a html file and calling a web browser to render it. """
 
271
        with file(self._options.output_file, 'w') as f:
 
272
            f.write(results)
 
273
        if self._options.open_results:
 
274
            Popen(self._options.browser + " " + self._options.output_file, stdout=None, stderr=None, shell=True)
 
275
 
 
276
    def check_crash_occurences(self, results):
 
277
        """ Checks if any test crashes and it sums them  """
 
278
        totals = [0,0,0]
 
279
        crash_count = 0
 
280
        txt = []
 
281
        #collecting results into one container with checking crash
 
282
        for result in results:
 
283
            found = None
 
284
            if result.output():
 
285
                txt.append(result.output())
 
286
                found = re.search(r"([0-9]+) passed, ([0-9]+) failed, ([0-9]+) skipped", result.output())
 
287
 
 
288
            if found:
 
289
                totals = reduce(lambda x, y: (int(x[0]) + int(y[0]), int(x[1]) + int(y[1]), int(x[2]) + int(y[2])), (totals, found.groups()))
 
290
            else:
 
291
                txt.append('CRASHED: %s' % result.test_file_name())
 
292
                crash_count += 1
 
293
                self.warn("Missing sub-summary: %s" % result.test_file_name())
 
294
 
 
295
        txt='\n\n'.join(txt)
 
296
 
 
297
        totals = list(totals)
 
298
        totals.append(crash_count)
 
299
        totals = map(str, totals)
 
300
        return txt, totals
 
301
 
 
302
    def convert_to_stdout(self, results):
 
303
        """ Converts results, that they could be nicely presented in the stdout. """
 
304
        txt, totals = self.check_crash_occurences(results)
 
305
 
 
306
        totals = "%s passed, %s failed, %s skipped, %s crashed" % (totals[0], totals[1], totals[2], totals[3])
 
307
 
 
308
        txt += '\n' + '*' * 70
 
309
        txt += "\n**" + ("TOTALS: " + totals).center(66) + '**'
 
310
        txt += '\n' + '*' * 70 + '\n'
 
311
        return txt
 
312
 
 
313
    def convert_to_html(self, results):
 
314
        """ Converts results, that they could showed as a html page. """
 
315
        txt, totals = self.check_crash_occurences(results)
 
316
        txt = txt.replace('&', '&amp;').replace('<', "&lt;").replace('>', "&gt;")
 
317
        # Add a color and a style.
 
318
        txt = re.sub(r"([* ]+(Finished)[ a-z_A-Z0-9]+[*]+)",
 
319
            lambda w: r"",
 
320
            txt)
 
321
        txt = re.sub(r"([*]+[ a-z_A-Z0-9]+[*]+)",
 
322
            lambda w: "<case class='good'><br><br><b>" + w.group(0) + r"</b></case>",
 
323
            txt)
 
324
        txt = re.sub(r"(Config: Using QTest library)((.)+)",
 
325
            lambda w: "\n<case class='good'><br><i>" + w.group(0) + r"</i>  ",
 
326
            txt)
 
327
        txt = re.sub(r"\n(PASS)((.)+)",
 
328
            lambda w: "</case>\n<case class='good'><br><status class='pass'>" + w.group(1) + r"</status>" + w.group(2),
 
329
            txt)
 
330
        txt = re.sub(r"\n(FAIL!)((.)+)",
 
331
            lambda w: "</case>\n<case class='bad'><br><status class='fail'>" + w.group(1) + r"</status>" + w.group(2),
 
332
            txt)
 
333
        txt = re.sub(r"\n(XPASS)((.)+)",
 
334
            lambda w: "</case>\n<case class='bad'><br><status class='xpass'>" + w.group(1) + r"</status>" + w.group(2),
 
335
            txt)
 
336
        txt = re.sub(r"\n(XFAIL)((.)+)",
 
337
            lambda w: "</case>\n<case class='good'><br><status class='xfail'>" + w.group(1) + r"</status>" + w.group(2),
 
338
            txt)
 
339
        txt = re.sub(r"\n(SKIP)((.)+)",
 
340
            lambda w: "</case>\n<case class='good'><br><status class='xfail'>" + w.group(1) + r"</status>" + w.group(2),
 
341
            txt)
 
342
        txt = re.sub(r"\n(QWARN)((.)+)",
 
343
            lambda w: "</case>\n<case class='bad'><br><status class='warn'>" + w.group(1) + r"</status>" + w.group(2),
 
344
            txt)
 
345
        txt = re.sub(r"\n(RESULT)((.)+)",
 
346
            lambda w: "</case>\n<case class='good'><br><status class='benchmark'>" + w.group(1) + r"</status>" + w.group(2),
 
347
            txt)
 
348
        txt = re.sub(r"\n(QFATAL|CRASHED)((.)+)",
 
349
            lambda w: "</case>\n<case class='bad'><br><status class='crash'>" + w.group(1) + r"</status>" + w.group(2),
 
350
            txt)
 
351
        txt = re.sub(r"\n(Totals:)([0-9', a-z]*)",
 
352
            lambda w: "</case>\n<case class='good'><br><b>" + w.group(1) + r"</b>" + w.group(2) + "</case>",
 
353
            txt)
 
354
        # Find total count of failed, skipped, passed and crashed tests.
 
355
        totals = "%s passed, %s failed, %s skipped, %s crashed." % (totals[0], totals[1], totals[2], totals[3])
 
356
        # Create a header of the html source.
 
357
        txt = """
 
358
        <html>
 
359
        <head>
 
360
          <script>
 
361
          function init() {
 
362
              // Try to find the right styleSheet (this document could be embedded in an other html doc)
 
363
              for (i = document.styleSheets.length - 1; i >= 0; --i) {
 
364
                  if (document.styleSheets[i].cssRules[0].selectorText == "case.good") {
 
365
                      resultStyleSheet = i;
 
366
                      return;
 
367
                  }
 
368
              }
 
369
              // The styleSheet hasn't been found, but it should be the last one.
 
370
              resultStyleSheet = document.styleSheets.length - 1;
 
371
          }
 
372
 
 
373
          function hide() {
 
374
              document.styleSheets[resultStyleSheet].cssRules[0].style.display='none';
 
375
          }
 
376
 
 
377
          function show() {
 
378
              document.styleSheets[resultStyleSheet].cssRules[0].style.display='';
 
379
          }
 
380
 
 
381
          </script>
 
382
          <style type="text/css">
 
383
            case.good {color:black}
 
384
            case.bad {color:black}
 
385
            status.pass {color:green}
 
386
            status.crash {color:red}
 
387
            status.fail {color:red}
 
388
            status.xpass {color:663300}
 
389
            status.xfail {color:004500}
 
390
            status.benchmark {color:000088}
 
391
            status.warn {color:orange}
 
392
            status.crash {color:red; text-decoration:blink; background-color:black}
 
393
          </style>
 
394
        </head>
 
395
        <body onload="init()">
 
396
        <center>
 
397
          <h1>Qt's autotests results</h1>%(totals)s<br>
 
398
          <hr>
 
399
          <form>
 
400
            <input type="button" value="Show failures only" onclick="hide()"/>
 
401
            &nbsp;
 
402
            <input type="button" value="Show all" onclick="show()"/>
 
403
          </form>
 
404
        </center>
 
405
        <hr>
 
406
        %(results)s
 
407
        </body>
 
408
        </html>""" % {"totals": totals, "results": txt}
 
409
        return txt
 
410
 
 
411
 
 
412
if __name__ == '__main__':
 
413
    options = Options(sys.argv[1:])
 
414
    main = Main(options)
 
415
    main.run()