[CherryPick] Input Method upversion
[framework/web/webkit-efl.git] / Tools / Scripts / run-qtwebkit-tests
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()