1############################################################################# 2## 3## Copyright (C) 2018 The Qt Company Ltd. 4## Contact: https://www.qt.io/licensing/ 5## 6## This file is part of the Qt for Python project. 7## 8## $QT_BEGIN_LICENSE:LGPL$ 9## Commercial License Usage 10## Licensees holding valid commercial Qt licenses may use this file in 11## accordance with the commercial license agreement provided with the 12## Software or, alternatively, in accordance with the terms contained in 13## a written agreement between you and The Qt Company. For licensing terms 14## and conditions see https://www.qt.io/terms-conditions. For further 15## information use the contact form at https://www.qt.io/contact-us. 16## 17## GNU Lesser General Public License Usage 18## Alternatively, this file may be used under the terms of the GNU Lesser 19## General Public License version 3 as published by the Free Software 20## Foundation and appearing in the file LICENSE.LGPL3 included in the 21## packaging of this file. Please review the following information to 22## ensure the GNU Lesser General Public License version 3 requirements 23## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24## 25## GNU General Public License Usage 26## Alternatively, this file may be used under the terms of the GNU 27## General Public License version 2.0 or (at your option) the GNU General 28## Public license version 3 or any later version approved by the KDE Free 29## Qt Foundation. The licenses are as published by the Free Software 30## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31## included in the packaging of this file. Please review the following 32## information to ensure the GNU General Public License requirements will 33## be met: https://www.gnu.org/licenses/gpl-2.0.html and 34## https://www.gnu.org/licenses/gpl-3.0.html. 35## 36## $QT_END_LICENSE$ 37## 38############################################################################# 39 40from __future__ import print_function 41 42import os 43import sys 44import re 45import subprocess 46import inspect 47 48from collections import namedtuple 49from textwrap import dedent 50 51from .buildlog import builds 52from .helper import decorate, PY3, TimeoutExpired 53 54# Get the dir path to the utils module 55try: 56 this_file = __file__ 57except NameError: 58 this_file = sys.argv[0] 59this_file = os.path.abspath(this_file) 60this_dir = os.path.dirname(this_file) 61build_scripts_dir = os.path.abspath(os.path.join(this_dir, '../build_scripts')) 62 63sys.path.append(build_scripts_dir) 64from utils import detect_clang 65 66class TestRunner(object): 67 def __init__(self, log_entry, project, index): 68 self.log_entry = log_entry 69 built_path = log_entry.build_dir 70 self.test_dir = os.path.join(built_path, project) 71 log_dir = log_entry.log_dir 72 if index is not None: 73 self.logfile = os.path.join(log_dir, project + ".{}.log".format(index)) 74 else: 75 self.logfile = os.path.join(log_dir, project + ".log") 76 os.environ['CTEST_OUTPUT_ON_FAILURE'] = '1' 77 self._setup_clang() 78 self._setup() 79 80 def _setup_clang(self): 81 if sys.platform != "win32": 82 return 83 clang_dir = detect_clang() 84 if clang_dir[0]: 85 clang_bin_dir = os.path.join(clang_dir[0], 'bin') 86 path = os.environ.get('PATH') 87 if not clang_bin_dir in path: 88 os.environ['PATH'] = clang_bin_dir + os.pathsep + path 89 print("Adding %s as detected by %s to PATH" % (clang_bin_dir, clang_dir[1])) 90 91 def _find_ctest_in_file(self, file_name): 92 """ 93 Helper for _find_ctest() that finds the ctest binary in a build 94 system file (ninja, Makefile). 95 """ 96 look_for = "--force-new-ctest-process" 97 line = None 98 with open(file_name) as makefile: 99 for line in makefile: 100 if look_for in line: 101 break 102 else: 103 # We have probably forgotten to build the tests. 104 # Give a nice error message with a shortened but exact path. 105 rel_path = os.path.relpath(file_name) 106 msg = dedent("""\n 107 {line} 108 ** ctest is not in '{}'. 109 * Did you forget to build the tests with '--build-tests' in setup.py? 110 """).format(rel_path, line=79 * "*") 111 raise RuntimeError(msg) 112 # the ctest program is on the left to look_for 113 assert line, "Did not find {}".format(look_for) 114 ctest = re.search(r'(\S+|"([^"]+)")\s+' + look_for, line).groups() 115 return ctest[1] or ctest[0] 116 117 def _find_ctest(self): 118 """ 119 Find ctest in a build system file (ninja, Makefile) 120 121 We no longer use make, but the ctest command directly. 122 It is convenient to look for the ctest program using the Makefile. 123 This serves us two purposes: 124 125 - there is no dependency of the PATH variable, 126 - each project is checked whether ctest was configured. 127 """ 128 candidate_files = ["Makefile", "build.ninja"] 129 for candidate in candidate_files: 130 path = os.path.join(self.test_dir, candidate) 131 if os.path.exists(path): 132 return self._find_ctest_in_file(path) 133 raise RuntimeError('Cannot find any of the build system files {}.'.format( 134 ', '.join(candidate_files))) 135 136 def _setup(self): 137 self.ctestCommand = self._find_ctest() 138 139 def _run(self, cmd_tuple, label, timeout): 140 """ 141 Perform a test run in a given build 142 143 The build can be stopped by a keyboard interrupt for testing 144 this script. Also, a timeout can be used. 145 146 After the change to directly using ctest, we no longer use 147 "--force-new-ctest-process". Until now this has no drawbacks 148 but was a little faster. 149 """ 150 151 self.cmd = cmd_tuple 152 # We no longer use the shell option. It introduces wrong handling 153 # of certain characters which are not yet correctly escaped: 154 # Especially the "^" caret char is treated as an escape, and pipe symbols 155 # without a caret are interpreted as such which leads to weirdness. 156 # Since we have all commands with explicit paths and don't use shell 157 # commands, this should work fine. 158 print(dedent("""\ 159 running {cmd} 160 in {test_dir} 161 """).format(**self.__dict__)) 162 ctest_process = subprocess.Popen(self.cmd, 163 cwd=self.test_dir, 164 stdout=subprocess.PIPE, 165 stderr=subprocess.STDOUT) 166 def py_tee(input, output, label): 167 ''' 168 A simple (incomplete) tee command in Python 169 170 This script simply logs everything from input to output 171 while the output gets some decoration. The specific reason 172 to have this script at all is: 173 174 - it is necessary to have some decoration as prefix, since 175 we run commands several times 176 177 - collecting all output and then decorating is not nice if 178 you have to wait for a long time 179 180 The special escape is for the case of an embedded file in 181 the output. 182 ''' 183 def xprint(*args, **kw): 184 print(*args, file=output, **kw) 185 186 # 'for line in input:' would read into too large chunks 187 labelled = True 188 # make sure that this text is not found in a traceback of the runner! 189 text_a = "BEGIN" "_FILE" 190 text_z = "END" "_FILE" 191 while True: 192 line = input.readline() 193 if not line: 194 break 195 if line.startswith(text_a): 196 labelled = False 197 txt = line.rstrip() 198 xprint(label, txt) if label and labelled else xprint(txt) 199 if line.startswith(text_z): 200 labelled = True 201 202 tee_src = dedent("""\ 203 from __future__ import print_function 204 import sys 205 {} 206 py_tee(sys.stdin, sys.stdout, '{label}') 207 """).format(dedent(inspect.getsource(py_tee)), label=label) 208 tee_cmd = (sys.executable, "-E", "-u", "-c", tee_src) 209 tee_process = subprocess.Popen(tee_cmd, 210 cwd=self.test_dir, 211 stdin=ctest_process.stdout) 212 try: 213 comm = tee_process.communicate 214 output = (comm(timeout=timeout) if PY3 else comm())[0] 215 except (TimeoutExpired, KeyboardInterrupt): 216 print() 217 print("aborted, partial result") 218 ctest_process.kill() 219 outs, errs = ctest_process.communicate() 220 # ctest lists to a temp file. Move it to the log 221 tmp_name = self.logfile + ".tmp" 222 if os.path.exists(tmp_name): 223 if os.path.exists(self.logfile): 224 os.unlink(self.logfile) 225 os.rename(tmp_name, self.logfile) 226 self.partial = True 227 else: 228 self.partial = False 229 finally: 230 print("End of the test run") 231 print() 232 tee_process.wait() 233 234 def run(self, label, rerun, timeout): 235 cmd = self.ctestCommand, "--output-log", self.logfile 236 if rerun is not None: 237 # cmd += ("--rerun-failed",) 238 # For some reason, this worked never in the script file. 239 # We pass instead the test names as a regex: 240 words = "^(" + "|".join(rerun) + ")$" 241 cmd += ("--tests-regex", words) 242 self._run(cmd, label, timeout) 243# eof 244