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