##// END OF EJS Templates
hg: remove HGUNICODEPEDANTRY and RTUNICODEPEDANTRY...
Gregory Szorc -
r43343:5c9c71cd default
parent child Browse files
Show More
@@ -1,43 +1,36
1 1 #!/usr/bin/env python
2 2 #
3 3 # mercurial - scalable distributed SCM
4 4 #
5 5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9 from __future__ import absolute_import
10 10
11 11 import os
12 12 import sys
13 13
14 if os.environ.get('HGUNICODEPEDANTRY', False):
15 try:
16 reload(sys)
17 sys.setdefaultencoding("undefined")
18 except NameError:
19 pass
20
21 14 libdir = '@LIBDIR@'
22 15
23 16 if libdir != '@' 'LIBDIR' '@':
24 17 if not os.path.isabs(libdir):
25 18 libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
26 19 libdir)
27 20 libdir = os.path.abspath(libdir)
28 21 sys.path.insert(0, libdir)
29 22
30 23 from hgdemandimport import tracing
31 24 with tracing.log('hg script'):
32 25 # enable importing on demand to reduce startup time
33 26 try:
34 27 if sys.version_info[0] < 3 or sys.version_info >= (3, 6):
35 28 import hgdemandimport; hgdemandimport.enable()
36 29 except ImportError:
37 30 sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" %
38 31 ' '.join(sys.path))
39 32 sys.stderr.write("(check your install and PYTHONPATH)\n")
40 33 sys.exit(-1)
41 34
42 35 from mercurial import dispatch
43 36 dispatch.run()
@@ -1,233 +1,219
1 1 // main.rs -- Main routines for `hg` program
2 2 //
3 3 // Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 extern crate libc;
9 9 extern crate cpython;
10 10 extern crate python27_sys;
11 11
12 12 use cpython::{NoArgs, ObjectProtocol, PyModule, PyResult, Python};
13 13 use libc::{c_char, c_int};
14 14
15 15 use std::env;
16 16 use std::path::PathBuf;
17 17 use std::ffi::{CString, OsStr};
18 18 #[cfg(target_family = "unix")]
19 19 use std::os::unix::ffi::{OsStrExt, OsStringExt};
20 20
21 21 #[derive(Debug)]
22 22 struct Environment {
23 23 _exe: PathBuf,
24 24 python_exe: PathBuf,
25 25 python_home: PathBuf,
26 26 mercurial_modules: PathBuf,
27 27 }
28 28
29 29 /// Run Mercurial locally from a source distribution or checkout.
30 30 ///
31 31 /// hg is <srcdir>/rust/target/<target>/hg
32 32 /// Python interpreter is detected by build script.
33 33 /// Python home is relative to Python interpreter.
34 34 /// Mercurial files are relative to hg binary, which is relative to source root.
35 35 #[cfg(feature = "localdev")]
36 36 fn get_environment() -> Environment {
37 37 let exe = env::current_exe().unwrap();
38 38
39 39 let mut mercurial_modules = exe.clone();
40 40 mercurial_modules.pop(); // /rust/target/<target>
41 41 mercurial_modules.pop(); // /rust/target
42 42 mercurial_modules.pop(); // /rust
43 43 mercurial_modules.pop(); // /
44 44
45 45 let python_exe: &'static str = env!("PYTHON_INTERPRETER");
46 46 let python_exe = PathBuf::from(python_exe);
47 47
48 48 let mut python_home = python_exe.clone();
49 49 python_home.pop();
50 50
51 51 // On Windows, python2.7.exe exists at the root directory of the Python
52 52 // install. Everywhere else, the Python install root is one level up.
53 53 if !python_exe.ends_with("python2.7.exe") {
54 54 python_home.pop();
55 55 }
56 56
57 57 Environment {
58 58 _exe: exe.clone(),
59 59 python_exe: python_exe,
60 60 python_home: python_home,
61 61 mercurial_modules: mercurial_modules.to_path_buf(),
62 62 }
63 63 }
64 64
65 65 // On UNIX, platform string is just bytes and should not contain NUL.
66 66 #[cfg(target_family = "unix")]
67 67 fn cstring_from_os<T: AsRef<OsStr>>(s: T) -> CString {
68 68 CString::new(s.as_ref().as_bytes()).unwrap()
69 69 }
70 70
71 71 // TODO convert to ANSI characters?
72 72 #[cfg(target_family = "windows")]
73 73 fn cstring_from_os<T: AsRef<OsStr>>(s: T) -> CString {
74 74 CString::new(s.as_ref().to_str().unwrap()).unwrap()
75 75 }
76 76
77 77 // On UNIX, argv starts as an array of char*. So it is easy to convert
78 78 // to C strings.
79 79 #[cfg(target_family = "unix")]
80 80 fn args_to_cstrings() -> Vec<CString> {
81 81 env::args_os()
82 82 .map(|a| CString::new(a.into_vec()).unwrap())
83 83 .collect()
84 84 }
85 85
86 86 // TODO Windows support is incomplete. We should either use env::args_os()
87 87 // (or call into GetCommandLineW() + CommandLinetoArgvW()), convert these to
88 88 // PyUnicode instances, and pass these into Python/Mercurial outside the
89 89 // standard PySys_SetArgvEx() mechanism. This will allow us to preserve the
90 90 // raw bytes (since PySys_SetArgvEx() is based on char* and can drop wchar
91 91 // data.
92 92 //
93 93 // For now, we use env::args(). This will choke on invalid UTF-8 arguments.
94 94 // But it is better than nothing.
95 95 #[cfg(target_family = "windows")]
96 96 fn args_to_cstrings() -> Vec<CString> {
97 97 env::args().map(|a| CString::new(a).unwrap()).collect()
98 98 }
99 99
100 100 fn set_python_home(env: &Environment) {
101 101 let raw = cstring_from_os(&env.python_home).into_raw();
102 102 unsafe {
103 103 python27_sys::Py_SetPythonHome(raw);
104 104 }
105 105 }
106 106
107 fn update_encoding(_py: Python, _sys_mod: &PyModule) {
108 // Call sys.setdefaultencoding("undefined") if HGUNICODEPEDANTRY is set.
109 let pedantry = env::var("HGUNICODEPEDANTRY").is_ok();
110
111 if pedantry {
112 // site.py removes the sys.setdefaultencoding attribute. So we need
113 // to reload the module to get a handle on it. This is a lesser
114 // used feature and we'll support this later.
115 // TODO support this
116 panic!("HGUNICODEPEDANTRY is not yet supported");
117 }
118 }
119
120 107 fn update_modules_path(env: &Environment, py: Python, sys_mod: &PyModule) {
121 108 let sys_path = sys_mod.get(py, "path").unwrap();
122 109 sys_path
123 110 .call_method(py, "insert", (0, env.mercurial_modules.to_str()), None)
124 111 .expect("failed to update sys.path to location of Mercurial modules");
125 112 }
126 113
127 114 fn run() -> Result<(), i32> {
128 115 let env = get_environment();
129 116
130 117 //println!("{:?}", env);
131 118
132 119 // Tell Python where it is installed.
133 120 set_python_home(&env);
134 121
135 122 // Set program name. The backing memory needs to live for the duration of the
136 123 // interpreter.
137 124 //
138 125 // TODO consider storing this in a static or associating with lifetime of
139 126 // the Python interpreter.
140 127 //
141 128 // Yes, we use the path to the Python interpreter not argv[0] here. The
142 129 // reason is because Python uses the given path to find the location of
143 130 // Python files. Apparently we could define our own ``Py_GetPath()``
144 131 // implementation. But this may require statically linking Python, which is
145 132 // not desirable.
146 133 let program_name = cstring_from_os(&env.python_exe).as_ptr();
147 134 unsafe {
148 135 python27_sys::Py_SetProgramName(program_name as *mut i8);
149 136 }
150 137
151 138 unsafe {
152 139 python27_sys::Py_Initialize();
153 140 }
154 141
155 142 // https://docs.python.org/2/c-api/init.html#c.PySys_SetArgvEx has important
156 143 // usage information about PySys_SetArgvEx:
157 144 //
158 145 // * It says the first argument should be the script that is being executed.
159 146 // If not a script, it can be empty. We are definitely not a script.
160 147 // However, parts of Mercurial do look at sys.argv[0]. So we need to set
161 148 // something here.
162 149 //
163 150 // * When embedding Python, we should use ``PySys_SetArgvEx()`` and set
164 151 // ``updatepath=0`` for security reasons. Essentially, Python's default
165 152 // logic will treat an empty argv[0] in a manner that could result in
166 153 // sys.path picking up directories it shouldn't and this could lead to
167 154 // loading untrusted modules.
168 155
169 156 // env::args() will panic if it sees a non-UTF-8 byte sequence. And
170 157 // Mercurial supports arbitrary encodings of input data. So we need to
171 158 // use OS-specific mechanisms to get the raw bytes without UTF-8
172 159 // interference.
173 160 let args = args_to_cstrings();
174 161 let argv: Vec<*const c_char> = args.iter().map(|a| a.as_ptr()).collect();
175 162
176 163 unsafe {
177 164 python27_sys::PySys_SetArgvEx(args.len() as c_int, argv.as_ptr() as *mut *mut i8, 0);
178 165 }
179 166
180 167 let result;
181 168 {
182 169 // These need to be dropped before we call Py_Finalize(). Hence the
183 170 // block.
184 171 let gil = Python::acquire_gil();
185 172 let py = gil.python();
186 173
187 174 // Mercurial code could call sys.exit(), which will call exit()
188 175 // itself. So this may not return.
189 176 // TODO this may cause issues on Windows due to the CRT mismatch.
190 177 // Investigate if we can intercept sys.exit() or SystemExit() to
191 178 // ensure we handle process exit.
192 179 result = match run_py(&env, py) {
193 180 // Print unhandled exceptions and exit code 255, as this is what
194 181 // `python` does.
195 182 Err(err) => {
196 183 err.print(py);
197 184 Err(255)
198 185 }
199 186 Ok(()) => Ok(()),
200 187 };
201 188 }
202 189
203 190 unsafe {
204 191 python27_sys::Py_Finalize();
205 192 }
206 193
207 194 result
208 195 }
209 196
210 197 fn run_py(env: &Environment, py: Python) -> PyResult<()> {
211 198 let sys_mod = py.import("sys").unwrap();
212 199
213 update_encoding(py, &sys_mod);
214 200 update_modules_path(&env, py, &sys_mod);
215 201
216 202 // TODO consider a better error message on failure to import.
217 203 let demand_mod = py.import("hgdemandimport")?;
218 204 demand_mod.call(py, "enable", NoArgs, None)?;
219 205
220 206 let dispatch_mod = py.import("mercurial.dispatch")?;
221 207 dispatch_mod.call(py, "run", NoArgs, None)?;
222 208
223 209 Ok(())
224 210 }
225 211
226 212 fn main() {
227 213 let exit_code = match run() {
228 214 Err(err) => err,
229 215 Ok(()) => 0,
230 216 };
231 217
232 218 std::process::exit(exit_code);
233 219 }
@@ -1,3357 +1,3350
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 # 10) parallel, pure, tests that call run-tests:
39 39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 40 #
41 41 # (You could use any subset of the tests: test-s* happens to match
42 42 # enough that it's worth doing parallel runs, few enough that it
43 43 # completes fairly quickly, includes both shell and Python scripts, and
44 44 # includes some scripts that run daemon processes.)
45 45
46 46 from __future__ import absolute_import, print_function
47 47
48 48 import argparse
49 49 import collections
50 50 import difflib
51 51 import distutils.version as version
52 52 import errno
53 53 import json
54 54 import multiprocessing
55 55 import os
56 56 import random
57 57 import re
58 58 import shutil
59 59 import signal
60 60 import socket
61 61 import subprocess
62 62 import sys
63 63 import sysconfig
64 64 import tempfile
65 65 import threading
66 66 import time
67 67 import unittest
68 68 import uuid
69 69 import xml.dom.minidom as minidom
70 70
71 71 try:
72 72 import Queue as queue
73 73 except ImportError:
74 74 import queue
75 75
76 76 try:
77 77 import shlex
78 78 shellquote = shlex.quote
79 79 except (ImportError, AttributeError):
80 80 import pipes
81 81 shellquote = pipes.quote
82 82
83 if os.environ.get('RTUNICODEPEDANTRY', False):
84 try:
85 reload(sys)
86 sys.setdefaultencoding("undefined")
87 except NameError:
88 pass
89
90 83 processlock = threading.Lock()
91 84
92 85 pygmentspresent = False
93 86 # ANSI color is unsupported prior to Windows 10
94 87 if os.name != 'nt':
95 88 try: # is pygments installed
96 89 import pygments
97 90 import pygments.lexers as lexers
98 91 import pygments.lexer as lexer
99 92 import pygments.formatters as formatters
100 93 import pygments.token as token
101 94 import pygments.style as style
102 95 pygmentspresent = True
103 96 difflexer = lexers.DiffLexer()
104 97 terminal256formatter = formatters.Terminal256Formatter()
105 98 except ImportError:
106 99 pass
107 100
108 101 if pygmentspresent:
109 102 class TestRunnerStyle(style.Style):
110 103 default_style = ""
111 104 skipped = token.string_to_tokentype("Token.Generic.Skipped")
112 105 failed = token.string_to_tokentype("Token.Generic.Failed")
113 106 skippedname = token.string_to_tokentype("Token.Generic.SName")
114 107 failedname = token.string_to_tokentype("Token.Generic.FName")
115 108 styles = {
116 109 skipped: '#e5e5e5',
117 110 skippedname: '#00ffff',
118 111 failed: '#7f0000',
119 112 failedname: '#ff0000',
120 113 }
121 114
122 115 class TestRunnerLexer(lexer.RegexLexer):
123 116 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
124 117 tokens = {
125 118 'root': [
126 119 (r'^Skipped', token.Generic.Skipped, 'skipped'),
127 120 (r'^Failed ', token.Generic.Failed, 'failed'),
128 121 (r'^ERROR: ', token.Generic.Failed, 'failed'),
129 122 ],
130 123 'skipped': [
131 124 (testpattern, token.Generic.SName),
132 125 (r':.*', token.Generic.Skipped),
133 126 ],
134 127 'failed': [
135 128 (testpattern, token.Generic.FName),
136 129 (r'(:| ).*', token.Generic.Failed),
137 130 ]
138 131 }
139 132
140 133 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
141 134 runnerlexer = TestRunnerLexer()
142 135
143 136 origenviron = os.environ.copy()
144 137
145 138 if sys.version_info > (3, 5, 0):
146 139 PYTHON3 = True
147 140 xrange = range # we use xrange in one place, and we'd rather not use range
148 141 def _bytespath(p):
149 142 if p is None:
150 143 return p
151 144 return p.encode('utf-8')
152 145
153 146 def _strpath(p):
154 147 if p is None:
155 148 return p
156 149 return p.decode('utf-8')
157 150
158 151 osenvironb = getattr(os, 'environb', None)
159 152 if osenvironb is None:
160 153 # Windows lacks os.environb, for instance. A proxy over the real thing
161 154 # instead of a copy allows the environment to be updated via bytes on
162 155 # all platforms.
163 156 class environbytes(object):
164 157 def __init__(self, strenv):
165 158 self.__len__ = strenv.__len__
166 159 self.clear = strenv.clear
167 160 self._strenv = strenv
168 161 def __getitem__(self, k):
169 162 v = self._strenv.__getitem__(_strpath(k))
170 163 return _bytespath(v)
171 164 def __setitem__(self, k, v):
172 165 self._strenv.__setitem__(_strpath(k), _strpath(v))
173 166 def __delitem__(self, k):
174 167 self._strenv.__delitem__(_strpath(k))
175 168 def __contains__(self, k):
176 169 return self._strenv.__contains__(_strpath(k))
177 170 def __iter__(self):
178 171 return iter([_bytespath(k) for k in iter(self._strenv)])
179 172 def get(self, k, default=None):
180 173 v = self._strenv.get(_strpath(k), _strpath(default))
181 174 return _bytespath(v)
182 175 def pop(self, k, default=None):
183 176 v = self._strenv.pop(_strpath(k), _strpath(default))
184 177 return _bytespath(v)
185 178
186 179 osenvironb = environbytes(os.environ)
187 180
188 181 getcwdb = getattr(os, 'getcwdb')
189 182 if not getcwdb or os.name == 'nt':
190 183 getcwdb = lambda: _bytespath(os.getcwd())
191 184
192 185 elif sys.version_info >= (3, 0, 0):
193 186 print('%s is only supported on Python 3.5+ and 2.7, not %s' %
194 187 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
195 188 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
196 189 else:
197 190 PYTHON3 = False
198 191
199 192 # In python 2.x, path operations are generally done using
200 193 # bytestrings by default, so we don't have to do any extra
201 194 # fiddling there. We define the wrapper functions anyway just to
202 195 # help keep code consistent between platforms.
203 196 def _bytespath(p):
204 197 return p
205 198
206 199 _strpath = _bytespath
207 200 osenvironb = os.environ
208 201 getcwdb = os.getcwd
209 202
210 203 # For Windows support
211 204 wifexited = getattr(os, "WIFEXITED", lambda x: False)
212 205
213 206 # Whether to use IPv6
214 207 def checksocketfamily(name, port=20058):
215 208 """return true if we can listen on localhost using family=name
216 209
217 210 name should be either 'AF_INET', or 'AF_INET6'.
218 211 port being used is okay - EADDRINUSE is considered as successful.
219 212 """
220 213 family = getattr(socket, name, None)
221 214 if family is None:
222 215 return False
223 216 try:
224 217 s = socket.socket(family, socket.SOCK_STREAM)
225 218 s.bind(('localhost', port))
226 219 s.close()
227 220 return True
228 221 except socket.error as exc:
229 222 if exc.errno == errno.EADDRINUSE:
230 223 return True
231 224 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
232 225 return False
233 226 else:
234 227 raise
235 228 else:
236 229 return False
237 230
238 231 # useipv6 will be set by parseargs
239 232 useipv6 = None
240 233
241 234 def checkportisavailable(port):
242 235 """return true if a port seems free to bind on localhost"""
243 236 if useipv6:
244 237 family = socket.AF_INET6
245 238 else:
246 239 family = socket.AF_INET
247 240 try:
248 241 s = socket.socket(family, socket.SOCK_STREAM)
249 242 s.bind(('localhost', port))
250 243 s.close()
251 244 return True
252 245 except socket.error as exc:
253 246 if exc.errno not in (errno.EADDRINUSE, errno.EADDRNOTAVAIL,
254 247 errno.EPROTONOSUPPORT):
255 248 raise
256 249 return False
257 250
258 251 closefds = os.name == 'posix'
259 252 def Popen4(cmd, wd, timeout, env=None):
260 253 processlock.acquire()
261 254 p = subprocess.Popen(_strpath(cmd), shell=True, bufsize=-1,
262 255 cwd=_strpath(wd), env=env,
263 256 close_fds=closefds,
264 257 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
265 258 stderr=subprocess.STDOUT)
266 259 processlock.release()
267 260
268 261 p.fromchild = p.stdout
269 262 p.tochild = p.stdin
270 263 p.childerr = p.stderr
271 264
272 265 p.timeout = False
273 266 if timeout:
274 267 def t():
275 268 start = time.time()
276 269 while time.time() - start < timeout and p.returncode is None:
277 270 time.sleep(.1)
278 271 p.timeout = True
279 272 if p.returncode is None:
280 273 terminate(p)
281 274 threading.Thread(target=t).start()
282 275
283 276 return p
284 277
285 278 if sys.executable:
286 279 sysexecutable = sys.executable
287 280 elif os.environ.get('PYTHONEXECUTABLE'):
288 281 sysexecutable = os.environ['PYTHONEXECUTABLE']
289 282 elif os.environ.get('PYTHON'):
290 283 sysexecutable = os.environ['PYTHON']
291 284 else:
292 285 raise AssertionError('Could not find Python interpreter')
293 286
294 287 PYTHON = _bytespath(sysexecutable.replace('\\', '/'))
295 288 IMPL_PATH = b'PYTHONPATH'
296 289 if 'java' in sys.platform:
297 290 IMPL_PATH = b'JYTHONPATH'
298 291
299 292 defaults = {
300 293 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
301 294 'timeout': ('HGTEST_TIMEOUT', 180),
302 295 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
303 296 'port': ('HGTEST_PORT', 20059),
304 297 'shell': ('HGTEST_SHELL', 'sh'),
305 298 }
306 299
307 300 def canonpath(path):
308 301 return os.path.realpath(os.path.expanduser(path))
309 302
310 303 def parselistfiles(files, listtype, warn=True):
311 304 entries = dict()
312 305 for filename in files:
313 306 try:
314 307 path = os.path.expanduser(os.path.expandvars(filename))
315 308 f = open(path, "rb")
316 309 except IOError as err:
317 310 if err.errno != errno.ENOENT:
318 311 raise
319 312 if warn:
320 313 print("warning: no such %s file: %s" % (listtype, filename))
321 314 continue
322 315
323 316 for line in f.readlines():
324 317 line = line.split(b'#', 1)[0].strip()
325 318 if line:
326 319 entries[line] = filename
327 320
328 321 f.close()
329 322 return entries
330 323
331 324 def parsettestcases(path):
332 325 """read a .t test file, return a set of test case names
333 326
334 327 If path does not exist, return an empty set.
335 328 """
336 329 cases = []
337 330 try:
338 331 with open(path, 'rb') as f:
339 332 for l in f:
340 333 if l.startswith(b'#testcases '):
341 334 cases.append(sorted(l[11:].split()))
342 335 except IOError as ex:
343 336 if ex.errno != errno.ENOENT:
344 337 raise
345 338 return cases
346 339
347 340 def getparser():
348 341 """Obtain the OptionParser used by the CLI."""
349 342 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
350 343
351 344 selection = parser.add_argument_group('Test Selection')
352 345 selection.add_argument('--allow-slow-tests', action='store_true',
353 346 help='allow extremely slow tests')
354 347 selection.add_argument("--blacklist", action="append",
355 348 help="skip tests listed in the specified blacklist file")
356 349 selection.add_argument("--changed",
357 350 help="run tests that are changed in parent rev or working directory")
358 351 selection.add_argument("-k", "--keywords",
359 352 help="run tests matching keywords")
360 353 selection.add_argument("-r", "--retest", action="store_true",
361 354 help = "retest failed tests")
362 355 selection.add_argument("--test-list", action="append",
363 356 help="read tests to run from the specified file")
364 357 selection.add_argument("--whitelist", action="append",
365 358 help="always run tests listed in the specified whitelist file")
366 359 selection.add_argument('tests', metavar='TESTS', nargs='*',
367 360 help='Tests to run')
368 361
369 362 harness = parser.add_argument_group('Test Harness Behavior')
370 363 harness.add_argument('--bisect-repo',
371 364 metavar='bisect_repo',
372 365 help=("Path of a repo to bisect. Use together with "
373 366 "--known-good-rev"))
374 367 harness.add_argument("-d", "--debug", action="store_true",
375 368 help="debug mode: write output of test scripts to console"
376 369 " rather than capturing and diffing it (disables timeout)")
377 370 harness.add_argument("-f", "--first", action="store_true",
378 371 help="exit on the first test failure")
379 372 harness.add_argument("-i", "--interactive", action="store_true",
380 373 help="prompt to accept changed output")
381 374 harness.add_argument("-j", "--jobs", type=int,
382 375 help="number of jobs to run in parallel"
383 376 " (default: $%s or %d)" % defaults['jobs'])
384 377 harness.add_argument("--keep-tmpdir", action="store_true",
385 378 help="keep temporary directory after running tests")
386 379 harness.add_argument('--known-good-rev',
387 380 metavar="known_good_rev",
388 381 help=("Automatically bisect any failures using this "
389 382 "revision as a known-good revision."))
390 383 harness.add_argument("--list-tests", action="store_true",
391 384 help="list tests instead of running them")
392 385 harness.add_argument("--loop", action="store_true",
393 386 help="loop tests repeatedly")
394 387 harness.add_argument('--random', action="store_true",
395 388 help='run tests in random order')
396 389 harness.add_argument('--order-by-runtime', action="store_true",
397 390 help='run slowest tests first, according to .testtimes')
398 391 harness.add_argument("-p", "--port", type=int,
399 392 help="port on which servers should listen"
400 393 " (default: $%s or %d)" % defaults['port'])
401 394 harness.add_argument('--profile-runner', action='store_true',
402 395 help='run statprof on run-tests')
403 396 harness.add_argument("-R", "--restart", action="store_true",
404 397 help="restart at last error")
405 398 harness.add_argument("--runs-per-test", type=int, dest="runs_per_test",
406 399 help="run each test N times (default=1)", default=1)
407 400 harness.add_argument("--shell",
408 401 help="shell to use (default: $%s or %s)" % defaults['shell'])
409 402 harness.add_argument('--showchannels', action='store_true',
410 403 help='show scheduling channels')
411 404 harness.add_argument("--slowtimeout", type=int,
412 405 help="kill errant slow tests after SLOWTIMEOUT seconds"
413 406 " (default: $%s or %d)" % defaults['slowtimeout'])
414 407 harness.add_argument("-t", "--timeout", type=int,
415 408 help="kill errant tests after TIMEOUT seconds"
416 409 " (default: $%s or %d)" % defaults['timeout'])
417 410 harness.add_argument("--tmpdir",
418 411 help="run tests in the given temporary directory"
419 412 " (implies --keep-tmpdir)")
420 413 harness.add_argument("-v", "--verbose", action="store_true",
421 414 help="output verbose messages")
422 415
423 416 hgconf = parser.add_argument_group('Mercurial Configuration')
424 417 hgconf.add_argument("--chg", action="store_true",
425 418 help="install and use chg wrapper in place of hg")
426 419 hgconf.add_argument("--compiler",
427 420 help="compiler to build with")
428 421 hgconf.add_argument('--extra-config-opt', action="append", default=[],
429 422 help='set the given config opt in the test hgrc')
430 423 hgconf.add_argument("-l", "--local", action="store_true",
431 424 help="shortcut for --with-hg=<testdir>/../hg, "
432 425 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
433 426 hgconf.add_argument("--ipv6", action="store_true",
434 427 help="prefer IPv6 to IPv4 for network related tests")
435 428 hgconf.add_argument("--pure", action="store_true",
436 429 help="use pure Python code instead of C extensions")
437 430 hgconf.add_argument("-3", "--py3-warnings", action="store_true",
438 431 help="enable Py3k warnings on Python 2.7+")
439 432 hgconf.add_argument("--with-chg", metavar="CHG",
440 433 help="use specified chg wrapper in place of hg")
441 434 hgconf.add_argument("--with-hg",
442 435 metavar="HG",
443 436 help="test using specified hg script rather than a "
444 437 "temporary installation")
445 438
446 439 reporting = parser.add_argument_group('Results Reporting')
447 440 reporting.add_argument("-C", "--annotate", action="store_true",
448 441 help="output files annotated with coverage")
449 442 reporting.add_argument("--color", choices=["always", "auto", "never"],
450 443 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
451 444 help="colorisation: always|auto|never (default: auto)")
452 445 reporting.add_argument("-c", "--cover", action="store_true",
453 446 help="print a test coverage report")
454 447 reporting.add_argument('--exceptions', action='store_true',
455 448 help='log all exceptions and generate an exception report')
456 449 reporting.add_argument("-H", "--htmlcov", action="store_true",
457 450 help="create an HTML report of the coverage of the files")
458 451 reporting.add_argument("--json", action="store_true",
459 452 help="store test result data in 'report.json' file")
460 453 reporting.add_argument("--outputdir",
461 454 help="directory to write error logs to (default=test directory)")
462 455 reporting.add_argument("-n", "--nodiff", action="store_true",
463 456 help="skip showing test changes")
464 457 reporting.add_argument("-S", "--noskips", action="store_true",
465 458 help="don't report skip tests verbosely")
466 459 reporting.add_argument("--time", action="store_true",
467 460 help="time how long each test takes")
468 461 reporting.add_argument("--view",
469 462 help="external diff viewer")
470 463 reporting.add_argument("--xunit",
471 464 help="record xunit results at specified path")
472 465
473 466 for option, (envvar, default) in defaults.items():
474 467 defaults[option] = type(default)(os.environ.get(envvar, default))
475 468 parser.set_defaults(**defaults)
476 469
477 470 return parser
478 471
479 472 def parseargs(args, parser):
480 473 """Parse arguments with our OptionParser and validate results."""
481 474 options = parser.parse_args(args)
482 475
483 476 # jython is always pure
484 477 if 'java' in sys.platform or '__pypy__' in sys.modules:
485 478 options.pure = True
486 479
487 480 if options.local:
488 481 if options.with_hg or options.with_chg:
489 482 parser.error('--local cannot be used with --with-hg or --with-chg')
490 483 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
491 484 reporootdir = os.path.dirname(testdir)
492 485 pathandattrs = [(b'hg', 'with_hg')]
493 486 if options.chg:
494 487 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
495 488 for relpath, attr in pathandattrs:
496 489 binpath = os.path.join(reporootdir, relpath)
497 490 if os.name != 'nt' and not os.access(binpath, os.X_OK):
498 491 parser.error('--local specified, but %r not found or '
499 492 'not executable' % binpath)
500 493 setattr(options, attr, _strpath(binpath))
501 494
502 495 if options.with_hg:
503 496 options.with_hg = canonpath(_bytespath(options.with_hg))
504 497 if not (os.path.isfile(options.with_hg) and
505 498 os.access(options.with_hg, os.X_OK)):
506 499 parser.error('--with-hg must specify an executable hg script')
507 500 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
508 501 sys.stderr.write('warning: --with-hg should specify an hg script\n')
509 502 sys.stderr.flush()
510 503
511 504 if (options.chg or options.with_chg) and os.name == 'nt':
512 505 parser.error('chg does not work on %s' % os.name)
513 506 if options.with_chg:
514 507 options.chg = False # no installation to temporary location
515 508 options.with_chg = canonpath(_bytespath(options.with_chg))
516 509 if not (os.path.isfile(options.with_chg) and
517 510 os.access(options.with_chg, os.X_OK)):
518 511 parser.error('--with-chg must specify a chg executable')
519 512 if options.chg and options.with_hg:
520 513 # chg shares installation location with hg
521 514 parser.error('--chg does not work when --with-hg is specified '
522 515 '(use --with-chg instead)')
523 516
524 517 if options.color == 'always' and not pygmentspresent:
525 518 sys.stderr.write('warning: --color=always ignored because '
526 519 'pygments is not installed\n')
527 520
528 521 if options.bisect_repo and not options.known_good_rev:
529 522 parser.error("--bisect-repo cannot be used without --known-good-rev")
530 523
531 524 global useipv6
532 525 if options.ipv6:
533 526 useipv6 = checksocketfamily('AF_INET6')
534 527 else:
535 528 # only use IPv6 if IPv4 is unavailable and IPv6 is available
536 529 useipv6 = ((not checksocketfamily('AF_INET'))
537 530 and checksocketfamily('AF_INET6'))
538 531
539 532 options.anycoverage = options.cover or options.annotate or options.htmlcov
540 533 if options.anycoverage:
541 534 try:
542 535 import coverage
543 536 covver = version.StrictVersion(coverage.__version__).version
544 537 if covver < (3, 3):
545 538 parser.error('coverage options require coverage 3.3 or later')
546 539 except ImportError:
547 540 parser.error('coverage options now require the coverage package')
548 541
549 542 if options.anycoverage and options.local:
550 543 # this needs some path mangling somewhere, I guess
551 544 parser.error("sorry, coverage options do not work when --local "
552 545 "is specified")
553 546
554 547 if options.anycoverage and options.with_hg:
555 548 parser.error("sorry, coverage options do not work when --with-hg "
556 549 "is specified")
557 550
558 551 global verbose
559 552 if options.verbose:
560 553 verbose = ''
561 554
562 555 if options.tmpdir:
563 556 options.tmpdir = canonpath(options.tmpdir)
564 557
565 558 if options.jobs < 1:
566 559 parser.error('--jobs must be positive')
567 560 if options.interactive and options.debug:
568 561 parser.error("-i/--interactive and -d/--debug are incompatible")
569 562 if options.debug:
570 563 if options.timeout != defaults['timeout']:
571 564 sys.stderr.write(
572 565 'warning: --timeout option ignored with --debug\n')
573 566 if options.slowtimeout != defaults['slowtimeout']:
574 567 sys.stderr.write(
575 568 'warning: --slowtimeout option ignored with --debug\n')
576 569 options.timeout = 0
577 570 options.slowtimeout = 0
578 571 if options.py3_warnings:
579 572 if PYTHON3:
580 573 parser.error(
581 574 '--py3-warnings can only be used on Python 2.7')
582 575
583 576 if options.blacklist:
584 577 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
585 578 if options.whitelist:
586 579 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
587 580 else:
588 581 options.whitelisted = {}
589 582
590 583 if options.showchannels:
591 584 options.nodiff = True
592 585
593 586 return options
594 587
595 588 def rename(src, dst):
596 589 """Like os.rename(), trade atomicity and opened files friendliness
597 590 for existing destination support.
598 591 """
599 592 shutil.copy(src, dst)
600 593 os.remove(src)
601 594
602 595 def makecleanable(path):
603 596 """Try to fix directory permission recursively so that the entire tree
604 597 can be deleted"""
605 598 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
606 599 for d in dirnames:
607 600 p = os.path.join(dirpath, d)
608 601 try:
609 602 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
610 603 except OSError:
611 604 pass
612 605
613 606 _unified_diff = difflib.unified_diff
614 607 if PYTHON3:
615 608 import functools
616 609 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
617 610
618 611 def getdiff(expected, output, ref, err):
619 612 servefail = False
620 613 lines = []
621 614 for line in _unified_diff(expected, output, ref, err):
622 615 if line.startswith(b'+++') or line.startswith(b'---'):
623 616 line = line.replace(b'\\', b'/')
624 617 if line.endswith(b' \n'):
625 618 line = line[:-2] + b'\n'
626 619 lines.append(line)
627 620 if not servefail and line.startswith(
628 621 b'+ abort: child process failed to start'):
629 622 servefail = True
630 623
631 624 return servefail, lines
632 625
633 626 verbose = False
634 627 def vlog(*msg):
635 628 """Log only when in verbose mode."""
636 629 if verbose is False:
637 630 return
638 631
639 632 return log(*msg)
640 633
641 634 # Bytes that break XML even in a CDATA block: control characters 0-31
642 635 # sans \t, \n and \r
643 636 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
644 637
645 638 # Match feature conditionalized output lines in the form, capturing the feature
646 639 # list in group 2, and the preceeding line output in group 1:
647 640 #
648 641 # output..output (feature !)\n
649 642 optline = re.compile(br'(.*) \((.+?) !\)\n$')
650 643
651 644 def cdatasafe(data):
652 645 """Make a string safe to include in a CDATA block.
653 646
654 647 Certain control characters are illegal in a CDATA block, and
655 648 there's no way to include a ]]> in a CDATA either. This function
656 649 replaces illegal bytes with ? and adds a space between the ]] so
657 650 that it won't break the CDATA block.
658 651 """
659 652 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
660 653
661 654 def log(*msg):
662 655 """Log something to stdout.
663 656
664 657 Arguments are strings to print.
665 658 """
666 659 with iolock:
667 660 if verbose:
668 661 print(verbose, end=' ')
669 662 for m in msg:
670 663 print(m, end=' ')
671 664 print()
672 665 sys.stdout.flush()
673 666
674 667 def highlightdiff(line, color):
675 668 if not color:
676 669 return line
677 670 assert pygmentspresent
678 671 return pygments.highlight(line.decode('latin1'), difflexer,
679 672 terminal256formatter).encode('latin1')
680 673
681 674 def highlightmsg(msg, color):
682 675 if not color:
683 676 return msg
684 677 assert pygmentspresent
685 678 return pygments.highlight(msg, runnerlexer, runnerformatter)
686 679
687 680 def terminate(proc):
688 681 """Terminate subprocess"""
689 682 vlog('# Terminating process %d' % proc.pid)
690 683 try:
691 684 proc.terminate()
692 685 except OSError:
693 686 pass
694 687
695 688 def killdaemons(pidfile):
696 689 import killdaemons as killmod
697 690 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
698 691 logfn=vlog)
699 692
700 693 class Test(unittest.TestCase):
701 694 """Encapsulates a single, runnable test.
702 695
703 696 While this class conforms to the unittest.TestCase API, it differs in that
704 697 instances need to be instantiated manually. (Typically, unittest.TestCase
705 698 classes are instantiated automatically by scanning modules.)
706 699 """
707 700
708 701 # Status code reserved for skipped tests (used by hghave).
709 702 SKIPPED_STATUS = 80
710 703
711 704 def __init__(self, path, outputdir, tmpdir, keeptmpdir=False,
712 705 debug=False,
713 706 first=False,
714 707 timeout=None,
715 708 startport=None, extraconfigopts=None,
716 709 py3warnings=False, shell=None, hgcommand=None,
717 710 slowtimeout=None, usechg=False,
718 711 useipv6=False):
719 712 """Create a test from parameters.
720 713
721 714 path is the full path to the file defining the test.
722 715
723 716 tmpdir is the main temporary directory to use for this test.
724 717
725 718 keeptmpdir determines whether to keep the test's temporary directory
726 719 after execution. It defaults to removal (False).
727 720
728 721 debug mode will make the test execute verbosely, with unfiltered
729 722 output.
730 723
731 724 timeout controls the maximum run time of the test. It is ignored when
732 725 debug is True. See slowtimeout for tests with #require slow.
733 726
734 727 slowtimeout overrides timeout if the test has #require slow.
735 728
736 729 startport controls the starting port number to use for this test. Each
737 730 test will reserve 3 port numbers for execution. It is the caller's
738 731 responsibility to allocate a non-overlapping port range to Test
739 732 instances.
740 733
741 734 extraconfigopts is an iterable of extra hgrc config options. Values
742 735 must have the form "key=value" (something understood by hgrc). Values
743 736 of the form "foo.key=value" will result in "[foo] key=value".
744 737
745 738 py3warnings enables Py3k warnings.
746 739
747 740 shell is the shell to execute tests in.
748 741 """
749 742 if timeout is None:
750 743 timeout = defaults['timeout']
751 744 if startport is None:
752 745 startport = defaults['port']
753 746 if slowtimeout is None:
754 747 slowtimeout = defaults['slowtimeout']
755 748 self.path = path
756 749 self.bname = os.path.basename(path)
757 750 self.name = _strpath(self.bname)
758 751 self._testdir = os.path.dirname(path)
759 752 self._outputdir = outputdir
760 753 self._tmpname = os.path.basename(path)
761 754 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
762 755
763 756 self._threadtmp = tmpdir
764 757 self._keeptmpdir = keeptmpdir
765 758 self._debug = debug
766 759 self._first = first
767 760 self._timeout = timeout
768 761 self._slowtimeout = slowtimeout
769 762 self._startport = startport
770 763 self._extraconfigopts = extraconfigopts or []
771 764 self._py3warnings = py3warnings
772 765 self._shell = _bytespath(shell)
773 766 self._hgcommand = hgcommand or b'hg'
774 767 self._usechg = usechg
775 768 self._useipv6 = useipv6
776 769
777 770 self._aborted = False
778 771 self._daemonpids = []
779 772 self._finished = None
780 773 self._ret = None
781 774 self._out = None
782 775 self._skipped = None
783 776 self._testtmp = None
784 777 self._chgsockdir = None
785 778
786 779 self._refout = self.readrefout()
787 780
788 781 def readrefout(self):
789 782 """read reference output"""
790 783 # If we're not in --debug mode and reference output file exists,
791 784 # check test output against it.
792 785 if self._debug:
793 786 return None # to match "out is None"
794 787 elif os.path.exists(self.refpath):
795 788 with open(self.refpath, 'rb') as f:
796 789 return f.read().splitlines(True)
797 790 else:
798 791 return []
799 792
800 793 # needed to get base class __repr__ running
801 794 @property
802 795 def _testMethodName(self):
803 796 return self.name
804 797
805 798 def __str__(self):
806 799 return self.name
807 800
808 801 def shortDescription(self):
809 802 return self.name
810 803
811 804 def setUp(self):
812 805 """Tasks to perform before run()."""
813 806 self._finished = False
814 807 self._ret = None
815 808 self._out = None
816 809 self._skipped = None
817 810
818 811 try:
819 812 os.mkdir(self._threadtmp)
820 813 except OSError as e:
821 814 if e.errno != errno.EEXIST:
822 815 raise
823 816
824 817 name = self._tmpname
825 818 self._testtmp = os.path.join(self._threadtmp, name)
826 819 os.mkdir(self._testtmp)
827 820
828 821 # Remove any previous output files.
829 822 if os.path.exists(self.errpath):
830 823 try:
831 824 os.remove(self.errpath)
832 825 except OSError as e:
833 826 # We might have raced another test to clean up a .err
834 827 # file, so ignore ENOENT when removing a previous .err
835 828 # file.
836 829 if e.errno != errno.ENOENT:
837 830 raise
838 831
839 832 if self._usechg:
840 833 self._chgsockdir = os.path.join(self._threadtmp,
841 834 b'%s.chgsock' % name)
842 835 os.mkdir(self._chgsockdir)
843 836
844 837 def run(self, result):
845 838 """Run this test and report results against a TestResult instance."""
846 839 # This function is extremely similar to unittest.TestCase.run(). Once
847 840 # we require Python 2.7 (or at least its version of unittest), this
848 841 # function can largely go away.
849 842 self._result = result
850 843 result.startTest(self)
851 844 try:
852 845 try:
853 846 self.setUp()
854 847 except (KeyboardInterrupt, SystemExit):
855 848 self._aborted = True
856 849 raise
857 850 except Exception:
858 851 result.addError(self, sys.exc_info())
859 852 return
860 853
861 854 success = False
862 855 try:
863 856 self.runTest()
864 857 except KeyboardInterrupt:
865 858 self._aborted = True
866 859 raise
867 860 except unittest.SkipTest as e:
868 861 result.addSkip(self, str(e))
869 862 # The base class will have already counted this as a
870 863 # test we "ran", but we want to exclude skipped tests
871 864 # from those we count towards those run.
872 865 result.testsRun -= 1
873 866 except self.failureException as e:
874 867 # This differs from unittest in that we don't capture
875 868 # the stack trace. This is for historical reasons and
876 869 # this decision could be revisited in the future,
877 870 # especially for PythonTest instances.
878 871 if result.addFailure(self, str(e)):
879 872 success = True
880 873 except Exception:
881 874 result.addError(self, sys.exc_info())
882 875 else:
883 876 success = True
884 877
885 878 try:
886 879 self.tearDown()
887 880 except (KeyboardInterrupt, SystemExit):
888 881 self._aborted = True
889 882 raise
890 883 except Exception:
891 884 result.addError(self, sys.exc_info())
892 885 success = False
893 886
894 887 if success:
895 888 result.addSuccess(self)
896 889 finally:
897 890 result.stopTest(self, interrupted=self._aborted)
898 891
899 892 def runTest(self):
900 893 """Run this test instance.
901 894
902 895 This will return a tuple describing the result of the test.
903 896 """
904 897 env = self._getenv()
905 898 self._genrestoreenv(env)
906 899 self._daemonpids.append(env['DAEMON_PIDS'])
907 900 self._createhgrc(env['HGRCPATH'])
908 901
909 902 vlog('# Test', self.name)
910 903
911 904 ret, out = self._run(env)
912 905 self._finished = True
913 906 self._ret = ret
914 907 self._out = out
915 908
916 909 def describe(ret):
917 910 if ret < 0:
918 911 return 'killed by signal: %d' % -ret
919 912 return 'returned error code %d' % ret
920 913
921 914 self._skipped = False
922 915
923 916 if ret == self.SKIPPED_STATUS:
924 917 if out is None: # Debug mode, nothing to parse.
925 918 missing = ['unknown']
926 919 failed = None
927 920 else:
928 921 missing, failed = TTest.parsehghaveoutput(out)
929 922
930 923 if not missing:
931 924 missing = ['skipped']
932 925
933 926 if failed:
934 927 self.fail('hg have failed checking for %s' % failed[-1])
935 928 else:
936 929 self._skipped = True
937 930 raise unittest.SkipTest(missing[-1])
938 931 elif ret == 'timeout':
939 932 self.fail('timed out')
940 933 elif ret is False:
941 934 self.fail('no result code from test')
942 935 elif out != self._refout:
943 936 # Diff generation may rely on written .err file.
944 937 if ((ret != 0 or out != self._refout) and not self._skipped
945 938 and not self._debug):
946 939 with open(self.errpath, 'wb') as f:
947 940 for line in out:
948 941 f.write(line)
949 942
950 943 # The result object handles diff calculation for us.
951 944 with firstlock:
952 945 if self._result.addOutputMismatch(self, ret, out, self._refout):
953 946 # change was accepted, skip failing
954 947 return
955 948 if self._first:
956 949 global firsterror
957 950 firsterror = True
958 951
959 952 if ret:
960 953 msg = 'output changed and ' + describe(ret)
961 954 else:
962 955 msg = 'output changed'
963 956
964 957 self.fail(msg)
965 958 elif ret:
966 959 self.fail(describe(ret))
967 960
968 961 def tearDown(self):
969 962 """Tasks to perform after run()."""
970 963 for entry in self._daemonpids:
971 964 killdaemons(entry)
972 965 self._daemonpids = []
973 966
974 967 if self._keeptmpdir:
975 968 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
976 969 (self._testtmp.decode('utf-8'),
977 970 self._threadtmp.decode('utf-8')))
978 971 else:
979 972 try:
980 973 shutil.rmtree(self._testtmp)
981 974 except OSError:
982 975 # unreadable directory may be left in $TESTTMP; fix permission
983 976 # and try again
984 977 makecleanable(self._testtmp)
985 978 shutil.rmtree(self._testtmp, True)
986 979 shutil.rmtree(self._threadtmp, True)
987 980
988 981 if self._usechg:
989 982 # chgservers will stop automatically after they find the socket
990 983 # files are deleted
991 984 shutil.rmtree(self._chgsockdir, True)
992 985
993 986 if ((self._ret != 0 or self._out != self._refout) and not self._skipped
994 987 and not self._debug and self._out):
995 988 with open(self.errpath, 'wb') as f:
996 989 for line in self._out:
997 990 f.write(line)
998 991
999 992 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1000 993
1001 994 def _run(self, env):
1002 995 # This should be implemented in child classes to run tests.
1003 996 raise unittest.SkipTest('unknown test type')
1004 997
1005 998 def abort(self):
1006 999 """Terminate execution of this test."""
1007 1000 self._aborted = True
1008 1001
1009 1002 def _portmap(self, i):
1010 1003 offset = b'' if i == 0 else b'%d' % i
1011 1004 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1012 1005
1013 1006 def _getreplacements(self):
1014 1007 """Obtain a mapping of text replacements to apply to test output.
1015 1008
1016 1009 Test output needs to be normalized so it can be compared to expected
1017 1010 output. This function defines how some of that normalization will
1018 1011 occur.
1019 1012 """
1020 1013 r = [
1021 1014 # This list should be parallel to defineport in _getenv
1022 1015 self._portmap(0),
1023 1016 self._portmap(1),
1024 1017 self._portmap(2),
1025 1018 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1026 1019 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1027 1020 ]
1028 1021 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1029 1022
1030 1023 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1031 1024
1032 1025 if os.path.exists(replacementfile):
1033 1026 data = {}
1034 1027 with open(replacementfile, mode='rb') as source:
1035 1028 # the intermediate 'compile' step help with debugging
1036 1029 code = compile(source.read(), replacementfile, 'exec')
1037 1030 exec(code, data)
1038 1031 for value in data.get('substitutions', ()):
1039 1032 if len(value) != 2:
1040 1033 msg = 'malformatted substitution in %s: %r'
1041 1034 msg %= (replacementfile, value)
1042 1035 raise ValueError(msg)
1043 1036 r.append(value)
1044 1037 return r
1045 1038
1046 1039 def _escapepath(self, p):
1047 1040 if os.name == 'nt':
1048 1041 return (
1049 1042 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
1050 1043 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
1051 1044 for c in [p[i:i + 1] for i in range(len(p))]))
1052 1045 )
1053 1046 else:
1054 1047 return re.escape(p)
1055 1048
1056 1049 def _localip(self):
1057 1050 if self._useipv6:
1058 1051 return b'::1'
1059 1052 else:
1060 1053 return b'127.0.0.1'
1061 1054
1062 1055 def _genrestoreenv(self, testenv):
1063 1056 """Generate a script that can be used by tests to restore the original
1064 1057 environment."""
1065 1058 # Put the restoreenv script inside self._threadtmp
1066 1059 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1067 1060 testenv['HGTEST_RESTOREENV'] = _strpath(scriptpath)
1068 1061
1069 1062 # Only restore environment variable names that the shell allows
1070 1063 # us to export.
1071 1064 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1072 1065
1073 1066 # Do not restore these variables; otherwise tests would fail.
1074 1067 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1075 1068
1076 1069 with open(scriptpath, 'w') as envf:
1077 1070 for name, value in origenviron.items():
1078 1071 if not name_regex.match(name):
1079 1072 # Skip environment variables with unusual names not
1080 1073 # allowed by most shells.
1081 1074 continue
1082 1075 if name in reqnames:
1083 1076 continue
1084 1077 envf.write('%s=%s\n' % (name, shellquote(value)))
1085 1078
1086 1079 for name in testenv:
1087 1080 if name in origenviron or name in reqnames:
1088 1081 continue
1089 1082 envf.write('unset %s\n' % (name,))
1090 1083
1091 1084 def _getenv(self):
1092 1085 """Obtain environment variables to use during test execution."""
1093 1086 def defineport(i):
1094 1087 offset = '' if i == 0 else '%s' % i
1095 1088 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1096 1089 env = os.environ.copy()
1097 1090 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1098 1091 env['HGEMITWARNINGS'] = '1'
1099 1092 env['TESTTMP'] = _strpath(self._testtmp)
1100 1093 env['TESTNAME'] = self.name
1101 1094 env['HOME'] = _strpath(self._testtmp)
1102 1095 # This number should match portneeded in _getport
1103 1096 for port in xrange(3):
1104 1097 # This list should be parallel to _portmap in _getreplacements
1105 1098 defineport(port)
1106 1099 env["HGRCPATH"] = _strpath(os.path.join(self._threadtmp, b'.hgrc'))
1107 1100 env["DAEMON_PIDS"] = _strpath(os.path.join(self._threadtmp,
1108 1101 b'daemon.pids'))
1109 1102 env["HGEDITOR"] = ('"' + sysexecutable + '"'
1110 1103 + ' -c "import sys; sys.exit(0)"')
1111 1104 env["HGUSER"] = "test"
1112 1105 env["HGENCODING"] = "ascii"
1113 1106 env["HGENCODINGMODE"] = "strict"
1114 1107 env["HGHOSTNAME"] = "test-hostname"
1115 1108 env['HGIPV6'] = str(int(self._useipv6))
1116 1109 # See contrib/catapipe.py for how to use this functionality.
1117 1110 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1118 1111 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1119 1112 # non-test one in as a default, otherwise set to devnull
1120 1113 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1121 1114 'HGCATAPULTSERVERPIPE', os.devnull)
1122 1115
1123 1116 extraextensions = []
1124 1117 for opt in self._extraconfigopts:
1125 1118 section, key = opt.encode('utf-8').split(b'.', 1)
1126 1119 if section != 'extensions':
1127 1120 continue
1128 1121 name = key.split(b'=', 1)[0]
1129 1122 extraextensions.append(name)
1130 1123
1131 1124 if extraextensions:
1132 1125 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1133 1126
1134 1127 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1135 1128 # IP addresses.
1136 1129 env['LOCALIP'] = _strpath(self._localip())
1137 1130
1138 1131 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1139 1132 # but this is needed for testing python instances like dummyssh,
1140 1133 # dummysmtpd.py, and dumbhttp.py.
1141 1134 if PYTHON3 and os.name == 'nt':
1142 1135 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1143 1136
1144 1137 # Reset some environment variables to well-known values so that
1145 1138 # the tests produce repeatable output.
1146 1139 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1147 1140 env['TZ'] = 'GMT'
1148 1141 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1149 1142 env['COLUMNS'] = '80'
1150 1143 env['TERM'] = 'xterm'
1151 1144
1152 1145 dropped = [
1153 1146 'CDPATH',
1154 1147 'CHGDEBUG',
1155 1148 'EDITOR',
1156 1149 'GREP_OPTIONS',
1157 1150 'HG',
1158 1151 'HGMERGE',
1159 1152 'HGPLAIN',
1160 1153 'HGPLAINEXCEPT',
1161 1154 'HGPROF',
1162 1155 'http_proxy',
1163 1156 'no_proxy',
1164 1157 'NO_PROXY',
1165 1158 'PAGER',
1166 1159 'VISUAL',
1167 1160 ]
1168 1161
1169 1162 for k in dropped:
1170 1163 if k in env:
1171 1164 del env[k]
1172 1165
1173 1166 # unset env related to hooks
1174 1167 for k in list(env):
1175 1168 if k.startswith('HG_'):
1176 1169 del env[k]
1177 1170
1178 1171 if self._usechg:
1179 1172 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1180 1173
1181 1174 return env
1182 1175
1183 1176 def _createhgrc(self, path):
1184 1177 """Create an hgrc file for this test."""
1185 1178 with open(path, 'wb') as hgrc:
1186 1179 hgrc.write(b'[ui]\n')
1187 1180 hgrc.write(b'slash = True\n')
1188 1181 hgrc.write(b'interactive = False\n')
1189 1182 hgrc.write(b'merge = internal:merge\n')
1190 1183 hgrc.write(b'mergemarkers = detailed\n')
1191 1184 hgrc.write(b'promptecho = True\n')
1192 1185 hgrc.write(b'[defaults]\n')
1193 1186 hgrc.write(b'[devel]\n')
1194 1187 hgrc.write(b'all-warnings = true\n')
1195 1188 hgrc.write(b'default-date = 0 0\n')
1196 1189 hgrc.write(b'[largefiles]\n')
1197 1190 hgrc.write(b'usercache = %s\n' %
1198 1191 (os.path.join(self._testtmp, b'.cache/largefiles')))
1199 1192 hgrc.write(b'[lfs]\n')
1200 1193 hgrc.write(b'usercache = %s\n' %
1201 1194 (os.path.join(self._testtmp, b'.cache/lfs')))
1202 1195 hgrc.write(b'[web]\n')
1203 1196 hgrc.write(b'address = localhost\n')
1204 1197 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
1205 1198 hgrc.write(b'server-header = testing stub value\n')
1206 1199
1207 1200 for opt in self._extraconfigopts:
1208 1201 section, key = opt.encode('utf-8').split(b'.', 1)
1209 1202 assert b'=' in key, ('extra config opt %s must '
1210 1203 'have an = for assignment' % opt)
1211 1204 hgrc.write(b'[%s]\n%s\n' % (section, key))
1212 1205
1213 1206 def fail(self, msg):
1214 1207 # unittest differentiates between errored and failed.
1215 1208 # Failed is denoted by AssertionError (by default at least).
1216 1209 raise AssertionError(msg)
1217 1210
1218 1211 def _runcommand(self, cmd, env, normalizenewlines=False):
1219 1212 """Run command in a sub-process, capturing the output (stdout and
1220 1213 stderr).
1221 1214
1222 1215 Return a tuple (exitcode, output). output is None in debug mode.
1223 1216 """
1224 1217 if self._debug:
1225 1218 proc = subprocess.Popen(_strpath(cmd), shell=True,
1226 1219 cwd=_strpath(self._testtmp),
1227 1220 env=env)
1228 1221 ret = proc.wait()
1229 1222 return (ret, None)
1230 1223
1231 1224 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1232 1225 def cleanup():
1233 1226 terminate(proc)
1234 1227 ret = proc.wait()
1235 1228 if ret == 0:
1236 1229 ret = signal.SIGTERM << 8
1237 1230 killdaemons(env['DAEMON_PIDS'])
1238 1231 return ret
1239 1232
1240 1233 proc.tochild.close()
1241 1234
1242 1235 try:
1243 1236 output = proc.fromchild.read()
1244 1237 except KeyboardInterrupt:
1245 1238 vlog('# Handling keyboard interrupt')
1246 1239 cleanup()
1247 1240 raise
1248 1241
1249 1242 ret = proc.wait()
1250 1243 if wifexited(ret):
1251 1244 ret = os.WEXITSTATUS(ret)
1252 1245
1253 1246 if proc.timeout:
1254 1247 ret = 'timeout'
1255 1248
1256 1249 if ret:
1257 1250 killdaemons(env['DAEMON_PIDS'])
1258 1251
1259 1252 for s, r in self._getreplacements():
1260 1253 output = re.sub(s, r, output)
1261 1254
1262 1255 if normalizenewlines:
1263 1256 output = output.replace(b'\r\n', b'\n')
1264 1257
1265 1258 return ret, output.splitlines(True)
1266 1259
1267 1260 class PythonTest(Test):
1268 1261 """A Python-based test."""
1269 1262
1270 1263 @property
1271 1264 def refpath(self):
1272 1265 return os.path.join(self._testdir, b'%s.out' % self.bname)
1273 1266
1274 1267 def _run(self, env):
1275 1268 py3switch = self._py3warnings and b' -3' or b''
1276 1269 # Quote the python(3) executable for Windows
1277 1270 cmd = b'"%s"%s "%s"' % (PYTHON, py3switch, self.path)
1278 1271 vlog("# Running", cmd)
1279 1272 normalizenewlines = os.name == 'nt'
1280 1273 result = self._runcommand(cmd, env,
1281 1274 normalizenewlines=normalizenewlines)
1282 1275 if self._aborted:
1283 1276 raise KeyboardInterrupt()
1284 1277
1285 1278 return result
1286 1279
1287 1280 # Some glob patterns apply only in some circumstances, so the script
1288 1281 # might want to remove (glob) annotations that otherwise should be
1289 1282 # retained.
1290 1283 checkcodeglobpats = [
1291 1284 # On Windows it looks like \ doesn't require a (glob), but we know
1292 1285 # better.
1293 1286 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1294 1287 re.compile(br'^moving \S+/.*[^)]$'),
1295 1288 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1296 1289 # Not all platforms have 127.0.0.1 as loopback (though most do),
1297 1290 # so we always glob that too.
1298 1291 re.compile(br'.*\$LOCALIP.*$'),
1299 1292 ]
1300 1293
1301 1294 bchr = chr
1302 1295 if PYTHON3:
1303 1296 bchr = lambda x: bytes([x])
1304 1297
1305 1298 WARN_UNDEFINED = 1
1306 1299 WARN_YES = 2
1307 1300 WARN_NO = 3
1308 1301
1309 1302 MARK_OPTIONAL = b" (?)\n"
1310 1303
1311 1304 def isoptional(line):
1312 1305 return line.endswith(MARK_OPTIONAL)
1313 1306
1314 1307 class TTest(Test):
1315 1308 """A "t test" is a test backed by a .t file."""
1316 1309
1317 1310 SKIPPED_PREFIX = b'skipped: '
1318 1311 FAILED_PREFIX = b'hghave check failed: '
1319 1312 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1320 1313
1321 1314 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1322 1315 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1323 1316 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1324 1317
1325 1318 def __init__(self, path, *args, **kwds):
1326 1319 # accept an extra "case" parameter
1327 1320 case = kwds.pop('case', [])
1328 1321 self._case = case
1329 1322 self._allcases = {x for y in parsettestcases(path) for x in y}
1330 1323 super(TTest, self).__init__(path, *args, **kwds)
1331 1324 if case:
1332 1325 casepath = b'#'.join(case)
1333 1326 self.name = '%s#%s' % (self.name, _strpath(casepath))
1334 1327 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1335 1328 self._tmpname += b'-%s' % casepath
1336 1329 self._have = {}
1337 1330
1338 1331 @property
1339 1332 def refpath(self):
1340 1333 return os.path.join(self._testdir, self.bname)
1341 1334
1342 1335 def _run(self, env):
1343 1336 with open(self.path, 'rb') as f:
1344 1337 lines = f.readlines()
1345 1338
1346 1339 # .t file is both reference output and the test input, keep reference
1347 1340 # output updated with the the test input. This avoids some race
1348 1341 # conditions where the reference output does not match the actual test.
1349 1342 if self._refout is not None:
1350 1343 self._refout = lines
1351 1344
1352 1345 salt, script, after, expected = self._parsetest(lines)
1353 1346
1354 1347 # Write out the generated script.
1355 1348 fname = b'%s.sh' % self._testtmp
1356 1349 with open(fname, 'wb') as f:
1357 1350 for l in script:
1358 1351 f.write(l)
1359 1352
1360 1353 cmd = b'%s "%s"' % (self._shell, fname)
1361 1354 vlog("# Running", cmd)
1362 1355
1363 1356 exitcode, output = self._runcommand(cmd, env)
1364 1357
1365 1358 if self._aborted:
1366 1359 raise KeyboardInterrupt()
1367 1360
1368 1361 # Do not merge output if skipped. Return hghave message instead.
1369 1362 # Similarly, with --debug, output is None.
1370 1363 if exitcode == self.SKIPPED_STATUS or output is None:
1371 1364 return exitcode, output
1372 1365
1373 1366 return self._processoutput(exitcode, output, salt, after, expected)
1374 1367
1375 1368 def _hghave(self, reqs):
1376 1369 allreqs = b' '.join(reqs)
1377 1370
1378 1371 self._detectslow(reqs)
1379 1372
1380 1373 if allreqs in self._have:
1381 1374 return self._have.get(allreqs)
1382 1375
1383 1376 # TODO do something smarter when all other uses of hghave are gone.
1384 1377 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1385 1378 tdir = runtestdir.replace(b'\\', b'/')
1386 1379 proc = Popen4(b'%s -c "%s/hghave %s"' %
1387 1380 (self._shell, tdir, allreqs),
1388 1381 self._testtmp, 0, self._getenv())
1389 1382 stdout, stderr = proc.communicate()
1390 1383 ret = proc.wait()
1391 1384 if wifexited(ret):
1392 1385 ret = os.WEXITSTATUS(ret)
1393 1386 if ret == 2:
1394 1387 print(stdout.decode('utf-8'))
1395 1388 sys.exit(1)
1396 1389
1397 1390 if ret != 0:
1398 1391 self._have[allreqs] = (False, stdout)
1399 1392 return False, stdout
1400 1393
1401 1394 self._have[allreqs] = (True, None)
1402 1395 return True, None
1403 1396
1404 1397 def _detectslow(self, reqs):
1405 1398 """update the timeout of slow test when appropriate"""
1406 1399 if b'slow' in reqs:
1407 1400 self._timeout = self._slowtimeout
1408 1401
1409 1402 def _iftest(self, args):
1410 1403 # implements "#if"
1411 1404 reqs = []
1412 1405 for arg in args:
1413 1406 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1414 1407 if arg[3:] in self._case:
1415 1408 return False
1416 1409 elif arg in self._allcases:
1417 1410 if arg not in self._case:
1418 1411 return False
1419 1412 else:
1420 1413 reqs.append(arg)
1421 1414 self._detectslow(reqs)
1422 1415 return self._hghave(reqs)[0]
1423 1416
1424 1417 def _parsetest(self, lines):
1425 1418 # We generate a shell script which outputs unique markers to line
1426 1419 # up script results with our source. These markers include input
1427 1420 # line number and the last return code.
1428 1421 salt = b"SALT%d" % time.time()
1429 1422 def addsalt(line, inpython):
1430 1423 if inpython:
1431 1424 script.append(b'%s %d 0\n' % (salt, line))
1432 1425 else:
1433 1426 script.append(b'echo %s %d $?\n' % (salt, line))
1434 1427 activetrace = []
1435 1428 session = str(uuid.uuid4())
1436 1429 if PYTHON3:
1437 1430 session = session.encode('ascii')
1438 1431 hgcatapult = (os.getenv('HGTESTCATAPULTSERVERPIPE') or
1439 1432 os.getenv('HGCATAPULTSERVERPIPE'))
1440 1433 def toggletrace(cmd=None):
1441 1434 if not hgcatapult or hgcatapult == os.devnull:
1442 1435 return
1443 1436
1444 1437 if activetrace:
1445 1438 script.append(
1446 1439 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (
1447 1440 session, activetrace[0]))
1448 1441 if cmd is None:
1449 1442 return
1450 1443
1451 1444 if isinstance(cmd, str):
1452 1445 quoted = shellquote(cmd.strip())
1453 1446 else:
1454 1447 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1455 1448 quoted = quoted.replace(b'\\', b'\\\\')
1456 1449 script.append(
1457 1450 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' % (
1458 1451 session, quoted))
1459 1452 activetrace[0:] = [quoted]
1460 1453
1461 1454 script = []
1462 1455
1463 1456 # After we run the shell script, we re-unify the script output
1464 1457 # with non-active parts of the source, with synchronization by our
1465 1458 # SALT line number markers. The after table contains the non-active
1466 1459 # components, ordered by line number.
1467 1460 after = {}
1468 1461
1469 1462 # Expected shell script output.
1470 1463 expected = {}
1471 1464
1472 1465 pos = prepos = -1
1473 1466
1474 1467 # True or False when in a true or false conditional section
1475 1468 skipping = None
1476 1469
1477 1470 # We keep track of whether or not we're in a Python block so we
1478 1471 # can generate the surrounding doctest magic.
1479 1472 inpython = False
1480 1473
1481 1474 if self._debug:
1482 1475 script.append(b'set -x\n')
1483 1476 if self._hgcommand != b'hg':
1484 1477 script.append(b'alias hg="%s"\n' % self._hgcommand)
1485 1478 if os.getenv('MSYSTEM'):
1486 1479 script.append(b'alias pwd="pwd -W"\n')
1487 1480
1488 1481 if hgcatapult and hgcatapult != os.devnull:
1489 1482 if PYTHON3:
1490 1483 hgcatapult = hgcatapult.encode('utf8')
1491 1484 cataname = self.name.encode('utf8')
1492 1485 else:
1493 1486 cataname = self.name
1494 1487
1495 1488 # Kludge: use a while loop to keep the pipe from getting
1496 1489 # closed by our echo commands. The still-running file gets
1497 1490 # reaped at the end of the script, which causes the while
1498 1491 # loop to exit and closes the pipe. Sigh.
1499 1492 script.append(
1500 1493 b'rtendtracing() {\n'
1501 1494 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1502 1495 b' rm -f "$TESTTMP/.still-running"\n'
1503 1496 b'}\n'
1504 1497 b'trap "rtendtracing" 0\n'
1505 1498 b'touch "$TESTTMP/.still-running"\n'
1506 1499 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1507 1500 b'> %(catapult)s &\n'
1508 1501 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1509 1502 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1510 1503 % {
1511 1504 b'name': cataname,
1512 1505 b'session': session,
1513 1506 b'catapult': hgcatapult,
1514 1507 }
1515 1508 )
1516 1509
1517 1510 if self._case:
1518 1511 casestr = b'#'.join(self._case)
1519 1512 if isinstance(self._case, str):
1520 1513 quoted = shellquote(casestr)
1521 1514 else:
1522 1515 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1523 1516 script.append(b'TESTCASE=%s\n' % quoted)
1524 1517 script.append(b'export TESTCASE\n')
1525 1518
1526 1519 n = 0
1527 1520 for n, l in enumerate(lines):
1528 1521 if not l.endswith(b'\n'):
1529 1522 l += b'\n'
1530 1523 if l.startswith(b'#require'):
1531 1524 lsplit = l.split()
1532 1525 if len(lsplit) < 2 or lsplit[0] != b'#require':
1533 1526 after.setdefault(pos, []).append(' !!! invalid #require\n')
1534 1527 if not skipping:
1535 1528 haveresult, message = self._hghave(lsplit[1:])
1536 1529 if not haveresult:
1537 1530 script = [b'echo "%s"\nexit 80\n' % message]
1538 1531 break
1539 1532 after.setdefault(pos, []).append(l)
1540 1533 elif l.startswith(b'#if'):
1541 1534 lsplit = l.split()
1542 1535 if len(lsplit) < 2 or lsplit[0] != b'#if':
1543 1536 after.setdefault(pos, []).append(' !!! invalid #if\n')
1544 1537 if skipping is not None:
1545 1538 after.setdefault(pos, []).append(' !!! nested #if\n')
1546 1539 skipping = not self._iftest(lsplit[1:])
1547 1540 after.setdefault(pos, []).append(l)
1548 1541 elif l.startswith(b'#else'):
1549 1542 if skipping is None:
1550 1543 after.setdefault(pos, []).append(' !!! missing #if\n')
1551 1544 skipping = not skipping
1552 1545 after.setdefault(pos, []).append(l)
1553 1546 elif l.startswith(b'#endif'):
1554 1547 if skipping is None:
1555 1548 after.setdefault(pos, []).append(' !!! missing #if\n')
1556 1549 skipping = None
1557 1550 after.setdefault(pos, []).append(l)
1558 1551 elif skipping:
1559 1552 after.setdefault(pos, []).append(l)
1560 1553 elif l.startswith(b' >>> '): # python inlines
1561 1554 after.setdefault(pos, []).append(l)
1562 1555 prepos = pos
1563 1556 pos = n
1564 1557 if not inpython:
1565 1558 # We've just entered a Python block. Add the header.
1566 1559 inpython = True
1567 1560 addsalt(prepos, False) # Make sure we report the exit code.
1568 1561 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1569 1562 addsalt(n, True)
1570 1563 script.append(l[2:])
1571 1564 elif l.startswith(b' ... '): # python inlines
1572 1565 after.setdefault(prepos, []).append(l)
1573 1566 script.append(l[2:])
1574 1567 elif l.startswith(b' $ '): # commands
1575 1568 if inpython:
1576 1569 script.append(b'EOF\n')
1577 1570 inpython = False
1578 1571 after.setdefault(pos, []).append(l)
1579 1572 prepos = pos
1580 1573 pos = n
1581 1574 addsalt(n, False)
1582 1575 rawcmd = l[4:]
1583 1576 cmd = rawcmd.split()
1584 1577 toggletrace(rawcmd)
1585 1578 if len(cmd) == 2 and cmd[0] == b'cd':
1586 1579 l = b' $ cd %s || exit 1\n' % cmd[1]
1587 1580 script.append(rawcmd)
1588 1581 elif l.startswith(b' > '): # continuations
1589 1582 after.setdefault(prepos, []).append(l)
1590 1583 script.append(l[4:])
1591 1584 elif l.startswith(b' '): # results
1592 1585 # Queue up a list of expected results.
1593 1586 expected.setdefault(pos, []).append(l[2:])
1594 1587 else:
1595 1588 if inpython:
1596 1589 script.append(b'EOF\n')
1597 1590 inpython = False
1598 1591 # Non-command/result. Queue up for merged output.
1599 1592 after.setdefault(pos, []).append(l)
1600 1593
1601 1594 if inpython:
1602 1595 script.append(b'EOF\n')
1603 1596 if skipping is not None:
1604 1597 after.setdefault(pos, []).append(' !!! missing #endif\n')
1605 1598 addsalt(n + 1, False)
1606 1599 # Need to end any current per-command trace
1607 1600 if activetrace:
1608 1601 toggletrace()
1609 1602 return salt, script, after, expected
1610 1603
1611 1604 def _processoutput(self, exitcode, output, salt, after, expected):
1612 1605 # Merge the script output back into a unified test.
1613 1606 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
1614 1607 if exitcode != 0:
1615 1608 warnonly = WARN_NO
1616 1609
1617 1610 pos = -1
1618 1611 postout = []
1619 1612 for out_rawline in output:
1620 1613 out_line, cmd_line = out_rawline, None
1621 1614 if salt in out_rawline:
1622 1615 out_line, cmd_line = out_rawline.split(salt, 1)
1623 1616
1624 1617 pos, postout, warnonly = self._process_out_line(out_line,
1625 1618 pos,
1626 1619 postout,
1627 1620 expected,
1628 1621 warnonly)
1629 1622 pos, postout = self._process_cmd_line(cmd_line, pos, postout,
1630 1623 after)
1631 1624
1632 1625 if pos in after:
1633 1626 postout += after.pop(pos)
1634 1627
1635 1628 if warnonly == WARN_YES:
1636 1629 exitcode = False # Set exitcode to warned.
1637 1630
1638 1631 return exitcode, postout
1639 1632
1640 1633 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1641 1634 while out_line:
1642 1635 if not out_line.endswith(b'\n'):
1643 1636 out_line += b' (no-eol)\n'
1644 1637
1645 1638 # Find the expected output at the current position.
1646 1639 els = [None]
1647 1640 if expected.get(pos, None):
1648 1641 els = expected[pos]
1649 1642
1650 1643 optional = []
1651 1644 for i, el in enumerate(els):
1652 1645 r = False
1653 1646 if el:
1654 1647 r, exact = self.linematch(el, out_line)
1655 1648 if isinstance(r, str):
1656 1649 if r == '-glob':
1657 1650 out_line = ''.join(el.rsplit(' (glob)', 1))
1658 1651 r = '' # Warn only this line.
1659 1652 elif r == "retry":
1660 1653 postout.append(b' ' + el)
1661 1654 else:
1662 1655 log('\ninfo, unknown linematch result: %r\n' % r)
1663 1656 r = False
1664 1657 if r:
1665 1658 els.pop(i)
1666 1659 break
1667 1660 if el:
1668 1661 if isoptional(el):
1669 1662 optional.append(i)
1670 1663 else:
1671 1664 m = optline.match(el)
1672 1665 if m:
1673 1666 conditions = [
1674 1667 c for c in m.group(2).split(b' ')]
1675 1668
1676 1669 if not self._iftest(conditions):
1677 1670 optional.append(i)
1678 1671 if exact:
1679 1672 # Don't allow line to be matches against a later
1680 1673 # line in the output
1681 1674 els.pop(i)
1682 1675 break
1683 1676
1684 1677 if r:
1685 1678 if r == "retry":
1686 1679 continue
1687 1680 # clean up any optional leftovers
1688 1681 for i in optional:
1689 1682 postout.append(b' ' + els[i])
1690 1683 for i in reversed(optional):
1691 1684 del els[i]
1692 1685 postout.append(b' ' + el)
1693 1686 else:
1694 1687 if self.NEEDESCAPE(out_line):
1695 1688 out_line = TTest._stringescape(b'%s (esc)\n' %
1696 1689 out_line.rstrip(b'\n'))
1697 1690 postout.append(b' ' + out_line) # Let diff deal with it.
1698 1691 if r != '': # If line failed.
1699 1692 warnonly = WARN_NO
1700 1693 elif warnonly == WARN_UNDEFINED:
1701 1694 warnonly = WARN_YES
1702 1695 break
1703 1696 else:
1704 1697 # clean up any optional leftovers
1705 1698 while expected.get(pos, None):
1706 1699 el = expected[pos].pop(0)
1707 1700 if el:
1708 1701 if not isoptional(el):
1709 1702 m = optline.match(el)
1710 1703 if m:
1711 1704 conditions = [c for c in m.group(2).split(b' ')]
1712 1705
1713 1706 if self._iftest(conditions):
1714 1707 # Don't append as optional line
1715 1708 continue
1716 1709 else:
1717 1710 continue
1718 1711 postout.append(b' ' + el)
1719 1712 return pos, postout, warnonly
1720 1713
1721 1714 def _process_cmd_line(self, cmd_line, pos, postout, after):
1722 1715 """process a "command" part of a line from unified test output"""
1723 1716 if cmd_line:
1724 1717 # Add on last return code.
1725 1718 ret = int(cmd_line.split()[1])
1726 1719 if ret != 0:
1727 1720 postout.append(b' [%d]\n' % ret)
1728 1721 if pos in after:
1729 1722 # Merge in non-active test bits.
1730 1723 postout += after.pop(pos)
1731 1724 pos = int(cmd_line.split()[0])
1732 1725 return pos, postout
1733 1726
1734 1727 @staticmethod
1735 1728 def rematch(el, l):
1736 1729 try:
1737 1730 el = b'(?:' + el + b')'
1738 1731 # use \Z to ensure that the regex matches to the end of the string
1739 1732 if os.name == 'nt':
1740 1733 return re.match(el + br'\r?\n\Z', l)
1741 1734 return re.match(el + br'\n\Z', l)
1742 1735 except re.error:
1743 1736 # el is an invalid regex
1744 1737 return False
1745 1738
1746 1739 @staticmethod
1747 1740 def globmatch(el, l):
1748 1741 # The only supported special characters are * and ? plus / which also
1749 1742 # matches \ on windows. Escaping of these characters is supported.
1750 1743 if el + b'\n' == l:
1751 1744 if os.altsep:
1752 1745 # matching on "/" is not needed for this line
1753 1746 for pat in checkcodeglobpats:
1754 1747 if pat.match(el):
1755 1748 return True
1756 1749 return b'-glob'
1757 1750 return True
1758 1751 el = el.replace(b'$LOCALIP', b'*')
1759 1752 i, n = 0, len(el)
1760 1753 res = b''
1761 1754 while i < n:
1762 1755 c = el[i:i + 1]
1763 1756 i += 1
1764 1757 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1765 1758 res += el[i - 1:i + 1]
1766 1759 i += 1
1767 1760 elif c == b'*':
1768 1761 res += b'.*'
1769 1762 elif c == b'?':
1770 1763 res += b'.'
1771 1764 elif c == b'/' and os.altsep:
1772 1765 res += b'[/\\\\]'
1773 1766 else:
1774 1767 res += re.escape(c)
1775 1768 return TTest.rematch(res, l)
1776 1769
1777 1770 def linematch(self, el, l):
1778 1771 if el == l: # perfect match (fast)
1779 1772 return True, True
1780 1773 retry = False
1781 1774 if isoptional(el):
1782 1775 retry = "retry"
1783 1776 el = el[:-len(MARK_OPTIONAL)] + b"\n"
1784 1777 else:
1785 1778 m = optline.match(el)
1786 1779 if m:
1787 1780 conditions = [c for c in m.group(2).split(b' ')]
1788 1781
1789 1782 el = m.group(1) + b"\n"
1790 1783 if not self._iftest(conditions):
1791 1784 # listed feature missing, should not match
1792 1785 return "retry", False
1793 1786
1794 1787 if el.endswith(b" (esc)\n"):
1795 1788 if PYTHON3:
1796 1789 el = el[:-7].decode('unicode_escape') + '\n'
1797 1790 el = el.encode('utf-8')
1798 1791 else:
1799 1792 el = el[:-7].decode('string-escape') + '\n'
1800 1793 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1801 1794 return True, True
1802 1795 if el.endswith(b" (re)\n"):
1803 1796 return (TTest.rematch(el[:-6], l) or retry), False
1804 1797 if el.endswith(b" (glob)\n"):
1805 1798 # ignore '(glob)' added to l by 'replacements'
1806 1799 if l.endswith(b" (glob)\n"):
1807 1800 l = l[:-8] + b"\n"
1808 1801 return (TTest.globmatch(el[:-8], l) or retry), False
1809 1802 if os.altsep:
1810 1803 _l = l.replace(b'\\', b'/')
1811 1804 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1812 1805 return True, True
1813 1806 return retry, True
1814 1807
1815 1808 @staticmethod
1816 1809 def parsehghaveoutput(lines):
1817 1810 '''Parse hghave log lines.
1818 1811
1819 1812 Return tuple of lists (missing, failed):
1820 1813 * the missing/unknown features
1821 1814 * the features for which existence check failed'''
1822 1815 missing = []
1823 1816 failed = []
1824 1817 for line in lines:
1825 1818 if line.startswith(TTest.SKIPPED_PREFIX):
1826 1819 line = line.splitlines()[0]
1827 1820 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1828 1821 elif line.startswith(TTest.FAILED_PREFIX):
1829 1822 line = line.splitlines()[0]
1830 1823 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1831 1824
1832 1825 return missing, failed
1833 1826
1834 1827 @staticmethod
1835 1828 def _escapef(m):
1836 1829 return TTest.ESCAPEMAP[m.group(0)]
1837 1830
1838 1831 @staticmethod
1839 1832 def _stringescape(s):
1840 1833 return TTest.ESCAPESUB(TTest._escapef, s)
1841 1834
1842 1835 iolock = threading.RLock()
1843 1836 firstlock = threading.RLock()
1844 1837 firsterror = False
1845 1838
1846 1839 class TestResult(unittest._TextTestResult):
1847 1840 """Holds results when executing via unittest."""
1848 1841 # Don't worry too much about accessing the non-public _TextTestResult.
1849 1842 # It is relatively common in Python testing tools.
1850 1843 def __init__(self, options, *args, **kwargs):
1851 1844 super(TestResult, self).__init__(*args, **kwargs)
1852 1845
1853 1846 self._options = options
1854 1847
1855 1848 # unittest.TestResult didn't have skipped until 2.7. We need to
1856 1849 # polyfill it.
1857 1850 self.skipped = []
1858 1851
1859 1852 # We have a custom "ignored" result that isn't present in any Python
1860 1853 # unittest implementation. It is very similar to skipped. It may make
1861 1854 # sense to map it into skip some day.
1862 1855 self.ignored = []
1863 1856
1864 1857 self.times = []
1865 1858 self._firststarttime = None
1866 1859 # Data stored for the benefit of generating xunit reports.
1867 1860 self.successes = []
1868 1861 self.faildata = {}
1869 1862
1870 1863 if options.color == 'auto':
1871 1864 self.color = pygmentspresent and self.stream.isatty()
1872 1865 elif options.color == 'never':
1873 1866 self.color = False
1874 1867 else: # 'always', for testing purposes
1875 1868 self.color = pygmentspresent
1876 1869
1877 1870 def onStart(self, test):
1878 1871 """ Can be overriden by custom TestResult
1879 1872 """
1880 1873
1881 1874 def onEnd(self):
1882 1875 """ Can be overriden by custom TestResult
1883 1876 """
1884 1877
1885 1878 def addFailure(self, test, reason):
1886 1879 self.failures.append((test, reason))
1887 1880
1888 1881 if self._options.first:
1889 1882 self.stop()
1890 1883 else:
1891 1884 with iolock:
1892 1885 if reason == "timed out":
1893 1886 self.stream.write('t')
1894 1887 else:
1895 1888 if not self._options.nodiff:
1896 1889 self.stream.write('\n')
1897 1890 # Exclude the '\n' from highlighting to lex correctly
1898 1891 formatted = 'ERROR: %s output changed\n' % test
1899 1892 self.stream.write(highlightmsg(formatted, self.color))
1900 1893 self.stream.write('!')
1901 1894
1902 1895 self.stream.flush()
1903 1896
1904 1897 def addSuccess(self, test):
1905 1898 with iolock:
1906 1899 super(TestResult, self).addSuccess(test)
1907 1900 self.successes.append(test)
1908 1901
1909 1902 def addError(self, test, err):
1910 1903 super(TestResult, self).addError(test, err)
1911 1904 if self._options.first:
1912 1905 self.stop()
1913 1906
1914 1907 # Polyfill.
1915 1908 def addSkip(self, test, reason):
1916 1909 self.skipped.append((test, reason))
1917 1910 with iolock:
1918 1911 if self.showAll:
1919 1912 self.stream.writeln('skipped %s' % reason)
1920 1913 else:
1921 1914 self.stream.write('s')
1922 1915 self.stream.flush()
1923 1916
1924 1917 def addIgnore(self, test, reason):
1925 1918 self.ignored.append((test, reason))
1926 1919 with iolock:
1927 1920 if self.showAll:
1928 1921 self.stream.writeln('ignored %s' % reason)
1929 1922 else:
1930 1923 if reason not in ('not retesting', "doesn't match keyword"):
1931 1924 self.stream.write('i')
1932 1925 else:
1933 1926 self.testsRun += 1
1934 1927 self.stream.flush()
1935 1928
1936 1929 def addOutputMismatch(self, test, ret, got, expected):
1937 1930 """Record a mismatch in test output for a particular test."""
1938 1931 if self.shouldStop or firsterror:
1939 1932 # don't print, some other test case already failed and
1940 1933 # printed, we're just stale and probably failed due to our
1941 1934 # temp dir getting cleaned up.
1942 1935 return
1943 1936
1944 1937 accepted = False
1945 1938 lines = []
1946 1939
1947 1940 with iolock:
1948 1941 if self._options.nodiff:
1949 1942 pass
1950 1943 elif self._options.view:
1951 1944 v = self._options.view
1952 1945 subprocess.call(r'"%s" "%s" "%s"' %
1953 1946 (v, _strpath(test.refpath),
1954 1947 _strpath(test.errpath)), shell=True)
1955 1948 else:
1956 1949 servefail, lines = getdiff(expected, got,
1957 1950 test.refpath, test.errpath)
1958 1951 self.stream.write('\n')
1959 1952 for line in lines:
1960 1953 line = highlightdiff(line, self.color)
1961 1954 if PYTHON3:
1962 1955 self.stream.flush()
1963 1956 self.stream.buffer.write(line)
1964 1957 self.stream.buffer.flush()
1965 1958 else:
1966 1959 self.stream.write(line)
1967 1960 self.stream.flush()
1968 1961
1969 1962 if servefail:
1970 1963 raise test.failureException(
1971 1964 'server failed to start (HGPORT=%s)' % test._startport)
1972 1965
1973 1966 # handle interactive prompt without releasing iolock
1974 1967 if self._options.interactive:
1975 1968 if test.readrefout() != expected:
1976 1969 self.stream.write(
1977 1970 'Reference output has changed (run again to prompt '
1978 1971 'changes)')
1979 1972 else:
1980 1973 self.stream.write('Accept this change? [n] ')
1981 1974 self.stream.flush()
1982 1975 answer = sys.stdin.readline().strip()
1983 1976 if answer.lower() in ('y', 'yes'):
1984 1977 if test.path.endswith(b'.t'):
1985 1978 rename(test.errpath, test.path)
1986 1979 else:
1987 1980 rename(test.errpath, '%s.out' % test.path)
1988 1981 accepted = True
1989 1982 if not accepted:
1990 1983 self.faildata[test.name] = b''.join(lines)
1991 1984
1992 1985 return accepted
1993 1986
1994 1987 def startTest(self, test):
1995 1988 super(TestResult, self).startTest(test)
1996 1989
1997 1990 # os.times module computes the user time and system time spent by
1998 1991 # child's processes along with real elapsed time taken by a process.
1999 1992 # This module has one limitation. It can only work for Linux user
2000 1993 # and not for Windows.
2001 1994 test.started = os.times()
2002 1995 if self._firststarttime is None: # thread racy but irrelevant
2003 1996 self._firststarttime = test.started[4]
2004 1997
2005 1998 def stopTest(self, test, interrupted=False):
2006 1999 super(TestResult, self).stopTest(test)
2007 2000
2008 2001 test.stopped = os.times()
2009 2002
2010 2003 starttime = test.started
2011 2004 endtime = test.stopped
2012 2005 origin = self._firststarttime
2013 2006 self.times.append((test.name,
2014 2007 endtime[2] - starttime[2], # user space CPU time
2015 2008 endtime[3] - starttime[3], # sys space CPU time
2016 2009 endtime[4] - starttime[4], # real time
2017 2010 starttime[4] - origin, # start date in run context
2018 2011 endtime[4] - origin, # end date in run context
2019 2012 ))
2020 2013
2021 2014 if interrupted:
2022 2015 with iolock:
2023 2016 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
2024 2017 test.name, self.times[-1][3]))
2025 2018
2026 2019 def getTestResult():
2027 2020 """
2028 2021 Returns the relevant test result
2029 2022 """
2030 2023 if "CUSTOM_TEST_RESULT" in os.environ:
2031 2024 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2032 2025 return testresultmodule.TestResult
2033 2026 else:
2034 2027 return TestResult
2035 2028
2036 2029 class TestSuite(unittest.TestSuite):
2037 2030 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2038 2031
2039 2032 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
2040 2033 retest=False, keywords=None, loop=False, runs_per_test=1,
2041 2034 loadtest=None, showchannels=False,
2042 2035 *args, **kwargs):
2043 2036 """Create a new instance that can run tests with a configuration.
2044 2037
2045 2038 testdir specifies the directory where tests are executed from. This
2046 2039 is typically the ``tests`` directory from Mercurial's source
2047 2040 repository.
2048 2041
2049 2042 jobs specifies the number of jobs to run concurrently. Each test
2050 2043 executes on its own thread. Tests actually spawn new processes, so
2051 2044 state mutation should not be an issue.
2052 2045
2053 2046 If there is only one job, it will use the main thread.
2054 2047
2055 2048 whitelist and blacklist denote tests that have been whitelisted and
2056 2049 blacklisted, respectively. These arguments don't belong in TestSuite.
2057 2050 Instead, whitelist and blacklist should be handled by the thing that
2058 2051 populates the TestSuite with tests. They are present to preserve
2059 2052 backwards compatible behavior which reports skipped tests as part
2060 2053 of the results.
2061 2054
2062 2055 retest denotes whether to retest failed tests. This arguably belongs
2063 2056 outside of TestSuite.
2064 2057
2065 2058 keywords denotes key words that will be used to filter which tests
2066 2059 to execute. This arguably belongs outside of TestSuite.
2067 2060
2068 2061 loop denotes whether to loop over tests forever.
2069 2062 """
2070 2063 super(TestSuite, self).__init__(*args, **kwargs)
2071 2064
2072 2065 self._jobs = jobs
2073 2066 self._whitelist = whitelist
2074 2067 self._blacklist = blacklist
2075 2068 self._retest = retest
2076 2069 self._keywords = keywords
2077 2070 self._loop = loop
2078 2071 self._runs_per_test = runs_per_test
2079 2072 self._loadtest = loadtest
2080 2073 self._showchannels = showchannels
2081 2074
2082 2075 def run(self, result):
2083 2076 # We have a number of filters that need to be applied. We do this
2084 2077 # here instead of inside Test because it makes the running logic for
2085 2078 # Test simpler.
2086 2079 tests = []
2087 2080 num_tests = [0]
2088 2081 for test in self._tests:
2089 2082 def get():
2090 2083 num_tests[0] += 1
2091 2084 if getattr(test, 'should_reload', False):
2092 2085 return self._loadtest(test, num_tests[0])
2093 2086 return test
2094 2087 if not os.path.exists(test.path):
2095 2088 result.addSkip(test, "Doesn't exist")
2096 2089 continue
2097 2090
2098 2091 if not (self._whitelist and test.bname in self._whitelist):
2099 2092 if self._blacklist and test.bname in self._blacklist:
2100 2093 result.addSkip(test, 'blacklisted')
2101 2094 continue
2102 2095
2103 2096 if self._retest and not os.path.exists(test.errpath):
2104 2097 result.addIgnore(test, 'not retesting')
2105 2098 continue
2106 2099
2107 2100 if self._keywords:
2108 2101 with open(test.path, 'rb') as f:
2109 2102 t = f.read().lower() + test.bname.lower()
2110 2103 ignored = False
2111 2104 for k in self._keywords.lower().split():
2112 2105 if k not in t:
2113 2106 result.addIgnore(test, "doesn't match keyword")
2114 2107 ignored = True
2115 2108 break
2116 2109
2117 2110 if ignored:
2118 2111 continue
2119 2112 for _ in xrange(self._runs_per_test):
2120 2113 tests.append(get())
2121 2114
2122 2115 runtests = list(tests)
2123 2116 done = queue.Queue()
2124 2117 running = 0
2125 2118
2126 2119 channels = [""] * self._jobs
2127 2120
2128 2121 def job(test, result):
2129 2122 for n, v in enumerate(channels):
2130 2123 if not v:
2131 2124 channel = n
2132 2125 break
2133 2126 else:
2134 2127 raise ValueError('Could not find output channel')
2135 2128 channels[channel] = "=" + test.name[5:].split(".")[0]
2136 2129 try:
2137 2130 test(result)
2138 2131 done.put(None)
2139 2132 except KeyboardInterrupt:
2140 2133 pass
2141 2134 except: # re-raises
2142 2135 done.put(('!', test, 'run-test raised an error, see traceback'))
2143 2136 raise
2144 2137 finally:
2145 2138 try:
2146 2139 channels[channel] = ''
2147 2140 except IndexError:
2148 2141 pass
2149 2142
2150 2143 def stat():
2151 2144 count = 0
2152 2145 while channels:
2153 2146 d = '\n%03s ' % count
2154 2147 for n, v in enumerate(channels):
2155 2148 if v:
2156 2149 d += v[0]
2157 2150 channels[n] = v[1:] or '.'
2158 2151 else:
2159 2152 d += ' '
2160 2153 d += ' '
2161 2154 with iolock:
2162 2155 sys.stdout.write(d + ' ')
2163 2156 sys.stdout.flush()
2164 2157 for x in xrange(10):
2165 2158 if channels:
2166 2159 time.sleep(.1)
2167 2160 count += 1
2168 2161
2169 2162 stoppedearly = False
2170 2163
2171 2164 if self._showchannels:
2172 2165 statthread = threading.Thread(target=stat, name="stat")
2173 2166 statthread.start()
2174 2167
2175 2168 try:
2176 2169 while tests or running:
2177 2170 if not done.empty() or running == self._jobs or not tests:
2178 2171 try:
2179 2172 done.get(True, 1)
2180 2173 running -= 1
2181 2174 if result and result.shouldStop:
2182 2175 stoppedearly = True
2183 2176 break
2184 2177 except queue.Empty:
2185 2178 continue
2186 2179 if tests and not running == self._jobs:
2187 2180 test = tests.pop(0)
2188 2181 if self._loop:
2189 2182 if getattr(test, 'should_reload', False):
2190 2183 num_tests[0] += 1
2191 2184 tests.append(
2192 2185 self._loadtest(test, num_tests[0]))
2193 2186 else:
2194 2187 tests.append(test)
2195 2188 if self._jobs == 1:
2196 2189 job(test, result)
2197 2190 else:
2198 2191 t = threading.Thread(target=job, name=test.name,
2199 2192 args=(test, result))
2200 2193 t.start()
2201 2194 running += 1
2202 2195
2203 2196 # If we stop early we still need to wait on started tests to
2204 2197 # finish. Otherwise, there is a race between the test completing
2205 2198 # and the test's cleanup code running. This could result in the
2206 2199 # test reporting incorrect.
2207 2200 if stoppedearly:
2208 2201 while running:
2209 2202 try:
2210 2203 done.get(True, 1)
2211 2204 running -= 1
2212 2205 except queue.Empty:
2213 2206 continue
2214 2207 except KeyboardInterrupt:
2215 2208 for test in runtests:
2216 2209 test.abort()
2217 2210
2218 2211 channels = []
2219 2212
2220 2213 return result
2221 2214
2222 2215 # Save the most recent 5 wall-clock runtimes of each test to a
2223 2216 # human-readable text file named .testtimes. Tests are sorted
2224 2217 # alphabetically, while times for each test are listed from oldest to
2225 2218 # newest.
2226 2219
2227 2220 def loadtimes(outputdir):
2228 2221 times = []
2229 2222 try:
2230 2223 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2231 2224 for line in fp:
2232 2225 m = re.match('(.*?) ([0-9. ]+)', line)
2233 2226 times.append((m.group(1),
2234 2227 [float(t) for t in m.group(2).split()]))
2235 2228 except IOError as err:
2236 2229 if err.errno != errno.ENOENT:
2237 2230 raise
2238 2231 return times
2239 2232
2240 2233 def savetimes(outputdir, result):
2241 2234 saved = dict(loadtimes(outputdir))
2242 2235 maxruns = 5
2243 2236 skipped = set([str(t[0]) for t in result.skipped])
2244 2237 for tdata in result.times:
2245 2238 test, real = tdata[0], tdata[3]
2246 2239 if test not in skipped:
2247 2240 ts = saved.setdefault(test, [])
2248 2241 ts.append(real)
2249 2242 ts[:] = ts[-maxruns:]
2250 2243
2251 2244 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2252 2245 dir=outputdir, text=True)
2253 2246 with os.fdopen(fd, 'w') as fp:
2254 2247 for name, ts in sorted(saved.items()):
2255 2248 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2256 2249 timepath = os.path.join(outputdir, b'.testtimes')
2257 2250 try:
2258 2251 os.unlink(timepath)
2259 2252 except OSError:
2260 2253 pass
2261 2254 try:
2262 2255 os.rename(tmpname, timepath)
2263 2256 except OSError:
2264 2257 pass
2265 2258
2266 2259 class TextTestRunner(unittest.TextTestRunner):
2267 2260 """Custom unittest test runner that uses appropriate settings."""
2268 2261
2269 2262 def __init__(self, runner, *args, **kwargs):
2270 2263 super(TextTestRunner, self).__init__(*args, **kwargs)
2271 2264
2272 2265 self._runner = runner
2273 2266
2274 2267 self._result = getTestResult()(self._runner.options, self.stream,
2275 2268 self.descriptions, self.verbosity)
2276 2269
2277 2270 def listtests(self, test):
2278 2271 test = sorted(test, key=lambda t: t.name)
2279 2272
2280 2273 self._result.onStart(test)
2281 2274
2282 2275 for t in test:
2283 2276 print(t.name)
2284 2277 self._result.addSuccess(t)
2285 2278
2286 2279 if self._runner.options.xunit:
2287 2280 with open(self._runner.options.xunit, "wb") as xuf:
2288 2281 self._writexunit(self._result, xuf)
2289 2282
2290 2283 if self._runner.options.json:
2291 2284 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2292 2285 with open(jsonpath, 'w') as fp:
2293 2286 self._writejson(self._result, fp)
2294 2287
2295 2288 return self._result
2296 2289
2297 2290 def run(self, test):
2298 2291 self._result.onStart(test)
2299 2292 test(self._result)
2300 2293
2301 2294 failed = len(self._result.failures)
2302 2295 skipped = len(self._result.skipped)
2303 2296 ignored = len(self._result.ignored)
2304 2297
2305 2298 with iolock:
2306 2299 self.stream.writeln('')
2307 2300
2308 2301 if not self._runner.options.noskips:
2309 2302 for test, msg in sorted(self._result.skipped,
2310 2303 key=lambda s: s[0].name):
2311 2304 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2312 2305 msg = highlightmsg(formatted, self._result.color)
2313 2306 self.stream.write(msg)
2314 2307 for test, msg in sorted(self._result.failures,
2315 2308 key=lambda f: f[0].name):
2316 2309 formatted = 'Failed %s: %s\n' % (test.name, msg)
2317 2310 self.stream.write(highlightmsg(formatted, self._result.color))
2318 2311 for test, msg in sorted(self._result.errors,
2319 2312 key=lambda e: e[0].name):
2320 2313 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2321 2314
2322 2315 if self._runner.options.xunit:
2323 2316 with open(self._runner.options.xunit, "wb") as xuf:
2324 2317 self._writexunit(self._result, xuf)
2325 2318
2326 2319 if self._runner.options.json:
2327 2320 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2328 2321 with open(jsonpath, 'w') as fp:
2329 2322 self._writejson(self._result, fp)
2330 2323
2331 2324 self._runner._checkhglib('Tested')
2332 2325
2333 2326 savetimes(self._runner._outputdir, self._result)
2334 2327
2335 2328 if failed and self._runner.options.known_good_rev:
2336 2329 self._bisecttests(t for t, m in self._result.failures)
2337 2330 self.stream.writeln(
2338 2331 '# Ran %d tests, %d skipped, %d failed.'
2339 2332 % (self._result.testsRun, skipped + ignored, failed))
2340 2333 if failed:
2341 2334 self.stream.writeln('python hash seed: %s' %
2342 2335 os.environ['PYTHONHASHSEED'])
2343 2336 if self._runner.options.time:
2344 2337 self.printtimes(self._result.times)
2345 2338
2346 2339 if self._runner.options.exceptions:
2347 2340 exceptions = aggregateexceptions(
2348 2341 os.path.join(self._runner._outputdir, b'exceptions'))
2349 2342
2350 2343 self.stream.writeln('Exceptions Report:')
2351 2344 self.stream.writeln('%d total from %d frames' %
2352 2345 (exceptions['total'],
2353 2346 len(exceptions['exceptioncounts'])))
2354 2347 combined = exceptions['combined']
2355 2348 for key in sorted(combined, key=combined.get, reverse=True):
2356 2349 frame, line, exc = key
2357 2350 totalcount, testcount, leastcount, leasttest = combined[key]
2358 2351
2359 2352 self.stream.writeln('%d (%d tests)\t%s: %s (%s - %d total)'
2360 2353 % (totalcount,
2361 2354 testcount,
2362 2355 frame, exc,
2363 2356 leasttest, leastcount))
2364 2357
2365 2358 self.stream.flush()
2366 2359
2367 2360 return self._result
2368 2361
2369 2362 def _bisecttests(self, tests):
2370 2363 bisectcmd = ['hg', 'bisect']
2371 2364 bisectrepo = self._runner.options.bisect_repo
2372 2365 if bisectrepo:
2373 2366 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2374 2367 def pread(args):
2375 2368 env = os.environ.copy()
2376 2369 env['HGPLAIN'] = '1'
2377 2370 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2378 2371 stdout=subprocess.PIPE, env=env)
2379 2372 data = p.stdout.read()
2380 2373 p.wait()
2381 2374 return data
2382 2375 for test in tests:
2383 2376 pread(bisectcmd + ['--reset']),
2384 2377 pread(bisectcmd + ['--bad', '.'])
2385 2378 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2386 2379 # TODO: we probably need to forward more options
2387 2380 # that alter hg's behavior inside the tests.
2388 2381 opts = ''
2389 2382 withhg = self._runner.options.with_hg
2390 2383 if withhg:
2391 2384 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2392 2385 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts,
2393 2386 test)
2394 2387 data = pread(bisectcmd + ['--command', rtc])
2395 2388 m = re.search(
2396 2389 (br'\nThe first (?P<goodbad>bad|good) revision '
2397 2390 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2398 2391 br'summary: +(?P<summary>[^\n]+)\n'),
2399 2392 data, (re.MULTILINE | re.DOTALL))
2400 2393 if m is None:
2401 2394 self.stream.writeln(
2402 2395 'Failed to identify failure point for %s' % test)
2403 2396 continue
2404 2397 dat = m.groupdict()
2405 2398 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2406 2399 self.stream.writeln(
2407 2400 '%s %s by %s (%s)' % (
2408 2401 test, verb, dat['node'].decode('ascii'),
2409 2402 dat['summary'].decode('utf8', 'ignore')))
2410 2403
2411 2404 def printtimes(self, times):
2412 2405 # iolock held by run
2413 2406 self.stream.writeln('# Producing time report')
2414 2407 times.sort(key=lambda t: (t[3]))
2415 2408 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2416 2409 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2417 2410 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2418 2411 for tdata in times:
2419 2412 test = tdata[0]
2420 2413 cuser, csys, real, start, end = tdata[1:6]
2421 2414 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2422 2415
2423 2416 @staticmethod
2424 2417 def _writexunit(result, outf):
2425 2418 # See http://llg.cubic.org/docs/junit/ for a reference.
2426 2419 timesd = dict((t[0], t[3]) for t in result.times)
2427 2420 doc = minidom.Document()
2428 2421 s = doc.createElement('testsuite')
2429 2422 s.setAttribute('errors', "0") # TODO
2430 2423 s.setAttribute('failures', str(len(result.failures)))
2431 2424 s.setAttribute('name', 'run-tests')
2432 2425 s.setAttribute('skipped', str(len(result.skipped) +
2433 2426 len(result.ignored)))
2434 2427 s.setAttribute('tests', str(result.testsRun))
2435 2428 doc.appendChild(s)
2436 2429 for tc in result.successes:
2437 2430 t = doc.createElement('testcase')
2438 2431 t.setAttribute('name', tc.name)
2439 2432 tctime = timesd.get(tc.name)
2440 2433 if tctime is not None:
2441 2434 t.setAttribute('time', '%.3f' % tctime)
2442 2435 s.appendChild(t)
2443 2436 for tc, err in sorted(result.faildata.items()):
2444 2437 t = doc.createElement('testcase')
2445 2438 t.setAttribute('name', tc)
2446 2439 tctime = timesd.get(tc)
2447 2440 if tctime is not None:
2448 2441 t.setAttribute('time', '%.3f' % tctime)
2449 2442 # createCDATASection expects a unicode or it will
2450 2443 # convert using default conversion rules, which will
2451 2444 # fail if string isn't ASCII.
2452 2445 err = cdatasafe(err).decode('utf-8', 'replace')
2453 2446 cd = doc.createCDATASection(err)
2454 2447 # Use 'failure' here instead of 'error' to match errors = 0,
2455 2448 # failures = len(result.failures) in the testsuite element.
2456 2449 failelem = doc.createElement('failure')
2457 2450 failelem.setAttribute('message', 'output changed')
2458 2451 failelem.setAttribute('type', 'output-mismatch')
2459 2452 failelem.appendChild(cd)
2460 2453 t.appendChild(failelem)
2461 2454 s.appendChild(t)
2462 2455 for tc, message in result.skipped:
2463 2456 # According to the schema, 'skipped' has no attributes. So store
2464 2457 # the skip message as a text node instead.
2465 2458 t = doc.createElement('testcase')
2466 2459 t.setAttribute('name', tc.name)
2467 2460 binmessage = message.encode('utf-8')
2468 2461 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2469 2462 cd = doc.createCDATASection(message)
2470 2463 skipelem = doc.createElement('skipped')
2471 2464 skipelem.appendChild(cd)
2472 2465 t.appendChild(skipelem)
2473 2466 s.appendChild(t)
2474 2467 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2475 2468
2476 2469 @staticmethod
2477 2470 def _writejson(result, outf):
2478 2471 timesd = {}
2479 2472 for tdata in result.times:
2480 2473 test = tdata[0]
2481 2474 timesd[test] = tdata[1:]
2482 2475
2483 2476 outcome = {}
2484 2477 groups = [('success', ((tc, None)
2485 2478 for tc in result.successes)),
2486 2479 ('failure', result.failures),
2487 2480 ('skip', result.skipped)]
2488 2481 for res, testcases in groups:
2489 2482 for tc, __ in testcases:
2490 2483 if tc.name in timesd:
2491 2484 diff = result.faildata.get(tc.name, b'')
2492 2485 try:
2493 2486 diff = diff.decode('unicode_escape')
2494 2487 except UnicodeDecodeError as e:
2495 2488 diff = '%r decoding diff, sorry' % e
2496 2489 tres = {'result': res,
2497 2490 'time': ('%0.3f' % timesd[tc.name][2]),
2498 2491 'cuser': ('%0.3f' % timesd[tc.name][0]),
2499 2492 'csys': ('%0.3f' % timesd[tc.name][1]),
2500 2493 'start': ('%0.3f' % timesd[tc.name][3]),
2501 2494 'end': ('%0.3f' % timesd[tc.name][4]),
2502 2495 'diff': diff,
2503 2496 }
2504 2497 else:
2505 2498 # blacklisted test
2506 2499 tres = {'result': res}
2507 2500
2508 2501 outcome[tc.name] = tres
2509 2502 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2510 2503 separators=(',', ': '))
2511 2504 outf.writelines(("testreport =", jsonout))
2512 2505
2513 2506 def sorttests(testdescs, previoustimes, shuffle=False):
2514 2507 """Do an in-place sort of tests."""
2515 2508 if shuffle:
2516 2509 random.shuffle(testdescs)
2517 2510 return
2518 2511
2519 2512 if previoustimes:
2520 2513 def sortkey(f):
2521 2514 f = f['path']
2522 2515 if f in previoustimes:
2523 2516 # Use most recent time as estimate
2524 2517 return -previoustimes[f][-1]
2525 2518 else:
2526 2519 # Default to a rather arbitrary value of 1 second for new tests
2527 2520 return -1.0
2528 2521 else:
2529 2522 # keywords for slow tests
2530 2523 slow = {b'svn': 10,
2531 2524 b'cvs': 10,
2532 2525 b'hghave': 10,
2533 2526 b'largefiles-update': 10,
2534 2527 b'run-tests': 10,
2535 2528 b'corruption': 10,
2536 2529 b'race': 10,
2537 2530 b'i18n': 10,
2538 2531 b'check': 100,
2539 2532 b'gendoc': 100,
2540 2533 b'contrib-perf': 200,
2541 2534 b'merge-combination': 100,
2542 2535 }
2543 2536 perf = {}
2544 2537
2545 2538 def sortkey(f):
2546 2539 # run largest tests first, as they tend to take the longest
2547 2540 f = f['path']
2548 2541 try:
2549 2542 return perf[f]
2550 2543 except KeyError:
2551 2544 try:
2552 2545 val = -os.stat(f).st_size
2553 2546 except OSError as e:
2554 2547 if e.errno != errno.ENOENT:
2555 2548 raise
2556 2549 perf[f] = -1e9 # file does not exist, tell early
2557 2550 return -1e9
2558 2551 for kw, mul in slow.items():
2559 2552 if kw in f:
2560 2553 val *= mul
2561 2554 if f.endswith(b'.py'):
2562 2555 val /= 10.0
2563 2556 perf[f] = val / 1000.0
2564 2557 return perf[f]
2565 2558
2566 2559 testdescs.sort(key=sortkey)
2567 2560
2568 2561 class TestRunner(object):
2569 2562 """Holds context for executing tests.
2570 2563
2571 2564 Tests rely on a lot of state. This object holds it for them.
2572 2565 """
2573 2566
2574 2567 # Programs required to run tests.
2575 2568 REQUIREDTOOLS = [
2576 2569 b'diff',
2577 2570 b'grep',
2578 2571 b'unzip',
2579 2572 b'gunzip',
2580 2573 b'bunzip2',
2581 2574 b'sed',
2582 2575 ]
2583 2576
2584 2577 # Maps file extensions to test class.
2585 2578 TESTTYPES = [
2586 2579 (b'.py', PythonTest),
2587 2580 (b'.t', TTest),
2588 2581 ]
2589 2582
2590 2583 def __init__(self):
2591 2584 self.options = None
2592 2585 self._hgroot = None
2593 2586 self._testdir = None
2594 2587 self._outputdir = None
2595 2588 self._hgtmp = None
2596 2589 self._installdir = None
2597 2590 self._bindir = None
2598 2591 self._tmpbinddir = None
2599 2592 self._pythondir = None
2600 2593 self._coveragefile = None
2601 2594 self._createdfiles = []
2602 2595 self._hgcommand = None
2603 2596 self._hgpath = None
2604 2597 self._portoffset = 0
2605 2598 self._ports = {}
2606 2599
2607 2600 def run(self, args, parser=None):
2608 2601 """Run the test suite."""
2609 2602 oldmask = os.umask(0o22)
2610 2603 try:
2611 2604 parser = parser or getparser()
2612 2605 options = parseargs(args, parser)
2613 2606 tests = [_bytespath(a) for a in options.tests]
2614 2607 if options.test_list is not None:
2615 2608 for listfile in options.test_list:
2616 2609 with open(listfile, 'rb') as f:
2617 2610 tests.extend(t for t in f.read().splitlines() if t)
2618 2611 self.options = options
2619 2612
2620 2613 self._checktools()
2621 2614 testdescs = self.findtests(tests)
2622 2615 if options.profile_runner:
2623 2616 import statprof
2624 2617 statprof.start()
2625 2618 result = self._run(testdescs)
2626 2619 if options.profile_runner:
2627 2620 statprof.stop()
2628 2621 statprof.display()
2629 2622 return result
2630 2623
2631 2624 finally:
2632 2625 os.umask(oldmask)
2633 2626
2634 2627 def _run(self, testdescs):
2635 2628 testdir = getcwdb()
2636 2629 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2637 2630 # assume all tests in same folder for now
2638 2631 if testdescs:
2639 2632 pathname = os.path.dirname(testdescs[0]['path'])
2640 2633 if pathname:
2641 2634 testdir = os.path.join(testdir, pathname)
2642 2635 self._testdir = osenvironb[b'TESTDIR'] = testdir
2643 2636 if self.options.outputdir:
2644 2637 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2645 2638 else:
2646 2639 self._outputdir = getcwdb()
2647 2640 if testdescs and pathname:
2648 2641 self._outputdir = os.path.join(self._outputdir, pathname)
2649 2642 previoustimes = {}
2650 2643 if self.options.order_by_runtime:
2651 2644 previoustimes = dict(loadtimes(self._outputdir))
2652 2645 sorttests(testdescs, previoustimes, shuffle=self.options.random)
2653 2646
2654 2647 if 'PYTHONHASHSEED' not in os.environ:
2655 2648 # use a random python hash seed all the time
2656 2649 # we do the randomness ourself to know what seed is used
2657 2650 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2658 2651
2659 2652 if self.options.tmpdir:
2660 2653 self.options.keep_tmpdir = True
2661 2654 tmpdir = _bytespath(self.options.tmpdir)
2662 2655 if os.path.exists(tmpdir):
2663 2656 # Meaning of tmpdir has changed since 1.3: we used to create
2664 2657 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2665 2658 # tmpdir already exists.
2666 2659 print("error: temp dir %r already exists" % tmpdir)
2667 2660 return 1
2668 2661
2669 2662 os.makedirs(tmpdir)
2670 2663 else:
2671 2664 d = None
2672 2665 if os.name == 'nt':
2673 2666 # without this, we get the default temp dir location, but
2674 2667 # in all lowercase, which causes troubles with paths (issue3490)
2675 2668 d = osenvironb.get(b'TMP', None)
2676 2669 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2677 2670
2678 2671 self._hgtmp = osenvironb[b'HGTMP'] = (
2679 2672 os.path.realpath(tmpdir))
2680 2673
2681 2674 if self.options.with_hg:
2682 2675 self._installdir = None
2683 2676 whg = self.options.with_hg
2684 2677 self._bindir = os.path.dirname(os.path.realpath(whg))
2685 2678 assert isinstance(self._bindir, bytes)
2686 2679 self._hgcommand = os.path.basename(whg)
2687 2680 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2688 2681 os.makedirs(self._tmpbindir)
2689 2682
2690 2683 normbin = os.path.normpath(os.path.abspath(whg))
2691 2684 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2692 2685
2693 2686 # Other Python scripts in the test harness need to
2694 2687 # `import mercurial`. If `hg` is a Python script, we assume
2695 2688 # the Mercurial modules are relative to its path and tell the tests
2696 2689 # to load Python modules from its directory.
2697 2690 with open(whg, 'rb') as fh:
2698 2691 initial = fh.read(1024)
2699 2692
2700 2693 if re.match(b'#!.*python', initial):
2701 2694 self._pythondir = self._bindir
2702 2695 # If it looks like our in-repo Rust binary, use the source root.
2703 2696 # This is a bit hacky. But rhg is still not supported outside the
2704 2697 # source directory. So until it is, do the simple thing.
2705 2698 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2706 2699 self._pythondir = os.path.dirname(self._testdir)
2707 2700 # Fall back to the legacy behavior.
2708 2701 else:
2709 2702 self._pythondir = self._bindir
2710 2703
2711 2704 else:
2712 2705 self._installdir = os.path.join(self._hgtmp, b"install")
2713 2706 self._bindir = os.path.join(self._installdir, b"bin")
2714 2707 self._hgcommand = b'hg'
2715 2708 self._tmpbindir = self._bindir
2716 2709 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2717 2710
2718 2711 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
2719 2712 # a python script and feed it to python.exe. Legacy stdio is force
2720 2713 # enabled by hg.exe, and this is a more realistic way to launch hg
2721 2714 # anyway.
2722 2715 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
2723 2716 self._hgcommand += b'.exe'
2724 2717
2725 2718 # set CHGHG, then replace "hg" command by "chg"
2726 2719 chgbindir = self._bindir
2727 2720 if self.options.chg or self.options.with_chg:
2728 2721 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2729 2722 else:
2730 2723 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2731 2724 if self.options.chg:
2732 2725 self._hgcommand = b'chg'
2733 2726 elif self.options.with_chg:
2734 2727 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2735 2728 self._hgcommand = os.path.basename(self.options.with_chg)
2736 2729
2737 2730 osenvironb[b"BINDIR"] = self._bindir
2738 2731 osenvironb[b"PYTHON"] = PYTHON
2739 2732
2740 2733 fileb = _bytespath(__file__)
2741 2734 runtestdir = os.path.abspath(os.path.dirname(fileb))
2742 2735 osenvironb[b'RUNTESTDIR'] = runtestdir
2743 2736 if PYTHON3:
2744 2737 sepb = _bytespath(os.pathsep)
2745 2738 else:
2746 2739 sepb = os.pathsep
2747 2740 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2748 2741 if os.path.islink(__file__):
2749 2742 # test helper will likely be at the end of the symlink
2750 2743 realfile = os.path.realpath(fileb)
2751 2744 realdir = os.path.abspath(os.path.dirname(realfile))
2752 2745 path.insert(2, realdir)
2753 2746 if chgbindir != self._bindir:
2754 2747 path.insert(1, chgbindir)
2755 2748 if self._testdir != runtestdir:
2756 2749 path = [self._testdir] + path
2757 2750 if self._tmpbindir != self._bindir:
2758 2751 path = [self._tmpbindir] + path
2759 2752 osenvironb[b"PATH"] = sepb.join(path)
2760 2753
2761 2754 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2762 2755 # can run .../tests/run-tests.py test-foo where test-foo
2763 2756 # adds an extension to HGRC. Also include run-test.py directory to
2764 2757 # import modules like heredoctest.
2765 2758 pypath = [self._pythondir, self._testdir, runtestdir]
2766 2759 # We have to augment PYTHONPATH, rather than simply replacing
2767 2760 # it, in case external libraries are only available via current
2768 2761 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2769 2762 # are in /opt/subversion.)
2770 2763 oldpypath = osenvironb.get(IMPL_PATH)
2771 2764 if oldpypath:
2772 2765 pypath.append(oldpypath)
2773 2766 osenvironb[IMPL_PATH] = sepb.join(pypath)
2774 2767
2775 2768 if self.options.pure:
2776 2769 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2777 2770 os.environ["HGMODULEPOLICY"] = "py"
2778 2771
2779 2772 if self.options.allow_slow_tests:
2780 2773 os.environ["HGTEST_SLOW"] = "slow"
2781 2774 elif 'HGTEST_SLOW' in os.environ:
2782 2775 del os.environ['HGTEST_SLOW']
2783 2776
2784 2777 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2785 2778
2786 2779 if self.options.exceptions:
2787 2780 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2788 2781 try:
2789 2782 os.makedirs(exceptionsdir)
2790 2783 except OSError as e:
2791 2784 if e.errno != errno.EEXIST:
2792 2785 raise
2793 2786
2794 2787 # Remove all existing exception reports.
2795 2788 for f in os.listdir(exceptionsdir):
2796 2789 os.unlink(os.path.join(exceptionsdir, f))
2797 2790
2798 2791 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2799 2792 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2800 2793 self.options.extra_config_opt.append(
2801 2794 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2802 2795
2803 2796 vlog("# Using TESTDIR", self._testdir)
2804 2797 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2805 2798 vlog("# Using HGTMP", self._hgtmp)
2806 2799 vlog("# Using PATH", os.environ["PATH"])
2807 2800 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2808 2801 vlog("# Writing to directory", self._outputdir)
2809 2802
2810 2803 try:
2811 2804 return self._runtests(testdescs) or 0
2812 2805 finally:
2813 2806 time.sleep(.1)
2814 2807 self._cleanup()
2815 2808
2816 2809 def findtests(self, args):
2817 2810 """Finds possible test files from arguments.
2818 2811
2819 2812 If you wish to inject custom tests into the test harness, this would
2820 2813 be a good function to monkeypatch or override in a derived class.
2821 2814 """
2822 2815 if not args:
2823 2816 if self.options.changed:
2824 2817 proc = Popen4(b'hg st --rev "%s" -man0 .' %
2825 2818 _bytespath(self.options.changed), None, 0)
2826 2819 stdout, stderr = proc.communicate()
2827 2820 args = stdout.strip(b'\0').split(b'\0')
2828 2821 else:
2829 2822 args = os.listdir(b'.')
2830 2823
2831 2824 expanded_args = []
2832 2825 for arg in args:
2833 2826 if os.path.isdir(arg):
2834 2827 if not arg.endswith(b'/'):
2835 2828 arg += b'/'
2836 2829 expanded_args.extend([arg + a for a in os.listdir(arg)])
2837 2830 else:
2838 2831 expanded_args.append(arg)
2839 2832 args = expanded_args
2840 2833
2841 2834 testcasepattern = re.compile(
2842 2835 br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-\.#]+))')
2843 2836 tests = []
2844 2837 for t in args:
2845 2838 case = []
2846 2839
2847 2840 if not (os.path.basename(t).startswith(b'test-')
2848 2841 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2849 2842
2850 2843 m = testcasepattern.match(os.path.basename(t))
2851 2844 if m is not None:
2852 2845 t_basename, casestr = m.groups()
2853 2846 t = os.path.join(os.path.dirname(t), t_basename)
2854 2847 if casestr:
2855 2848 case = casestr.split(b'#')
2856 2849 else:
2857 2850 continue
2858 2851
2859 2852 if t.endswith(b'.t'):
2860 2853 # .t file may contain multiple test cases
2861 2854 casedimensions = parsettestcases(t)
2862 2855 if casedimensions:
2863 2856 cases = []
2864 2857 def addcases(case, casedimensions):
2865 2858 if not casedimensions:
2866 2859 cases.append(case)
2867 2860 else:
2868 2861 for c in casedimensions[0]:
2869 2862 addcases(case + [c], casedimensions[1:])
2870 2863 addcases([], casedimensions)
2871 2864 if case and case in cases:
2872 2865 cases = [case]
2873 2866 elif case:
2874 2867 # Ignore invalid cases
2875 2868 cases = []
2876 2869 else:
2877 2870 pass
2878 2871 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2879 2872 else:
2880 2873 tests.append({'path': t})
2881 2874 else:
2882 2875 tests.append({'path': t})
2883 2876 return tests
2884 2877
2885 2878 def _runtests(self, testdescs):
2886 2879 def _reloadtest(test, i):
2887 2880 # convert a test back to its description dict
2888 2881 desc = {'path': test.path}
2889 2882 case = getattr(test, '_case', [])
2890 2883 if case:
2891 2884 desc['case'] = case
2892 2885 return self._gettest(desc, i)
2893 2886
2894 2887 try:
2895 2888 if self.options.restart:
2896 2889 orig = list(testdescs)
2897 2890 while testdescs:
2898 2891 desc = testdescs[0]
2899 2892 # desc['path'] is a relative path
2900 2893 if 'case' in desc:
2901 2894 casestr = b'#'.join(desc['case'])
2902 2895 errpath = b'%s#%s.err' % (desc['path'], casestr)
2903 2896 else:
2904 2897 errpath = b'%s.err' % desc['path']
2905 2898 errpath = os.path.join(self._outputdir, errpath)
2906 2899 if os.path.exists(errpath):
2907 2900 break
2908 2901 testdescs.pop(0)
2909 2902 if not testdescs:
2910 2903 print("running all tests")
2911 2904 testdescs = orig
2912 2905
2913 2906 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2914 2907 num_tests = len(tests) * self.options.runs_per_test
2915 2908
2916 2909 jobs = min(num_tests, self.options.jobs)
2917 2910
2918 2911 failed = False
2919 2912 kws = self.options.keywords
2920 2913 if kws is not None and PYTHON3:
2921 2914 kws = kws.encode('utf-8')
2922 2915
2923 2916 suite = TestSuite(self._testdir,
2924 2917 jobs=jobs,
2925 2918 whitelist=self.options.whitelisted,
2926 2919 blacklist=self.options.blacklist,
2927 2920 retest=self.options.retest,
2928 2921 keywords=kws,
2929 2922 loop=self.options.loop,
2930 2923 runs_per_test=self.options.runs_per_test,
2931 2924 showchannels=self.options.showchannels,
2932 2925 tests=tests, loadtest=_reloadtest)
2933 2926 verbosity = 1
2934 2927 if self.options.list_tests:
2935 2928 verbosity = 0
2936 2929 elif self.options.verbose:
2937 2930 verbosity = 2
2938 2931 runner = TextTestRunner(self, verbosity=verbosity)
2939 2932
2940 2933 if self.options.list_tests:
2941 2934 result = runner.listtests(suite)
2942 2935 else:
2943 2936 if self._installdir:
2944 2937 self._installhg()
2945 2938 self._checkhglib("Testing")
2946 2939 else:
2947 2940 self._usecorrectpython()
2948 2941 if self.options.chg:
2949 2942 assert self._installdir
2950 2943 self._installchg()
2951 2944
2952 2945 log('running %d tests using %d parallel processes' % (
2953 2946 num_tests, jobs))
2954 2947
2955 2948 result = runner.run(suite)
2956 2949
2957 2950 if result.failures or result.errors:
2958 2951 failed = True
2959 2952
2960 2953 result.onEnd()
2961 2954
2962 2955 if self.options.anycoverage:
2963 2956 self._outputcoverage()
2964 2957 except KeyboardInterrupt:
2965 2958 failed = True
2966 2959 print("\ninterrupted!")
2967 2960
2968 2961 if failed:
2969 2962 return 1
2970 2963
2971 2964 def _getport(self, count):
2972 2965 port = self._ports.get(count) # do we have a cached entry?
2973 2966 if port is None:
2974 2967 portneeded = 3
2975 2968 # above 100 tries we just give up and let test reports failure
2976 2969 for tries in xrange(100):
2977 2970 allfree = True
2978 2971 port = self.options.port + self._portoffset
2979 2972 for idx in xrange(portneeded):
2980 2973 if not checkportisavailable(port + idx):
2981 2974 allfree = False
2982 2975 break
2983 2976 self._portoffset += portneeded
2984 2977 if allfree:
2985 2978 break
2986 2979 self._ports[count] = port
2987 2980 return port
2988 2981
2989 2982 def _gettest(self, testdesc, count):
2990 2983 """Obtain a Test by looking at its filename.
2991 2984
2992 2985 Returns a Test instance. The Test may not be runnable if it doesn't
2993 2986 map to a known type.
2994 2987 """
2995 2988 path = testdesc['path']
2996 2989 lctest = path.lower()
2997 2990 testcls = Test
2998 2991
2999 2992 for ext, cls in self.TESTTYPES:
3000 2993 if lctest.endswith(ext):
3001 2994 testcls = cls
3002 2995 break
3003 2996
3004 2997 refpath = os.path.join(getcwdb(), path)
3005 2998 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3006 2999
3007 3000 # extra keyword parameters. 'case' is used by .t tests
3008 3001 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
3009 3002
3010 3003 t = testcls(refpath, self._outputdir, tmpdir,
3011 3004 keeptmpdir=self.options.keep_tmpdir,
3012 3005 debug=self.options.debug,
3013 3006 first=self.options.first,
3014 3007 timeout=self.options.timeout,
3015 3008 startport=self._getport(count),
3016 3009 extraconfigopts=self.options.extra_config_opt,
3017 3010 py3warnings=self.options.py3_warnings,
3018 3011 shell=self.options.shell,
3019 3012 hgcommand=self._hgcommand,
3020 3013 usechg=bool(self.options.with_chg or self.options.chg),
3021 3014 useipv6=useipv6, **kwds)
3022 3015 t.should_reload = True
3023 3016 return t
3024 3017
3025 3018 def _cleanup(self):
3026 3019 """Clean up state from this test invocation."""
3027 3020 if self.options.keep_tmpdir:
3028 3021 return
3029 3022
3030 3023 vlog("# Cleaning up HGTMP", self._hgtmp)
3031 3024 shutil.rmtree(self._hgtmp, True)
3032 3025 for f in self._createdfiles:
3033 3026 try:
3034 3027 os.remove(f)
3035 3028 except OSError:
3036 3029 pass
3037 3030
3038 3031 def _usecorrectpython(self):
3039 3032 """Configure the environment to use the appropriate Python in tests."""
3040 3033 # Tests must use the same interpreter as us or bad things will happen.
3041 3034 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3042 3035
3043 3036 # os.symlink() is a thing with py3 on Windows, but it requires
3044 3037 # Administrator rights.
3045 3038 if getattr(os, 'symlink', None) and os.name != 'nt':
3046 3039 vlog("# Making python executable in test path a symlink to '%s'" %
3047 3040 sysexecutable)
3048 3041 mypython = os.path.join(self._tmpbindir, pyexename)
3049 3042 try:
3050 3043 if os.readlink(mypython) == sysexecutable:
3051 3044 return
3052 3045 os.unlink(mypython)
3053 3046 except OSError as err:
3054 3047 if err.errno != errno.ENOENT:
3055 3048 raise
3056 3049 if self._findprogram(pyexename) != sysexecutable:
3057 3050 try:
3058 3051 os.symlink(sysexecutable, mypython)
3059 3052 self._createdfiles.append(mypython)
3060 3053 except OSError as err:
3061 3054 # child processes may race, which is harmless
3062 3055 if err.errno != errno.EEXIST:
3063 3056 raise
3064 3057 else:
3065 3058 exedir, exename = os.path.split(sysexecutable)
3066 3059 vlog("# Modifying search path to find %s as %s in '%s'" %
3067 3060 (exename, pyexename, exedir))
3068 3061 path = os.environ['PATH'].split(os.pathsep)
3069 3062 while exedir in path:
3070 3063 path.remove(exedir)
3071 3064 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3072 3065 if not self._findprogram(pyexename):
3073 3066 print("WARNING: Cannot find %s in search path" % pyexename)
3074 3067
3075 3068 def _installhg(self):
3076 3069 """Install hg into the test environment.
3077 3070
3078 3071 This will also configure hg with the appropriate testing settings.
3079 3072 """
3080 3073 vlog("# Performing temporary installation of HG")
3081 3074 installerrs = os.path.join(self._hgtmp, b"install.err")
3082 3075 compiler = ''
3083 3076 if self.options.compiler:
3084 3077 compiler = '--compiler ' + self.options.compiler
3085 3078 if self.options.pure:
3086 3079 pure = b"--pure"
3087 3080 else:
3088 3081 pure = b""
3089 3082
3090 3083 # Run installer in hg root
3091 3084 script = os.path.realpath(sys.argv[0])
3092 3085 exe = sysexecutable
3093 3086 if PYTHON3:
3094 3087 compiler = _bytespath(compiler)
3095 3088 script = _bytespath(script)
3096 3089 exe = _bytespath(exe)
3097 3090 hgroot = os.path.dirname(os.path.dirname(script))
3098 3091 self._hgroot = hgroot
3099 3092 os.chdir(hgroot)
3100 3093 nohome = b'--home=""'
3101 3094 if os.name == 'nt':
3102 3095 # The --home="" trick works only on OS where os.sep == '/'
3103 3096 # because of a distutils convert_path() fast-path. Avoid it at
3104 3097 # least on Windows for now, deal with .pydistutils.cfg bugs
3105 3098 # when they happen.
3106 3099 nohome = b''
3107 3100 cmd = (b'"%(exe)s" setup.py %(pure)s clean --all'
3108 3101 b' build %(compiler)s --build-base="%(base)s"'
3109 3102 b' install --force --prefix="%(prefix)s"'
3110 3103 b' --install-lib="%(libdir)s"'
3111 3104 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3112 3105 % {b'exe': exe, b'pure': pure,
3113 3106 b'compiler': compiler,
3114 3107 b'base': os.path.join(self._hgtmp, b"build"),
3115 3108 b'prefix': self._installdir, b'libdir': self._pythondir,
3116 3109 b'bindir': self._bindir,
3117 3110 b'nohome': nohome, b'logfile': installerrs})
3118 3111
3119 3112 # setuptools requires install directories to exist.
3120 3113 def makedirs(p):
3121 3114 try:
3122 3115 os.makedirs(p)
3123 3116 except OSError as e:
3124 3117 if e.errno != errno.EEXIST:
3125 3118 raise
3126 3119 makedirs(self._pythondir)
3127 3120 makedirs(self._bindir)
3128 3121
3129 3122 vlog("# Running", cmd)
3130 3123 if subprocess.call(_strpath(cmd), shell=True) == 0:
3131 3124 if not self.options.verbose:
3132 3125 try:
3133 3126 os.remove(installerrs)
3134 3127 except OSError as e:
3135 3128 if e.errno != errno.ENOENT:
3136 3129 raise
3137 3130 else:
3138 3131 with open(installerrs, 'rb') as f:
3139 3132 for line in f:
3140 3133 if PYTHON3:
3141 3134 sys.stdout.buffer.write(line)
3142 3135 else:
3143 3136 sys.stdout.write(line)
3144 3137 sys.exit(1)
3145 3138 os.chdir(self._testdir)
3146 3139
3147 3140 self._usecorrectpython()
3148 3141
3149 3142 if self.options.py3_warnings and not self.options.anycoverage:
3150 3143 vlog("# Updating hg command to enable Py3k Warnings switch")
3151 3144 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
3152 3145 lines = [line.rstrip() for line in f]
3153 3146 lines[0] += ' -3'
3154 3147 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
3155 3148 for line in lines:
3156 3149 f.write(line + '\n')
3157 3150
3158 3151 hgbat = os.path.join(self._bindir, b'hg.bat')
3159 3152 if os.path.isfile(hgbat):
3160 3153 # hg.bat expects to be put in bin/scripts while run-tests.py
3161 3154 # installation layout put it in bin/ directly. Fix it
3162 3155 with open(hgbat, 'rb') as f:
3163 3156 data = f.read()
3164 3157 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3165 3158 data = data.replace(br'"%~dp0..\python" "%~dp0hg" %*',
3166 3159 b'"%~dp0python" "%~dp0hg" %*')
3167 3160 with open(hgbat, 'wb') as f:
3168 3161 f.write(data)
3169 3162 else:
3170 3163 print('WARNING: cannot fix hg.bat reference to python.exe')
3171 3164
3172 3165 if self.options.anycoverage:
3173 3166 custom = os.path.join(osenvironb[b'RUNTESTDIR'], 'sitecustomize.py')
3174 3167 target = os.path.join(self._pythondir, 'sitecustomize.py')
3175 3168 vlog('# Installing coverage trigger to %s' % target)
3176 3169 shutil.copyfile(custom, target)
3177 3170 rc = os.path.join(self._testdir, '.coveragerc')
3178 3171 vlog('# Installing coverage rc to %s' % rc)
3179 3172 os.environ['COVERAGE_PROCESS_START'] = rc
3180 3173 covdir = os.path.join(self._installdir, '..', 'coverage')
3181 3174 try:
3182 3175 os.mkdir(covdir)
3183 3176 except OSError as e:
3184 3177 if e.errno != errno.EEXIST:
3185 3178 raise
3186 3179
3187 3180 os.environ['COVERAGE_DIR'] = covdir
3188 3181
3189 3182 def _checkhglib(self, verb):
3190 3183 """Ensure that the 'mercurial' package imported by python is
3191 3184 the one we expect it to be. If not, print a warning to stderr."""
3192 3185 if ((self._bindir == self._pythondir) and
3193 3186 (self._bindir != self._tmpbindir)):
3194 3187 # The pythondir has been inferred from --with-hg flag.
3195 3188 # We cannot expect anything sensible here.
3196 3189 return
3197 3190 expecthg = os.path.join(self._pythondir, b'mercurial')
3198 3191 actualhg = self._gethgpath()
3199 3192 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3200 3193 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
3201 3194 ' (expected %s)\n'
3202 3195 % (verb, actualhg, expecthg))
3203 3196 def _gethgpath(self):
3204 3197 """Return the path to the mercurial package that is actually found by
3205 3198 the current Python interpreter."""
3206 3199 if self._hgpath is not None:
3207 3200 return self._hgpath
3208 3201
3209 3202 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3210 3203 cmd = cmd % PYTHON
3211 3204 if PYTHON3:
3212 3205 cmd = _strpath(cmd)
3213 3206
3214 3207 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3215 3208 out, err = p.communicate()
3216 3209
3217 3210 self._hgpath = out.strip()
3218 3211
3219 3212 return self._hgpath
3220 3213
3221 3214 def _installchg(self):
3222 3215 """Install chg into the test environment"""
3223 3216 vlog('# Performing temporary installation of CHG')
3224 3217 assert os.path.dirname(self._bindir) == self._installdir
3225 3218 assert self._hgroot, 'must be called after _installhg()'
3226 3219 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
3227 3220 % {b'make': b'make', # TODO: switch by option or environment?
3228 3221 b'prefix': self._installdir})
3229 3222 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3230 3223 vlog("# Running", cmd)
3231 3224 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
3232 3225 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3233 3226 stderr=subprocess.STDOUT)
3234 3227 out, _err = proc.communicate()
3235 3228 if proc.returncode != 0:
3236 3229 if PYTHON3:
3237 3230 sys.stdout.buffer.write(out)
3238 3231 else:
3239 3232 sys.stdout.write(out)
3240 3233 sys.exit(1)
3241 3234
3242 3235 def _outputcoverage(self):
3243 3236 """Produce code coverage output."""
3244 3237 import coverage
3245 3238 coverage = coverage.coverage
3246 3239
3247 3240 vlog('# Producing coverage report')
3248 3241 # chdir is the easiest way to get short, relative paths in the
3249 3242 # output.
3250 3243 os.chdir(self._hgroot)
3251 3244 covdir = os.path.join(self._installdir, '..', 'coverage')
3252 3245 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3253 3246
3254 3247 # Map install directory paths back to source directory.
3255 3248 cov.config.paths['srcdir'] = ['.', self._pythondir]
3256 3249
3257 3250 cov.combine()
3258 3251
3259 3252 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
3260 3253 cov.report(ignore_errors=True, omit=omit)
3261 3254
3262 3255 if self.options.htmlcov:
3263 3256 htmldir = os.path.join(self._outputdir, 'htmlcov')
3264 3257 cov.html_report(directory=htmldir, omit=omit)
3265 3258 if self.options.annotate:
3266 3259 adir = os.path.join(self._outputdir, 'annotated')
3267 3260 if not os.path.isdir(adir):
3268 3261 os.mkdir(adir)
3269 3262 cov.annotate(directory=adir, omit=omit)
3270 3263
3271 3264 def _findprogram(self, program):
3272 3265 """Search PATH for a executable program"""
3273 3266 dpb = _bytespath(os.defpath)
3274 3267 sepb = _bytespath(os.pathsep)
3275 3268 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3276 3269 name = os.path.join(p, program)
3277 3270 if os.name == 'nt' or os.access(name, os.X_OK):
3278 3271 return name
3279 3272 return None
3280 3273
3281 3274 def _checktools(self):
3282 3275 """Ensure tools required to run tests are present."""
3283 3276 for p in self.REQUIREDTOOLS:
3284 3277 if os.name == 'nt' and not p.endswith(b'.exe'):
3285 3278 p += b'.exe'
3286 3279 found = self._findprogram(p)
3287 3280 if found:
3288 3281 vlog("# Found prerequisite", p, "at", found)
3289 3282 else:
3290 3283 print("WARNING: Did not find prerequisite tool: %s " %
3291 3284 p.decode("utf-8"))
3292 3285
3293 3286 def aggregateexceptions(path):
3294 3287 exceptioncounts = collections.Counter()
3295 3288 testsbyfailure = collections.defaultdict(set)
3296 3289 failuresbytest = collections.defaultdict(set)
3297 3290
3298 3291 for f in os.listdir(path):
3299 3292 with open(os.path.join(path, f), 'rb') as fh:
3300 3293 data = fh.read().split(b'\0')
3301 3294 if len(data) != 5:
3302 3295 continue
3303 3296
3304 3297 exc, mainframe, hgframe, hgline, testname = data
3305 3298 exc = exc.decode('utf-8')
3306 3299 mainframe = mainframe.decode('utf-8')
3307 3300 hgframe = hgframe.decode('utf-8')
3308 3301 hgline = hgline.decode('utf-8')
3309 3302 testname = testname.decode('utf-8')
3310 3303
3311 3304 key = (hgframe, hgline, exc)
3312 3305 exceptioncounts[key] += 1
3313 3306 testsbyfailure[key].add(testname)
3314 3307 failuresbytest[testname].add(key)
3315 3308
3316 3309 # Find test having fewest failures for each failure.
3317 3310 leastfailing = {}
3318 3311 for key, tests in testsbyfailure.items():
3319 3312 fewesttest = None
3320 3313 fewestcount = 99999999
3321 3314 for test in sorted(tests):
3322 3315 if len(failuresbytest[test]) < fewestcount:
3323 3316 fewesttest = test
3324 3317 fewestcount = len(failuresbytest[test])
3325 3318
3326 3319 leastfailing[key] = (fewestcount, fewesttest)
3327 3320
3328 3321 # Create a combined counter so we can sort by total occurrences and
3329 3322 # impacted tests.
3330 3323 combined = {}
3331 3324 for key in exceptioncounts:
3332 3325 combined[key] = (exceptioncounts[key],
3333 3326 len(testsbyfailure[key]),
3334 3327 leastfailing[key][0],
3335 3328 leastfailing[key][1])
3336 3329
3337 3330 return {
3338 3331 'exceptioncounts': exceptioncounts,
3339 3332 'total': sum(exceptioncounts.values()),
3340 3333 'combined': combined,
3341 3334 'leastfailing': leastfailing,
3342 3335 'byfailure': testsbyfailure,
3343 3336 'bytest': failuresbytest,
3344 3337 }
3345 3338
3346 3339 if __name__ == '__main__':
3347 3340 runner = TestRunner()
3348 3341
3349 3342 try:
3350 3343 import msvcrt
3351 3344 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3352 3345 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3353 3346 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3354 3347 except ImportError:
3355 3348 pass
3356 3349
3357 3350 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now