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