Show More
@@ -1,4067 +1,4069 b'' | |||
|
1 | 1 | #!/usr/bin/env python3 |
|
2 | 2 | # |
|
3 | 3 | # run-tests.py - Run a set of tests on Mercurial |
|
4 | 4 | # |
|
5 | 5 | # Copyright 2006 Olivia Mackall <olivia@selenic.com> |
|
6 | 6 | # |
|
7 | 7 | # This software may be used and distributed according to the terms of the |
|
8 | 8 | # GNU General Public License version 2 or any later version. |
|
9 | 9 | |
|
10 | 10 | # Modifying this script is tricky because it has many modes: |
|
11 | 11 | # - serial (default) vs parallel (-jN, N > 1) |
|
12 | 12 | # - no coverage (default) vs coverage (-c, -C, -s) |
|
13 | 13 | # - temp install (default) vs specific hg script (--with-hg, --local) |
|
14 | 14 | # - tests are a mix of shell scripts and Python scripts |
|
15 | 15 | # |
|
16 | 16 | # If you change this script, it is recommended that you ensure you |
|
17 | 17 | # haven't broken it by running it in various modes with a representative |
|
18 | 18 | # sample of test scripts. For example: |
|
19 | 19 | # |
|
20 | 20 | # 1) serial, no coverage, temp install: |
|
21 | 21 | # ./run-tests.py test-s* |
|
22 | 22 | # 2) serial, no coverage, local hg: |
|
23 | 23 | # ./run-tests.py --local test-s* |
|
24 | 24 | # 3) serial, coverage, temp install: |
|
25 | 25 | # ./run-tests.py -c test-s* |
|
26 | 26 | # 4) serial, coverage, local hg: |
|
27 | 27 | # ./run-tests.py -c --local test-s* # unsupported |
|
28 | 28 | # 5) parallel, no coverage, temp install: |
|
29 | 29 | # ./run-tests.py -j2 test-s* |
|
30 | 30 | # 6) parallel, no coverage, local hg: |
|
31 | 31 | # ./run-tests.py -j2 --local test-s* |
|
32 | 32 | # 7) parallel, coverage, temp install: |
|
33 | 33 | # ./run-tests.py -j2 -c test-s* # currently broken |
|
34 | 34 | # 8) parallel, coverage, local install: |
|
35 | 35 | # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) |
|
36 | 36 | # 9) parallel, custom tmp dir: |
|
37 | 37 | # ./run-tests.py -j2 --tmpdir /tmp/myhgtests |
|
38 | 38 | # 10) parallel, pure, tests that call run-tests: |
|
39 | 39 | # ./run-tests.py --pure `grep -l run-tests.py *.t` |
|
40 | 40 | # |
|
41 | 41 | # (You could use any subset of the tests: test-s* happens to match |
|
42 | 42 | # enough that it's worth doing parallel runs, few enough that it |
|
43 | 43 | # completes fairly quickly, includes both shell and Python scripts, and |
|
44 | 44 | # includes some scripts that run daemon processes.) |
|
45 | 45 | |
|
46 | 46 | from __future__ import absolute_import, print_function |
|
47 | 47 | |
|
48 | 48 | import argparse |
|
49 | 49 | import collections |
|
50 | 50 | import contextlib |
|
51 | 51 | import difflib |
|
52 | 52 | import distutils.version as version |
|
53 | 53 | import errno |
|
54 | 54 | import json |
|
55 | 55 | import multiprocessing |
|
56 | 56 | import os |
|
57 | 57 | import platform |
|
58 | 58 | import random |
|
59 | 59 | import re |
|
60 | 60 | import shutil |
|
61 | 61 | import signal |
|
62 | 62 | import socket |
|
63 | 63 | import subprocess |
|
64 | 64 | import sys |
|
65 | 65 | import sysconfig |
|
66 | 66 | import tempfile |
|
67 | 67 | import threading |
|
68 | 68 | import time |
|
69 | 69 | import unittest |
|
70 | 70 | import uuid |
|
71 | 71 | import xml.dom.minidom as minidom |
|
72 | 72 | |
|
73 | 73 | WINDOWS = os.name == r'nt' |
|
74 | 74 | |
|
75 | 75 | try: |
|
76 | 76 | import Queue as queue |
|
77 | 77 | except ImportError: |
|
78 | 78 | import queue |
|
79 | 79 | |
|
80 | 80 | try: |
|
81 | 81 | import shlex |
|
82 | 82 | |
|
83 | 83 | shellquote = shlex.quote |
|
84 | 84 | except (ImportError, AttributeError): |
|
85 | 85 | import pipes |
|
86 | 86 | |
|
87 | 87 | shellquote = pipes.quote |
|
88 | 88 | |
|
89 | 89 | |
|
90 | 90 | processlock = threading.Lock() |
|
91 | 91 | |
|
92 | 92 | pygmentspresent = False |
|
93 | 93 | try: # is pygments installed |
|
94 | 94 | import pygments |
|
95 | 95 | import pygments.lexers as lexers |
|
96 | 96 | import pygments.lexer as lexer |
|
97 | 97 | import pygments.formatters as formatters |
|
98 | 98 | import pygments.token as token |
|
99 | 99 | import pygments.style as style |
|
100 | 100 | |
|
101 | 101 | if WINDOWS: |
|
102 | 102 | hgpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
103 | 103 | sys.path.append(hgpath) |
|
104 | 104 | try: |
|
105 | 105 | from mercurial import win32 # pytype: disable=import-error |
|
106 | 106 | |
|
107 | 107 | # Don't check the result code because it fails on heptapod, but |
|
108 | 108 | # something is able to convert to color anyway. |
|
109 | 109 | win32.enablevtmode() |
|
110 | 110 | finally: |
|
111 | 111 | sys.path = sys.path[:-1] |
|
112 | 112 | |
|
113 | 113 | pygmentspresent = True |
|
114 | 114 | difflexer = lexers.DiffLexer() |
|
115 | 115 | terminal256formatter = formatters.Terminal256Formatter() |
|
116 | 116 | except ImportError: |
|
117 | 117 | pass |
|
118 | 118 | |
|
119 | 119 | if pygmentspresent: |
|
120 | 120 | |
|
121 | 121 | class TestRunnerStyle(style.Style): |
|
122 | 122 | default_style = "" |
|
123 | 123 | skipped = token.string_to_tokentype("Token.Generic.Skipped") |
|
124 | 124 | failed = token.string_to_tokentype("Token.Generic.Failed") |
|
125 | 125 | skippedname = token.string_to_tokentype("Token.Generic.SName") |
|
126 | 126 | failedname = token.string_to_tokentype("Token.Generic.FName") |
|
127 | 127 | styles = { |
|
128 | 128 | skipped: '#e5e5e5', |
|
129 | 129 | skippedname: '#00ffff', |
|
130 | 130 | failed: '#7f0000', |
|
131 | 131 | failedname: '#ff0000', |
|
132 | 132 | } |
|
133 | 133 | |
|
134 | 134 | class TestRunnerLexer(lexer.RegexLexer): |
|
135 | 135 | testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?' |
|
136 | 136 | tokens = { |
|
137 | 137 | 'root': [ |
|
138 | 138 | (r'^Skipped', token.Generic.Skipped, 'skipped'), |
|
139 | 139 | (r'^Failed ', token.Generic.Failed, 'failed'), |
|
140 | 140 | (r'^ERROR: ', token.Generic.Failed, 'failed'), |
|
141 | 141 | ], |
|
142 | 142 | 'skipped': [ |
|
143 | 143 | (testpattern, token.Generic.SName), |
|
144 | 144 | (r':.*', token.Generic.Skipped), |
|
145 | 145 | ], |
|
146 | 146 | 'failed': [ |
|
147 | 147 | (testpattern, token.Generic.FName), |
|
148 | 148 | (r'(:| ).*', token.Generic.Failed), |
|
149 | 149 | ], |
|
150 | 150 | } |
|
151 | 151 | |
|
152 | 152 | runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle) |
|
153 | 153 | runnerlexer = TestRunnerLexer() |
|
154 | 154 | |
|
155 | 155 | origenviron = os.environ.copy() |
|
156 | 156 | |
|
157 | 157 | |
|
158 | 158 | if sys.version_info > (3, 5, 0): |
|
159 | 159 | PYTHON3 = True |
|
160 | 160 | xrange = range # we use xrange in one place, and we'd rather not use range |
|
161 | 161 | |
|
162 | 162 | def _sys2bytes(p): |
|
163 | 163 | if p is None: |
|
164 | 164 | return p |
|
165 | 165 | return p.encode('utf-8') |
|
166 | 166 | |
|
167 | 167 | def _bytes2sys(p): |
|
168 | 168 | if p is None: |
|
169 | 169 | return p |
|
170 | 170 | return p.decode('utf-8') |
|
171 | 171 | |
|
172 | 172 | osenvironb = getattr(os, 'environb', None) |
|
173 | 173 | if osenvironb is None: |
|
174 | 174 | # Windows lacks os.environb, for instance. A proxy over the real thing |
|
175 | 175 | # instead of a copy allows the environment to be updated via bytes on |
|
176 | 176 | # all platforms. |
|
177 | 177 | class environbytes(object): |
|
178 | 178 | def __init__(self, strenv): |
|
179 | 179 | self.__len__ = strenv.__len__ |
|
180 | 180 | self.clear = strenv.clear |
|
181 | 181 | self._strenv = strenv |
|
182 | 182 | |
|
183 | 183 | def __getitem__(self, k): |
|
184 | 184 | v = self._strenv.__getitem__(_bytes2sys(k)) |
|
185 | 185 | return _sys2bytes(v) |
|
186 | 186 | |
|
187 | 187 | def __setitem__(self, k, v): |
|
188 | 188 | self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v)) |
|
189 | 189 | |
|
190 | 190 | def __delitem__(self, k): |
|
191 | 191 | self._strenv.__delitem__(_bytes2sys(k)) |
|
192 | 192 | |
|
193 | 193 | def __contains__(self, k): |
|
194 | 194 | return self._strenv.__contains__(_bytes2sys(k)) |
|
195 | 195 | |
|
196 | 196 | def __iter__(self): |
|
197 | 197 | return iter([_sys2bytes(k) for k in iter(self._strenv)]) |
|
198 | 198 | |
|
199 | 199 | def get(self, k, default=None): |
|
200 | 200 | v = self._strenv.get(_bytes2sys(k), _bytes2sys(default)) |
|
201 | 201 | return _sys2bytes(v) |
|
202 | 202 | |
|
203 | 203 | def pop(self, k, default=None): |
|
204 | 204 | v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default)) |
|
205 | 205 | return _sys2bytes(v) |
|
206 | 206 | |
|
207 | 207 | osenvironb = environbytes(os.environ) |
|
208 | 208 | |
|
209 | 209 | getcwdb = getattr(os, 'getcwdb') |
|
210 | 210 | if not getcwdb or WINDOWS: |
|
211 | 211 | getcwdb = lambda: _sys2bytes(os.getcwd()) |
|
212 | 212 | |
|
213 | 213 | elif sys.version_info >= (3, 0, 0): |
|
214 | 214 | print( |
|
215 | 215 | '%s is only supported on Python 3.5+ and 2.7, not %s' |
|
216 | 216 | % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])) |
|
217 | 217 | ) |
|
218 | 218 | sys.exit(70) # EX_SOFTWARE from `man 3 sysexit` |
|
219 | 219 | else: |
|
220 | 220 | PYTHON3 = False |
|
221 | 221 | |
|
222 | 222 | # In python 2.x, path operations are generally done using |
|
223 | 223 | # bytestrings by default, so we don't have to do any extra |
|
224 | 224 | # fiddling there. We define the wrapper functions anyway just to |
|
225 | 225 | # help keep code consistent between platforms. |
|
226 | 226 | def _sys2bytes(p): |
|
227 | 227 | return p |
|
228 | 228 | |
|
229 | 229 | _bytes2sys = _sys2bytes |
|
230 | 230 | osenvironb = os.environ |
|
231 | 231 | getcwdb = os.getcwd |
|
232 | 232 | |
|
233 | 233 | if WINDOWS: |
|
234 | 234 | _getcwdb = getcwdb |
|
235 | 235 | |
|
236 | 236 | def getcwdb(): |
|
237 | 237 | cwd = _getcwdb() |
|
238 | 238 | if re.match(b'^[a-z]:', cwd): |
|
239 | 239 | # os.getcwd() is inconsistent on the capitalization of the drive |
|
240 | 240 | # letter, so adjust it. see https://bugs.python.org/issue40368 |
|
241 | 241 | cwd = cwd[0:1].upper() + cwd[1:] |
|
242 | 242 | return cwd |
|
243 | 243 | |
|
244 | 244 | |
|
245 | 245 | # For Windows support |
|
246 | 246 | wifexited = getattr(os, "WIFEXITED", lambda x: False) |
|
247 | 247 | |
|
248 | 248 | # Whether to use IPv6 |
|
249 | 249 | def checksocketfamily(name, port=20058): |
|
250 | 250 | """return true if we can listen on localhost using family=name |
|
251 | 251 | |
|
252 | 252 | name should be either 'AF_INET', or 'AF_INET6'. |
|
253 | 253 | port being used is okay - EADDRINUSE is considered as successful. |
|
254 | 254 | """ |
|
255 | 255 | family = getattr(socket, name, None) |
|
256 | 256 | if family is None: |
|
257 | 257 | return False |
|
258 | 258 | try: |
|
259 | 259 | s = socket.socket(family, socket.SOCK_STREAM) |
|
260 | 260 | s.bind(('localhost', port)) |
|
261 | 261 | s.close() |
|
262 | 262 | return True |
|
263 | 263 | except socket.error as exc: |
|
264 | 264 | if exc.errno == errno.EADDRINUSE: |
|
265 | 265 | return True |
|
266 | 266 | elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT): |
|
267 | 267 | return False |
|
268 | 268 | else: |
|
269 | 269 | raise |
|
270 | 270 | else: |
|
271 | 271 | return False |
|
272 | 272 | |
|
273 | 273 | |
|
274 | 274 | # useipv6 will be set by parseargs |
|
275 | 275 | useipv6 = None |
|
276 | 276 | |
|
277 | 277 | |
|
278 | 278 | def checkportisavailable(port): |
|
279 | 279 | """return true if a port seems free to bind on localhost""" |
|
280 | 280 | if useipv6: |
|
281 | 281 | family = socket.AF_INET6 |
|
282 | 282 | else: |
|
283 | 283 | family = socket.AF_INET |
|
284 | 284 | try: |
|
285 | 285 | with contextlib.closing(socket.socket(family, socket.SOCK_STREAM)) as s: |
|
286 | 286 | s.bind(('localhost', port)) |
|
287 | 287 | return True |
|
288 | 288 | except socket.error as exc: |
|
289 | 289 | if WINDOWS and exc.errno == errno.WSAEACCES: |
|
290 | 290 | return False |
|
291 | 291 | elif PYTHON3: |
|
292 | 292 | # TODO: make a proper exception handler after dropping py2. This |
|
293 | 293 | # works because socket.error is an alias for OSError on py3, |
|
294 | 294 | # which is also the baseclass of PermissionError. |
|
295 | 295 | if isinstance(exc, PermissionError): |
|
296 | 296 | return False |
|
297 | 297 | if exc.errno not in ( |
|
298 | 298 | errno.EADDRINUSE, |
|
299 | 299 | errno.EADDRNOTAVAIL, |
|
300 | 300 | errno.EPROTONOSUPPORT, |
|
301 | 301 | ): |
|
302 | 302 | raise |
|
303 | 303 | return False |
|
304 | 304 | |
|
305 | 305 | |
|
306 | 306 | closefds = os.name == 'posix' |
|
307 | 307 | |
|
308 | 308 | |
|
309 | 309 | def Popen4(cmd, wd, timeout, env=None): |
|
310 | 310 | processlock.acquire() |
|
311 | 311 | p = subprocess.Popen( |
|
312 | 312 | _bytes2sys(cmd), |
|
313 | 313 | shell=True, |
|
314 | 314 | bufsize=-1, |
|
315 | 315 | cwd=_bytes2sys(wd), |
|
316 | 316 | env=env, |
|
317 | 317 | close_fds=closefds, |
|
318 | 318 | stdin=subprocess.PIPE, |
|
319 | 319 | stdout=subprocess.PIPE, |
|
320 | 320 | stderr=subprocess.STDOUT, |
|
321 | 321 | ) |
|
322 | 322 | processlock.release() |
|
323 | 323 | |
|
324 | 324 | p.fromchild = p.stdout |
|
325 | 325 | p.tochild = p.stdin |
|
326 | 326 | p.childerr = p.stderr |
|
327 | 327 | |
|
328 | 328 | p.timeout = False |
|
329 | 329 | if timeout: |
|
330 | 330 | |
|
331 | 331 | def t(): |
|
332 | 332 | start = time.time() |
|
333 | 333 | while time.time() - start < timeout and p.returncode is None: |
|
334 | 334 | time.sleep(0.1) |
|
335 | 335 | p.timeout = True |
|
336 | 336 | vlog('# Timout reached for process %d' % p.pid) |
|
337 | 337 | if p.returncode is None: |
|
338 | 338 | terminate(p) |
|
339 | 339 | |
|
340 | 340 | threading.Thread(target=t).start() |
|
341 | 341 | |
|
342 | 342 | return p |
|
343 | 343 | |
|
344 | 344 | |
|
345 | 345 | if sys.executable: |
|
346 | 346 | sysexecutable = sys.executable |
|
347 | 347 | elif os.environ.get('PYTHONEXECUTABLE'): |
|
348 | 348 | sysexecutable = os.environ['PYTHONEXECUTABLE'] |
|
349 | 349 | elif os.environ.get('PYTHON'): |
|
350 | 350 | sysexecutable = os.environ['PYTHON'] |
|
351 | 351 | else: |
|
352 | 352 | raise AssertionError('Could not find Python interpreter') |
|
353 | 353 | |
|
354 | 354 | PYTHON = _sys2bytes(sysexecutable.replace('\\', '/')) |
|
355 | 355 | IMPL_PATH = b'PYTHONPATH' |
|
356 | 356 | if 'java' in sys.platform: |
|
357 | 357 | IMPL_PATH = b'JYTHONPATH' |
|
358 | 358 | |
|
359 | 359 | default_defaults = { |
|
360 | 360 | 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()), |
|
361 | 361 | 'timeout': ('HGTEST_TIMEOUT', 360), |
|
362 | 362 | 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500), |
|
363 | 363 | 'port': ('HGTEST_PORT', 20059), |
|
364 | 364 | 'shell': ('HGTEST_SHELL', 'sh'), |
|
365 | 365 | } |
|
366 | 366 | |
|
367 | 367 | defaults = default_defaults.copy() |
|
368 | 368 | |
|
369 | 369 | |
|
370 | 370 | def canonpath(path): |
|
371 | 371 | return os.path.realpath(os.path.expanduser(path)) |
|
372 | 372 | |
|
373 | 373 | |
|
374 | 374 | def which(exe): |
|
375 | 375 | if PYTHON3: |
|
376 | 376 | # shutil.which only accept bytes from 3.8 |
|
377 | 377 | cmd = _bytes2sys(exe) |
|
378 | 378 | real_exec = shutil.which(cmd) |
|
379 | 379 | return _sys2bytes(real_exec) |
|
380 | 380 | else: |
|
381 | 381 | # let us do the os work |
|
382 | 382 | for p in osenvironb[b'PATH'].split(os.pathsep): |
|
383 | 383 | f = os.path.join(p, exe) |
|
384 | 384 | if os.path.isfile(f): |
|
385 | 385 | return f |
|
386 | 386 | return None |
|
387 | 387 | |
|
388 | 388 | |
|
389 | 389 | def parselistfiles(files, listtype, warn=True): |
|
390 | 390 | entries = dict() |
|
391 | 391 | for filename in files: |
|
392 | 392 | try: |
|
393 | 393 | path = os.path.expanduser(os.path.expandvars(filename)) |
|
394 | 394 | f = open(path, "rb") |
|
395 | 395 | except IOError as err: |
|
396 | 396 | if err.errno != errno.ENOENT: |
|
397 | 397 | raise |
|
398 | 398 | if warn: |
|
399 | 399 | print("warning: no such %s file: %s" % (listtype, filename)) |
|
400 | 400 | continue |
|
401 | 401 | |
|
402 | 402 | for line in f.readlines(): |
|
403 | 403 | line = line.split(b'#', 1)[0].strip() |
|
404 | 404 | if line: |
|
405 | 405 | # Ensure path entries are compatible with os.path.relpath() |
|
406 | 406 | entries[os.path.normpath(line)] = filename |
|
407 | 407 | |
|
408 | 408 | f.close() |
|
409 | 409 | return entries |
|
410 | 410 | |
|
411 | 411 | |
|
412 | 412 | def parsettestcases(path): |
|
413 | 413 | """read a .t test file, return a set of test case names |
|
414 | 414 | |
|
415 | 415 | If path does not exist, return an empty set. |
|
416 | 416 | """ |
|
417 | 417 | cases = [] |
|
418 | 418 | try: |
|
419 | 419 | with open(path, 'rb') as f: |
|
420 | 420 | for l in f: |
|
421 | 421 | if l.startswith(b'#testcases '): |
|
422 | 422 | cases.append(sorted(l[11:].split())) |
|
423 | 423 | except IOError as ex: |
|
424 | 424 | if ex.errno != errno.ENOENT: |
|
425 | 425 | raise |
|
426 | 426 | return cases |
|
427 | 427 | |
|
428 | 428 | |
|
429 | 429 | def getparser(): |
|
430 | 430 | """Obtain the OptionParser used by the CLI.""" |
|
431 | 431 | parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]') |
|
432 | 432 | |
|
433 | 433 | selection = parser.add_argument_group('Test Selection') |
|
434 | 434 | selection.add_argument( |
|
435 | 435 | '--allow-slow-tests', |
|
436 | 436 | action='store_true', |
|
437 | 437 | help='allow extremely slow tests', |
|
438 | 438 | ) |
|
439 | 439 | selection.add_argument( |
|
440 | 440 | "--blacklist", |
|
441 | 441 | action="append", |
|
442 | 442 | help="skip tests listed in the specified blacklist file", |
|
443 | 443 | ) |
|
444 | 444 | selection.add_argument( |
|
445 | 445 | "--changed", |
|
446 | 446 | help="run tests that are changed in parent rev or working directory", |
|
447 | 447 | ) |
|
448 | 448 | selection.add_argument( |
|
449 | 449 | "-k", "--keywords", help="run tests matching keywords" |
|
450 | 450 | ) |
|
451 | 451 | selection.add_argument( |
|
452 | 452 | "-r", "--retest", action="store_true", help="retest failed tests" |
|
453 | 453 | ) |
|
454 | 454 | selection.add_argument( |
|
455 | 455 | "--test-list", |
|
456 | 456 | action="append", |
|
457 | 457 | help="read tests to run from the specified file", |
|
458 | 458 | ) |
|
459 | 459 | selection.add_argument( |
|
460 | 460 | "--whitelist", |
|
461 | 461 | action="append", |
|
462 | 462 | help="always run tests listed in the specified whitelist file", |
|
463 | 463 | ) |
|
464 | 464 | selection.add_argument( |
|
465 | 465 | 'tests', metavar='TESTS', nargs='*', help='Tests to run' |
|
466 | 466 | ) |
|
467 | 467 | |
|
468 | 468 | harness = parser.add_argument_group('Test Harness Behavior') |
|
469 | 469 | harness.add_argument( |
|
470 | 470 | '--bisect-repo', |
|
471 | 471 | metavar='bisect_repo', |
|
472 | 472 | help=( |
|
473 | 473 | "Path of a repo to bisect. Use together with " "--known-good-rev" |
|
474 | 474 | ), |
|
475 | 475 | ) |
|
476 | 476 | harness.add_argument( |
|
477 | 477 | "-d", |
|
478 | 478 | "--debug", |
|
479 | 479 | action="store_true", |
|
480 | 480 | help="debug mode: write output of test scripts to console" |
|
481 | 481 | " rather than capturing and diffing it (disables timeout)", |
|
482 | 482 | ) |
|
483 | 483 | harness.add_argument( |
|
484 | 484 | "-f", |
|
485 | 485 | "--first", |
|
486 | 486 | action="store_true", |
|
487 | 487 | help="exit on the first test failure", |
|
488 | 488 | ) |
|
489 | 489 | harness.add_argument( |
|
490 | 490 | "-i", |
|
491 | 491 | "--interactive", |
|
492 | 492 | action="store_true", |
|
493 | 493 | help="prompt to accept changed output", |
|
494 | 494 | ) |
|
495 | 495 | harness.add_argument( |
|
496 | 496 | "-j", |
|
497 | 497 | "--jobs", |
|
498 | 498 | type=int, |
|
499 | 499 | help="number of jobs to run in parallel" |
|
500 | 500 | " (default: $%s or %d)" % defaults['jobs'], |
|
501 | 501 | ) |
|
502 | 502 | harness.add_argument( |
|
503 | 503 | "--keep-tmpdir", |
|
504 | 504 | action="store_true", |
|
505 | 505 | help="keep temporary directory after running tests", |
|
506 | 506 | ) |
|
507 | 507 | harness.add_argument( |
|
508 | 508 | '--known-good-rev', |
|
509 | 509 | metavar="known_good_rev", |
|
510 | 510 | help=( |
|
511 | 511 | "Automatically bisect any failures using this " |
|
512 | 512 | "revision as a known-good revision." |
|
513 | 513 | ), |
|
514 | 514 | ) |
|
515 | 515 | harness.add_argument( |
|
516 | 516 | "--list-tests", |
|
517 | 517 | action="store_true", |
|
518 | 518 | help="list tests instead of running them", |
|
519 | 519 | ) |
|
520 | 520 | harness.add_argument( |
|
521 | 521 | "--loop", action="store_true", help="loop tests repeatedly" |
|
522 | 522 | ) |
|
523 | 523 | harness.add_argument( |
|
524 | 524 | '--random', action="store_true", help='run tests in random order' |
|
525 | 525 | ) |
|
526 | 526 | harness.add_argument( |
|
527 | 527 | '--order-by-runtime', |
|
528 | 528 | action="store_true", |
|
529 | 529 | help='run slowest tests first, according to .testtimes', |
|
530 | 530 | ) |
|
531 | 531 | harness.add_argument( |
|
532 | 532 | "-p", |
|
533 | 533 | "--port", |
|
534 | 534 | type=int, |
|
535 | 535 | help="port on which servers should listen" |
|
536 | 536 | " (default: $%s or %d)" % defaults['port'], |
|
537 | 537 | ) |
|
538 | 538 | harness.add_argument( |
|
539 | 539 | '--profile-runner', |
|
540 | 540 | action='store_true', |
|
541 | 541 | help='run statprof on run-tests', |
|
542 | 542 | ) |
|
543 | 543 | harness.add_argument( |
|
544 | 544 | "-R", "--restart", action="store_true", help="restart at last error" |
|
545 | 545 | ) |
|
546 | 546 | harness.add_argument( |
|
547 | 547 | "--runs-per-test", |
|
548 | 548 | type=int, |
|
549 | 549 | dest="runs_per_test", |
|
550 | 550 | help="run each test N times (default=1)", |
|
551 | 551 | default=1, |
|
552 | 552 | ) |
|
553 | 553 | harness.add_argument( |
|
554 | 554 | "--shell", help="shell to use (default: $%s or %s)" % defaults['shell'] |
|
555 | 555 | ) |
|
556 | 556 | harness.add_argument( |
|
557 | 557 | '--showchannels', action='store_true', help='show scheduling channels' |
|
558 | 558 | ) |
|
559 | 559 | harness.add_argument( |
|
560 | 560 | "--slowtimeout", |
|
561 | 561 | type=int, |
|
562 | 562 | help="kill errant slow tests after SLOWTIMEOUT seconds" |
|
563 | 563 | " (default: $%s or %d)" % defaults['slowtimeout'], |
|
564 | 564 | ) |
|
565 | 565 | harness.add_argument( |
|
566 | 566 | "-t", |
|
567 | 567 | "--timeout", |
|
568 | 568 | type=int, |
|
569 | 569 | help="kill errant tests after TIMEOUT seconds" |
|
570 | 570 | " (default: $%s or %d)" % defaults['timeout'], |
|
571 | 571 | ) |
|
572 | 572 | harness.add_argument( |
|
573 | 573 | "--tmpdir", |
|
574 | 574 | help="run tests in the given temporary directory" |
|
575 | 575 | " (implies --keep-tmpdir)", |
|
576 | 576 | ) |
|
577 | 577 | harness.add_argument( |
|
578 | 578 | "-v", "--verbose", action="store_true", help="output verbose messages" |
|
579 | 579 | ) |
|
580 | 580 | |
|
581 | 581 | hgconf = parser.add_argument_group('Mercurial Configuration') |
|
582 | 582 | hgconf.add_argument( |
|
583 | 583 | "--chg", |
|
584 | 584 | action="store_true", |
|
585 | 585 | help="install and use chg wrapper in place of hg", |
|
586 | 586 | ) |
|
587 | 587 | hgconf.add_argument( |
|
588 | 588 | "--chg-debug", |
|
589 | 589 | action="store_true", |
|
590 | 590 | help="show chg debug logs", |
|
591 | 591 | ) |
|
592 | 592 | hgconf.add_argument( |
|
593 | 593 | "--rhg", |
|
594 | 594 | action="store_true", |
|
595 | 595 | help="install and use rhg Rust implementation in place of hg", |
|
596 | 596 | ) |
|
597 | 597 | hgconf.add_argument( |
|
598 | 598 | "--pyoxidized", |
|
599 | 599 | action="store_true", |
|
600 | 600 | help="build the hg binary using pyoxidizer", |
|
601 | 601 | ) |
|
602 | 602 | hgconf.add_argument("--compiler", help="compiler to build with") |
|
603 | 603 | hgconf.add_argument( |
|
604 | 604 | '--extra-config-opt', |
|
605 | 605 | action="append", |
|
606 | 606 | default=[], |
|
607 | 607 | help='set the given config opt in the test hgrc', |
|
608 | 608 | ) |
|
609 | 609 | hgconf.add_argument( |
|
610 | 610 | "-l", |
|
611 | 611 | "--local", |
|
612 | 612 | action="store_true", |
|
613 | 613 | help="shortcut for --with-hg=<testdir>/../hg, " |
|
614 | 614 | "--with-rhg=<testdir>/../rust/target/release/rhg if --rhg is set, " |
|
615 | 615 | "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set", |
|
616 | 616 | ) |
|
617 | 617 | hgconf.add_argument( |
|
618 | 618 | "--ipv6", |
|
619 | 619 | action="store_true", |
|
620 | 620 | help="prefer IPv6 to IPv4 for network related tests", |
|
621 | 621 | ) |
|
622 | 622 | hgconf.add_argument( |
|
623 | 623 | "--pure", |
|
624 | 624 | action="store_true", |
|
625 | 625 | help="use pure Python code instead of C extensions", |
|
626 | 626 | ) |
|
627 | 627 | hgconf.add_argument( |
|
628 | 628 | "--rust", |
|
629 | 629 | action="store_true", |
|
630 | 630 | help="use Rust code alongside C extensions", |
|
631 | 631 | ) |
|
632 | 632 | hgconf.add_argument( |
|
633 | 633 | "--no-rust", |
|
634 | 634 | action="store_true", |
|
635 | 635 | help="do not use Rust code even if compiled", |
|
636 | 636 | ) |
|
637 | 637 | hgconf.add_argument( |
|
638 | 638 | "--with-chg", |
|
639 | 639 | metavar="CHG", |
|
640 | 640 | help="use specified chg wrapper in place of hg", |
|
641 | 641 | ) |
|
642 | 642 | hgconf.add_argument( |
|
643 | 643 | "--with-rhg", |
|
644 | 644 | metavar="RHG", |
|
645 | 645 | help="use specified rhg Rust implementation in place of hg", |
|
646 | 646 | ) |
|
647 | 647 | hgconf.add_argument( |
|
648 | 648 | "--with-hg", |
|
649 | 649 | metavar="HG", |
|
650 | 650 | help="test using specified hg script rather than a " |
|
651 | 651 | "temporary installation", |
|
652 | 652 | ) |
|
653 | 653 | |
|
654 | 654 | reporting = parser.add_argument_group('Results Reporting') |
|
655 | 655 | reporting.add_argument( |
|
656 | 656 | "-C", |
|
657 | 657 | "--annotate", |
|
658 | 658 | action="store_true", |
|
659 | 659 | help="output files annotated with coverage", |
|
660 | 660 | ) |
|
661 | 661 | reporting.add_argument( |
|
662 | 662 | "--color", |
|
663 | 663 | choices=["always", "auto", "never"], |
|
664 | 664 | default=os.environ.get('HGRUNTESTSCOLOR', 'auto'), |
|
665 | 665 | help="colorisation: always|auto|never (default: auto)", |
|
666 | 666 | ) |
|
667 | 667 | reporting.add_argument( |
|
668 | 668 | "-c", |
|
669 | 669 | "--cover", |
|
670 | 670 | action="store_true", |
|
671 | 671 | help="print a test coverage report", |
|
672 | 672 | ) |
|
673 | 673 | reporting.add_argument( |
|
674 | 674 | '--exceptions', |
|
675 | 675 | action='store_true', |
|
676 | 676 | help='log all exceptions and generate an exception report', |
|
677 | 677 | ) |
|
678 | 678 | reporting.add_argument( |
|
679 | 679 | "-H", |
|
680 | 680 | "--htmlcov", |
|
681 | 681 | action="store_true", |
|
682 | 682 | help="create an HTML report of the coverage of the files", |
|
683 | 683 | ) |
|
684 | 684 | reporting.add_argument( |
|
685 | 685 | "--json", |
|
686 | 686 | action="store_true", |
|
687 | 687 | help="store test result data in 'report.json' file", |
|
688 | 688 | ) |
|
689 | 689 | reporting.add_argument( |
|
690 | 690 | "--outputdir", |
|
691 | 691 | help="directory to write error logs to (default=test directory)", |
|
692 | 692 | ) |
|
693 | 693 | reporting.add_argument( |
|
694 | 694 | "-n", "--nodiff", action="store_true", help="skip showing test changes" |
|
695 | 695 | ) |
|
696 | 696 | reporting.add_argument( |
|
697 | 697 | "-S", |
|
698 | 698 | "--noskips", |
|
699 | 699 | action="store_true", |
|
700 | 700 | help="don't report skip tests verbosely", |
|
701 | 701 | ) |
|
702 | 702 | reporting.add_argument( |
|
703 | 703 | "--time", action="store_true", help="time how long each test takes" |
|
704 | 704 | ) |
|
705 | 705 | reporting.add_argument("--view", help="external diff viewer") |
|
706 | 706 | reporting.add_argument( |
|
707 | 707 | "--xunit", help="record xunit results at specified path" |
|
708 | 708 | ) |
|
709 | 709 | |
|
710 | 710 | for option, (envvar, default) in defaults.items(): |
|
711 | 711 | defaults[option] = type(default)(os.environ.get(envvar, default)) |
|
712 | 712 | parser.set_defaults(**defaults) |
|
713 | 713 | |
|
714 | 714 | return parser |
|
715 | 715 | |
|
716 | 716 | |
|
717 | 717 | def parseargs(args, parser): |
|
718 | 718 | """Parse arguments with our OptionParser and validate results.""" |
|
719 | 719 | options = parser.parse_args(args) |
|
720 | 720 | |
|
721 | 721 | # jython is always pure |
|
722 | 722 | if 'java' in sys.platform or '__pypy__' in sys.modules: |
|
723 | 723 | options.pure = True |
|
724 | 724 | |
|
725 | 725 | if platform.python_implementation() != 'CPython' and options.rust: |
|
726 | 726 | parser.error('Rust extensions are only available with CPython') |
|
727 | 727 | |
|
728 | 728 | if options.pure and options.rust: |
|
729 | 729 | parser.error('--rust cannot be used with --pure') |
|
730 | 730 | |
|
731 | 731 | if options.rust and options.no_rust: |
|
732 | 732 | parser.error('--rust cannot be used with --no-rust') |
|
733 | 733 | |
|
734 | 734 | if options.local: |
|
735 | 735 | if options.with_hg or options.with_rhg or options.with_chg: |
|
736 | 736 | parser.error( |
|
737 | 737 | '--local cannot be used with --with-hg or --with-rhg or --with-chg' |
|
738 | 738 | ) |
|
739 | 739 | if options.pyoxidized: |
|
740 | 740 | parser.error('--pyoxidized does not work with --local (yet)') |
|
741 | 741 | testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0]))) |
|
742 | 742 | reporootdir = os.path.dirname(testdir) |
|
743 | 743 | pathandattrs = [(b'hg', 'with_hg')] |
|
744 | 744 | if options.chg: |
|
745 | 745 | pathandattrs.append((b'contrib/chg/chg', 'with_chg')) |
|
746 | 746 | if options.rhg: |
|
747 | 747 | pathandattrs.append((b'rust/target/release/rhg', 'with_rhg')) |
|
748 | 748 | for relpath, attr in pathandattrs: |
|
749 | 749 | binpath = os.path.join(reporootdir, relpath) |
|
750 | 750 | if not (WINDOWS or os.access(binpath, os.X_OK)): |
|
751 | 751 | parser.error( |
|
752 | 752 | '--local specified, but %r not found or ' |
|
753 | 753 | 'not executable' % binpath |
|
754 | 754 | ) |
|
755 | 755 | setattr(options, attr, _bytes2sys(binpath)) |
|
756 | 756 | |
|
757 | 757 | if options.with_hg: |
|
758 | 758 | options.with_hg = canonpath(_sys2bytes(options.with_hg)) |
|
759 | 759 | if not ( |
|
760 | 760 | os.path.isfile(options.with_hg) |
|
761 | 761 | and os.access(options.with_hg, os.X_OK) |
|
762 | 762 | ): |
|
763 | 763 | parser.error('--with-hg must specify an executable hg script') |
|
764 | 764 | if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']: |
|
765 | 765 | msg = 'warning: --with-hg should specify an hg script, not: %s\n' |
|
766 | 766 | msg %= _bytes2sys(os.path.basename(options.with_hg)) |
|
767 | 767 | sys.stderr.write(msg) |
|
768 | 768 | sys.stderr.flush() |
|
769 | 769 | |
|
770 | 770 | if (options.chg or options.with_chg) and WINDOWS: |
|
771 | 771 | parser.error('chg does not work on %s' % os.name) |
|
772 | 772 | if (options.rhg or options.with_rhg) and WINDOWS: |
|
773 | 773 | parser.error('rhg does not work on %s' % os.name) |
|
774 | 774 | if options.pyoxidized and not WINDOWS: |
|
775 | 775 | parser.error('--pyoxidized is currently Windows only') |
|
776 | 776 | if options.with_chg: |
|
777 | 777 | options.chg = False # no installation to temporary location |
|
778 | 778 | options.with_chg = canonpath(_sys2bytes(options.with_chg)) |
|
779 | 779 | if not ( |
|
780 | 780 | os.path.isfile(options.with_chg) |
|
781 | 781 | and os.access(options.with_chg, os.X_OK) |
|
782 | 782 | ): |
|
783 | 783 | parser.error('--with-chg must specify a chg executable') |
|
784 | 784 | if options.with_rhg: |
|
785 | 785 | options.rhg = False # no installation to temporary location |
|
786 | 786 | options.with_rhg = canonpath(_sys2bytes(options.with_rhg)) |
|
787 | 787 | if not ( |
|
788 | 788 | os.path.isfile(options.with_rhg) |
|
789 | 789 | and os.access(options.with_rhg, os.X_OK) |
|
790 | 790 | ): |
|
791 | 791 | parser.error('--with-rhg must specify a rhg executable') |
|
792 | 792 | if options.chg and options.with_hg: |
|
793 | 793 | # chg shares installation location with hg |
|
794 | 794 | parser.error( |
|
795 | 795 | '--chg does not work when --with-hg is specified ' |
|
796 | 796 | '(use --with-chg instead)' |
|
797 | 797 | ) |
|
798 | 798 | if options.rhg and options.with_hg: |
|
799 | 799 | # rhg shares installation location with hg |
|
800 | 800 | parser.error( |
|
801 | 801 | '--rhg does not work when --with-hg is specified ' |
|
802 | 802 | '(use --with-rhg instead)' |
|
803 | 803 | ) |
|
804 | 804 | if options.rhg and options.chg: |
|
805 | 805 | parser.error('--rhg and --chg do not work together') |
|
806 | 806 | |
|
807 | 807 | if options.color == 'always' and not pygmentspresent: |
|
808 | 808 | sys.stderr.write( |
|
809 | 809 | 'warning: --color=always ignored because ' |
|
810 | 810 | 'pygments is not installed\n' |
|
811 | 811 | ) |
|
812 | 812 | |
|
813 | 813 | if options.bisect_repo and not options.known_good_rev: |
|
814 | 814 | parser.error("--bisect-repo cannot be used without --known-good-rev") |
|
815 | 815 | |
|
816 | 816 | global useipv6 |
|
817 | 817 | if options.ipv6: |
|
818 | 818 | useipv6 = checksocketfamily('AF_INET6') |
|
819 | 819 | else: |
|
820 | 820 | # only use IPv6 if IPv4 is unavailable and IPv6 is available |
|
821 | 821 | useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily( |
|
822 | 822 | 'AF_INET6' |
|
823 | 823 | ) |
|
824 | 824 | |
|
825 | 825 | options.anycoverage = options.cover or options.annotate or options.htmlcov |
|
826 | 826 | if options.anycoverage: |
|
827 | 827 | try: |
|
828 | 828 | import coverage |
|
829 | 829 | |
|
830 | 830 | covver = version.StrictVersion(coverage.__version__).version |
|
831 | 831 | if covver < (3, 3): |
|
832 | 832 | parser.error('coverage options require coverage 3.3 or later') |
|
833 | 833 | except ImportError: |
|
834 | 834 | parser.error('coverage options now require the coverage package') |
|
835 | 835 | |
|
836 | 836 | if options.anycoverage and options.local: |
|
837 | 837 | # this needs some path mangling somewhere, I guess |
|
838 | 838 | parser.error( |
|
839 | 839 | "sorry, coverage options do not work when --local " "is specified" |
|
840 | 840 | ) |
|
841 | 841 | |
|
842 | 842 | if options.anycoverage and options.with_hg: |
|
843 | 843 | parser.error( |
|
844 | 844 | "sorry, coverage options do not work when --with-hg " "is specified" |
|
845 | 845 | ) |
|
846 | 846 | |
|
847 | 847 | global verbose |
|
848 | 848 | if options.verbose: |
|
849 | 849 | verbose = '' |
|
850 | 850 | |
|
851 | 851 | if options.tmpdir: |
|
852 | 852 | options.tmpdir = canonpath(options.tmpdir) |
|
853 | 853 | |
|
854 | 854 | if options.jobs < 1: |
|
855 | 855 | parser.error('--jobs must be positive') |
|
856 | 856 | if options.interactive and options.debug: |
|
857 | 857 | parser.error("-i/--interactive and -d/--debug are incompatible") |
|
858 | 858 | if options.debug: |
|
859 | 859 | if options.timeout != defaults['timeout']: |
|
860 | 860 | sys.stderr.write('warning: --timeout option ignored with --debug\n') |
|
861 | 861 | if options.slowtimeout != defaults['slowtimeout']: |
|
862 | 862 | sys.stderr.write( |
|
863 | 863 | 'warning: --slowtimeout option ignored with --debug\n' |
|
864 | 864 | ) |
|
865 | 865 | options.timeout = 0 |
|
866 | 866 | options.slowtimeout = 0 |
|
867 | 867 | |
|
868 | 868 | if options.blacklist: |
|
869 | 869 | options.blacklist = parselistfiles(options.blacklist, 'blacklist') |
|
870 | 870 | if options.whitelist: |
|
871 | 871 | options.whitelisted = parselistfiles(options.whitelist, 'whitelist') |
|
872 | 872 | else: |
|
873 | 873 | options.whitelisted = {} |
|
874 | 874 | |
|
875 | 875 | if options.showchannels: |
|
876 | 876 | options.nodiff = True |
|
877 | 877 | |
|
878 | 878 | return options |
|
879 | 879 | |
|
880 | 880 | |
|
881 | 881 | def rename(src, dst): |
|
882 | 882 | """Like os.rename(), trade atomicity and opened files friendliness |
|
883 | 883 | for existing destination support. |
|
884 | 884 | """ |
|
885 | 885 | shutil.copy(src, dst) |
|
886 | 886 | os.remove(src) |
|
887 | 887 | |
|
888 | 888 | |
|
889 | 889 | def makecleanable(path): |
|
890 | 890 | """Try to fix directory permission recursively so that the entire tree |
|
891 | 891 | can be deleted""" |
|
892 | 892 | for dirpath, dirnames, _filenames in os.walk(path, topdown=True): |
|
893 | 893 | for d in dirnames: |
|
894 | 894 | p = os.path.join(dirpath, d) |
|
895 | 895 | try: |
|
896 | 896 | os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx |
|
897 | 897 | except OSError: |
|
898 | 898 | pass |
|
899 | 899 | |
|
900 | 900 | |
|
901 | 901 | _unified_diff = difflib.unified_diff |
|
902 | 902 | if PYTHON3: |
|
903 | 903 | import functools |
|
904 | 904 | |
|
905 | 905 | _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff) |
|
906 | 906 | |
|
907 | 907 | |
|
908 | 908 | def getdiff(expected, output, ref, err): |
|
909 | 909 | servefail = False |
|
910 | 910 | lines = [] |
|
911 | 911 | for line in _unified_diff(expected, output, ref, err): |
|
912 | 912 | if line.startswith(b'+++') or line.startswith(b'---'): |
|
913 | 913 | line = line.replace(b'\\', b'/') |
|
914 | 914 | if line.endswith(b' \n'): |
|
915 | 915 | line = line[:-2] + b'\n' |
|
916 | 916 | lines.append(line) |
|
917 | 917 | if not servefail and line.startswith( |
|
918 | 918 | b'+ abort: child process failed to start' |
|
919 | 919 | ): |
|
920 | 920 | servefail = True |
|
921 | 921 | |
|
922 | 922 | return servefail, lines |
|
923 | 923 | |
|
924 | 924 | |
|
925 | 925 | verbose = False |
|
926 | 926 | |
|
927 | 927 | |
|
928 | 928 | def vlog(*msg): |
|
929 | 929 | """Log only when in verbose mode.""" |
|
930 | 930 | if verbose is False: |
|
931 | 931 | return |
|
932 | 932 | |
|
933 | 933 | return log(*msg) |
|
934 | 934 | |
|
935 | 935 | |
|
936 | 936 | # Bytes that break XML even in a CDATA block: control characters 0-31 |
|
937 | 937 | # sans \t, \n and \r |
|
938 | 938 | CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]") |
|
939 | 939 | |
|
940 | 940 | # Match feature conditionalized output lines in the form, capturing the feature |
|
941 | 941 | # list in group 2, and the preceeding line output in group 1: |
|
942 | 942 | # |
|
943 | 943 | # output..output (feature !)\n |
|
944 | 944 | optline = re.compile(br'(.*) \((.+?) !\)\n$') |
|
945 | 945 | |
|
946 | 946 | |
|
947 | 947 | def cdatasafe(data): |
|
948 | 948 | """Make a string safe to include in a CDATA block. |
|
949 | 949 | |
|
950 | 950 | Certain control characters are illegal in a CDATA block, and |
|
951 | 951 | there's no way to include a ]]> in a CDATA either. This function |
|
952 | 952 | replaces illegal bytes with ? and adds a space between the ]] so |
|
953 | 953 | that it won't break the CDATA block. |
|
954 | 954 | """ |
|
955 | 955 | return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>') |
|
956 | 956 | |
|
957 | 957 | |
|
958 | 958 | def log(*msg): |
|
959 | 959 | """Log something to stdout. |
|
960 | 960 | |
|
961 | 961 | Arguments are strings to print. |
|
962 | 962 | """ |
|
963 | 963 | with iolock: |
|
964 | 964 | if verbose: |
|
965 | 965 | print(verbose, end=' ') |
|
966 | 966 | for m in msg: |
|
967 | 967 | print(m, end=' ') |
|
968 | 968 | print() |
|
969 | 969 | sys.stdout.flush() |
|
970 | 970 | |
|
971 | 971 | |
|
972 | 972 | def highlightdiff(line, color): |
|
973 | 973 | if not color: |
|
974 | 974 | return line |
|
975 | 975 | assert pygmentspresent |
|
976 | 976 | return pygments.highlight( |
|
977 | 977 | line.decode('latin1'), difflexer, terminal256formatter |
|
978 | 978 | ).encode('latin1') |
|
979 | 979 | |
|
980 | 980 | |
|
981 | 981 | def highlightmsg(msg, color): |
|
982 | 982 | if not color: |
|
983 | 983 | return msg |
|
984 | 984 | assert pygmentspresent |
|
985 | 985 | return pygments.highlight(msg, runnerlexer, runnerformatter) |
|
986 | 986 | |
|
987 | 987 | |
|
988 | 988 | def terminate(proc): |
|
989 | 989 | """Terminate subprocess""" |
|
990 | 990 | vlog('# Terminating process %d' % proc.pid) |
|
991 | 991 | try: |
|
992 | 992 | proc.terminate() |
|
993 | 993 | except OSError: |
|
994 | 994 | pass |
|
995 | 995 | |
|
996 | 996 | |
|
997 | 997 | def killdaemons(pidfile): |
|
998 | 998 | import killdaemons as killmod |
|
999 | 999 | |
|
1000 | 1000 | return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog) |
|
1001 | 1001 | |
|
1002 | 1002 | |
|
1003 | 1003 | class Test(unittest.TestCase): |
|
1004 | 1004 | """Encapsulates a single, runnable test. |
|
1005 | 1005 | |
|
1006 | 1006 | While this class conforms to the unittest.TestCase API, it differs in that |
|
1007 | 1007 | instances need to be instantiated manually. (Typically, unittest.TestCase |
|
1008 | 1008 | classes are instantiated automatically by scanning modules.) |
|
1009 | 1009 | """ |
|
1010 | 1010 | |
|
1011 | 1011 | # Status code reserved for skipped tests (used by hghave). |
|
1012 | 1012 | SKIPPED_STATUS = 80 |
|
1013 | 1013 | |
|
1014 | 1014 | def __init__( |
|
1015 | 1015 | self, |
|
1016 | 1016 | path, |
|
1017 | 1017 | outputdir, |
|
1018 | 1018 | tmpdir, |
|
1019 | 1019 | keeptmpdir=False, |
|
1020 | 1020 | debug=False, |
|
1021 | 1021 | first=False, |
|
1022 | 1022 | timeout=None, |
|
1023 | 1023 | startport=None, |
|
1024 | 1024 | extraconfigopts=None, |
|
1025 | 1025 | shell=None, |
|
1026 | 1026 | hgcommand=None, |
|
1027 | 1027 | slowtimeout=None, |
|
1028 | 1028 | usechg=False, |
|
1029 | 1029 | chgdebug=False, |
|
1030 | 1030 | useipv6=False, |
|
1031 | 1031 | ): |
|
1032 | 1032 | """Create a test from parameters. |
|
1033 | 1033 | |
|
1034 | 1034 | path is the full path to the file defining the test. |
|
1035 | 1035 | |
|
1036 | 1036 | tmpdir is the main temporary directory to use for this test. |
|
1037 | 1037 | |
|
1038 | 1038 | keeptmpdir determines whether to keep the test's temporary directory |
|
1039 | 1039 | after execution. It defaults to removal (False). |
|
1040 | 1040 | |
|
1041 | 1041 | debug mode will make the test execute verbosely, with unfiltered |
|
1042 | 1042 | output. |
|
1043 | 1043 | |
|
1044 | 1044 | timeout controls the maximum run time of the test. It is ignored when |
|
1045 | 1045 | debug is True. See slowtimeout for tests with #require slow. |
|
1046 | 1046 | |
|
1047 | 1047 | slowtimeout overrides timeout if the test has #require slow. |
|
1048 | 1048 | |
|
1049 | 1049 | startport controls the starting port number to use for this test. Each |
|
1050 | 1050 | test will reserve 3 port numbers for execution. It is the caller's |
|
1051 | 1051 | responsibility to allocate a non-overlapping port range to Test |
|
1052 | 1052 | instances. |
|
1053 | 1053 | |
|
1054 | 1054 | extraconfigopts is an iterable of extra hgrc config options. Values |
|
1055 | 1055 | must have the form "key=value" (something understood by hgrc). Values |
|
1056 | 1056 | of the form "foo.key=value" will result in "[foo] key=value". |
|
1057 | 1057 | |
|
1058 | 1058 | shell is the shell to execute tests in. |
|
1059 | 1059 | """ |
|
1060 | 1060 | if timeout is None: |
|
1061 | 1061 | timeout = defaults['timeout'] |
|
1062 | 1062 | if startport is None: |
|
1063 | 1063 | startport = defaults['port'] |
|
1064 | 1064 | if slowtimeout is None: |
|
1065 | 1065 | slowtimeout = defaults['slowtimeout'] |
|
1066 | 1066 | self.path = path |
|
1067 | 1067 | self.relpath = os.path.relpath(path) |
|
1068 | 1068 | self.bname = os.path.basename(path) |
|
1069 | 1069 | self.name = _bytes2sys(self.bname) |
|
1070 | 1070 | self._testdir = os.path.dirname(path) |
|
1071 | 1071 | self._outputdir = outputdir |
|
1072 | 1072 | self._tmpname = os.path.basename(path) |
|
1073 | 1073 | self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname) |
|
1074 | 1074 | |
|
1075 | 1075 | self._threadtmp = tmpdir |
|
1076 | 1076 | self._keeptmpdir = keeptmpdir |
|
1077 | 1077 | self._debug = debug |
|
1078 | 1078 | self._first = first |
|
1079 | 1079 | self._timeout = timeout |
|
1080 | 1080 | self._slowtimeout = slowtimeout |
|
1081 | 1081 | self._startport = startport |
|
1082 | 1082 | self._extraconfigopts = extraconfigopts or [] |
|
1083 | 1083 | self._shell = _sys2bytes(shell) |
|
1084 | 1084 | self._hgcommand = hgcommand or b'hg' |
|
1085 | 1085 | self._usechg = usechg |
|
1086 | 1086 | self._chgdebug = chgdebug |
|
1087 | 1087 | self._useipv6 = useipv6 |
|
1088 | 1088 | |
|
1089 | 1089 | self._aborted = False |
|
1090 | 1090 | self._daemonpids = [] |
|
1091 | 1091 | self._finished = None |
|
1092 | 1092 | self._ret = None |
|
1093 | 1093 | self._out = None |
|
1094 | 1094 | self._skipped = None |
|
1095 | 1095 | self._testtmp = None |
|
1096 | 1096 | self._chgsockdir = None |
|
1097 | 1097 | |
|
1098 | 1098 | self._refout = self.readrefout() |
|
1099 | 1099 | |
|
1100 | 1100 | def readrefout(self): |
|
1101 | 1101 | """read reference output""" |
|
1102 | 1102 | # If we're not in --debug mode and reference output file exists, |
|
1103 | 1103 | # check test output against it. |
|
1104 | 1104 | if self._debug: |
|
1105 | 1105 | return None # to match "out is None" |
|
1106 | 1106 | elif os.path.exists(self.refpath): |
|
1107 | 1107 | with open(self.refpath, 'rb') as f: |
|
1108 | 1108 | return f.read().splitlines(True) |
|
1109 | 1109 | else: |
|
1110 | 1110 | return [] |
|
1111 | 1111 | |
|
1112 | 1112 | # needed to get base class __repr__ running |
|
1113 | 1113 | @property |
|
1114 | 1114 | def _testMethodName(self): |
|
1115 | 1115 | return self.name |
|
1116 | 1116 | |
|
1117 | 1117 | def __str__(self): |
|
1118 | 1118 | return self.name |
|
1119 | 1119 | |
|
1120 | 1120 | def shortDescription(self): |
|
1121 | 1121 | return self.name |
|
1122 | 1122 | |
|
1123 | 1123 | def setUp(self): |
|
1124 | 1124 | """Tasks to perform before run().""" |
|
1125 | 1125 | self._finished = False |
|
1126 | 1126 | self._ret = None |
|
1127 | 1127 | self._out = None |
|
1128 | 1128 | self._skipped = None |
|
1129 | 1129 | |
|
1130 | 1130 | try: |
|
1131 | 1131 | os.mkdir(self._threadtmp) |
|
1132 | 1132 | except OSError as e: |
|
1133 | 1133 | if e.errno != errno.EEXIST: |
|
1134 | 1134 | raise |
|
1135 | 1135 | |
|
1136 | 1136 | name = self._tmpname |
|
1137 | 1137 | self._testtmp = os.path.join(self._threadtmp, name) |
|
1138 | 1138 | os.mkdir(self._testtmp) |
|
1139 | 1139 | |
|
1140 | 1140 | # Remove any previous output files. |
|
1141 | 1141 | if os.path.exists(self.errpath): |
|
1142 | 1142 | try: |
|
1143 | 1143 | os.remove(self.errpath) |
|
1144 | 1144 | except OSError as e: |
|
1145 | 1145 | # We might have raced another test to clean up a .err |
|
1146 | 1146 | # file, so ignore ENOENT when removing a previous .err |
|
1147 | 1147 | # file. |
|
1148 | 1148 | if e.errno != errno.ENOENT: |
|
1149 | 1149 | raise |
|
1150 | 1150 | |
|
1151 | 1151 | if self._usechg: |
|
1152 | 1152 | self._chgsockdir = os.path.join( |
|
1153 | 1153 | self._threadtmp, b'%s.chgsock' % name |
|
1154 | 1154 | ) |
|
1155 | 1155 | os.mkdir(self._chgsockdir) |
|
1156 | 1156 | |
|
1157 | 1157 | def run(self, result): |
|
1158 | 1158 | """Run this test and report results against a TestResult instance.""" |
|
1159 | 1159 | # This function is extremely similar to unittest.TestCase.run(). Once |
|
1160 | 1160 | # we require Python 2.7 (or at least its version of unittest), this |
|
1161 | 1161 | # function can largely go away. |
|
1162 | 1162 | self._result = result |
|
1163 | 1163 | result.startTest(self) |
|
1164 | 1164 | try: |
|
1165 | 1165 | try: |
|
1166 | 1166 | self.setUp() |
|
1167 | 1167 | except (KeyboardInterrupt, SystemExit): |
|
1168 | 1168 | self._aborted = True |
|
1169 | 1169 | raise |
|
1170 | 1170 | except Exception: |
|
1171 | 1171 | result.addError(self, sys.exc_info()) |
|
1172 | 1172 | return |
|
1173 | 1173 | |
|
1174 | 1174 | success = False |
|
1175 | 1175 | try: |
|
1176 | 1176 | self.runTest() |
|
1177 | 1177 | except KeyboardInterrupt: |
|
1178 | 1178 | self._aborted = True |
|
1179 | 1179 | raise |
|
1180 | 1180 | except unittest.SkipTest as e: |
|
1181 | 1181 | result.addSkip(self, str(e)) |
|
1182 | 1182 | # The base class will have already counted this as a |
|
1183 | 1183 | # test we "ran", but we want to exclude skipped tests |
|
1184 | 1184 | # from those we count towards those run. |
|
1185 | 1185 | result.testsRun -= 1 |
|
1186 | 1186 | except self.failureException as e: |
|
1187 | 1187 | # This differs from unittest in that we don't capture |
|
1188 | 1188 | # the stack trace. This is for historical reasons and |
|
1189 | 1189 | # this decision could be revisited in the future, |
|
1190 | 1190 | # especially for PythonTest instances. |
|
1191 | 1191 | if result.addFailure(self, str(e)): |
|
1192 | 1192 | success = True |
|
1193 | 1193 | except Exception: |
|
1194 | 1194 | result.addError(self, sys.exc_info()) |
|
1195 | 1195 | else: |
|
1196 | 1196 | success = True |
|
1197 | 1197 | |
|
1198 | 1198 | try: |
|
1199 | 1199 | self.tearDown() |
|
1200 | 1200 | except (KeyboardInterrupt, SystemExit): |
|
1201 | 1201 | self._aborted = True |
|
1202 | 1202 | raise |
|
1203 | 1203 | except Exception: |
|
1204 | 1204 | result.addError(self, sys.exc_info()) |
|
1205 | 1205 | success = False |
|
1206 | 1206 | |
|
1207 | 1207 | if success: |
|
1208 | 1208 | result.addSuccess(self) |
|
1209 | 1209 | finally: |
|
1210 | 1210 | result.stopTest(self, interrupted=self._aborted) |
|
1211 | 1211 | |
|
1212 | 1212 | def runTest(self): |
|
1213 | 1213 | """Run this test instance. |
|
1214 | 1214 | |
|
1215 | 1215 | This will return a tuple describing the result of the test. |
|
1216 | 1216 | """ |
|
1217 | 1217 | env = self._getenv() |
|
1218 | 1218 | self._genrestoreenv(env) |
|
1219 | 1219 | self._daemonpids.append(env['DAEMON_PIDS']) |
|
1220 | 1220 | self._createhgrc(env['HGRCPATH']) |
|
1221 | 1221 | |
|
1222 | 1222 | vlog('# Test', self.name) |
|
1223 | 1223 | |
|
1224 | 1224 | ret, out = self._run(env) |
|
1225 | 1225 | self._finished = True |
|
1226 | 1226 | self._ret = ret |
|
1227 | 1227 | self._out = out |
|
1228 | 1228 | |
|
1229 | 1229 | def describe(ret): |
|
1230 | 1230 | if ret < 0: |
|
1231 | 1231 | return 'killed by signal: %d' % -ret |
|
1232 | 1232 | return 'returned error code %d' % ret |
|
1233 | 1233 | |
|
1234 | 1234 | self._skipped = False |
|
1235 | 1235 | |
|
1236 | 1236 | if ret == self.SKIPPED_STATUS: |
|
1237 | 1237 | if out is None: # Debug mode, nothing to parse. |
|
1238 | 1238 | missing = ['unknown'] |
|
1239 | 1239 | failed = None |
|
1240 | 1240 | else: |
|
1241 | 1241 | missing, failed = TTest.parsehghaveoutput(out) |
|
1242 | 1242 | |
|
1243 | 1243 | if not missing: |
|
1244 | 1244 | missing = ['skipped'] |
|
1245 | 1245 | |
|
1246 | 1246 | if failed: |
|
1247 | 1247 | self.fail('hg have failed checking for %s' % failed[-1]) |
|
1248 | 1248 | else: |
|
1249 | 1249 | self._skipped = True |
|
1250 | 1250 | raise unittest.SkipTest(missing[-1]) |
|
1251 | 1251 | elif ret == 'timeout': |
|
1252 | 1252 | self.fail('timed out') |
|
1253 | 1253 | elif ret is False: |
|
1254 | 1254 | self.fail('no result code from test') |
|
1255 | 1255 | elif out != self._refout: |
|
1256 | 1256 | # Diff generation may rely on written .err file. |
|
1257 | 1257 | if ( |
|
1258 | 1258 | (ret != 0 or out != self._refout) |
|
1259 | 1259 | and not self._skipped |
|
1260 | 1260 | and not self._debug |
|
1261 | 1261 | ): |
|
1262 | 1262 | with open(self.errpath, 'wb') as f: |
|
1263 | 1263 | for line in out: |
|
1264 | 1264 | f.write(line) |
|
1265 | 1265 | |
|
1266 | 1266 | # The result object handles diff calculation for us. |
|
1267 | 1267 | with firstlock: |
|
1268 | 1268 | if self._result.addOutputMismatch(self, ret, out, self._refout): |
|
1269 | 1269 | # change was accepted, skip failing |
|
1270 | 1270 | return |
|
1271 | 1271 | if self._first: |
|
1272 | 1272 | global firsterror |
|
1273 | 1273 | firsterror = True |
|
1274 | 1274 | |
|
1275 | 1275 | if ret: |
|
1276 | 1276 | msg = 'output changed and ' + describe(ret) |
|
1277 | 1277 | else: |
|
1278 | 1278 | msg = 'output changed' |
|
1279 | 1279 | |
|
1280 | 1280 | self.fail(msg) |
|
1281 | 1281 | elif ret: |
|
1282 | 1282 | self.fail(describe(ret)) |
|
1283 | 1283 | |
|
1284 | 1284 | def tearDown(self): |
|
1285 | 1285 | """Tasks to perform after run().""" |
|
1286 | 1286 | for entry in self._daemonpids: |
|
1287 | 1287 | killdaemons(entry) |
|
1288 | 1288 | self._daemonpids = [] |
|
1289 | 1289 | |
|
1290 | 1290 | if self._keeptmpdir: |
|
1291 | 1291 | log( |
|
1292 | 1292 | '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' |
|
1293 | 1293 | % ( |
|
1294 | 1294 | _bytes2sys(self._testtmp), |
|
1295 | 1295 | _bytes2sys(self._threadtmp), |
|
1296 | 1296 | ) |
|
1297 | 1297 | ) |
|
1298 | 1298 | else: |
|
1299 | 1299 | try: |
|
1300 | 1300 | shutil.rmtree(self._testtmp) |
|
1301 | 1301 | except OSError: |
|
1302 | 1302 | # unreadable directory may be left in $TESTTMP; fix permission |
|
1303 | 1303 | # and try again |
|
1304 | 1304 | makecleanable(self._testtmp) |
|
1305 | 1305 | shutil.rmtree(self._testtmp, True) |
|
1306 | 1306 | shutil.rmtree(self._threadtmp, True) |
|
1307 | 1307 | |
|
1308 | 1308 | if self._usechg: |
|
1309 | 1309 | # chgservers will stop automatically after they find the socket |
|
1310 | 1310 | # files are deleted |
|
1311 | 1311 | shutil.rmtree(self._chgsockdir, True) |
|
1312 | 1312 | |
|
1313 | 1313 | if ( |
|
1314 | 1314 | (self._ret != 0 or self._out != self._refout) |
|
1315 | 1315 | and not self._skipped |
|
1316 | 1316 | and not self._debug |
|
1317 | 1317 | and self._out |
|
1318 | 1318 | ): |
|
1319 | 1319 | with open(self.errpath, 'wb') as f: |
|
1320 | 1320 | for line in self._out: |
|
1321 | 1321 | f.write(line) |
|
1322 | 1322 | |
|
1323 | 1323 | vlog("# Ret was:", self._ret, '(%s)' % self.name) |
|
1324 | 1324 | |
|
1325 | 1325 | def _run(self, env): |
|
1326 | 1326 | # This should be implemented in child classes to run tests. |
|
1327 | 1327 | raise unittest.SkipTest('unknown test type') |
|
1328 | 1328 | |
|
1329 | 1329 | def abort(self): |
|
1330 | 1330 | """Terminate execution of this test.""" |
|
1331 | 1331 | self._aborted = True |
|
1332 | 1332 | |
|
1333 | 1333 | def _portmap(self, i): |
|
1334 | 1334 | offset = b'' if i == 0 else b'%d' % i |
|
1335 | 1335 | return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset) |
|
1336 | 1336 | |
|
1337 | 1337 | def _getreplacements(self): |
|
1338 | 1338 | """Obtain a mapping of text replacements to apply to test output. |
|
1339 | 1339 | |
|
1340 | 1340 | Test output needs to be normalized so it can be compared to expected |
|
1341 | 1341 | output. This function defines how some of that normalization will |
|
1342 | 1342 | occur. |
|
1343 | 1343 | """ |
|
1344 | 1344 | r = [ |
|
1345 | 1345 | # This list should be parallel to defineport in _getenv |
|
1346 | 1346 | self._portmap(0), |
|
1347 | 1347 | self._portmap(1), |
|
1348 | 1348 | self._portmap(2), |
|
1349 | 1349 | (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'), |
|
1350 | 1350 | (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'), |
|
1351 | 1351 | ] |
|
1352 | 1352 | r.append((self._escapepath(self._testtmp), b'$TESTTMP')) |
|
1353 | 1353 | if WINDOWS: |
|
1354 | 1354 | # JSON output escapes backslashes in Windows paths, so also catch a |
|
1355 | 1355 | # double-escape. |
|
1356 | 1356 | replaced = self._testtmp.replace(b'\\', br'\\') |
|
1357 | 1357 | r.append((self._escapepath(replaced), b'$STR_REPR_TESTTMP')) |
|
1358 | 1358 | |
|
1359 | 1359 | replacementfile = os.path.join(self._testdir, b'common-pattern.py') |
|
1360 | 1360 | |
|
1361 | 1361 | if os.path.exists(replacementfile): |
|
1362 | 1362 | data = {} |
|
1363 | 1363 | with open(replacementfile, mode='rb') as source: |
|
1364 | 1364 | # the intermediate 'compile' step help with debugging |
|
1365 | 1365 | code = compile(source.read(), replacementfile, 'exec') |
|
1366 | 1366 | exec(code, data) |
|
1367 | 1367 | for value in data.get('substitutions', ()): |
|
1368 | 1368 | if len(value) != 2: |
|
1369 | 1369 | msg = 'malformatted substitution in %s: %r' |
|
1370 | 1370 | msg %= (replacementfile, value) |
|
1371 | 1371 | raise ValueError(msg) |
|
1372 | 1372 | r.append(value) |
|
1373 | 1373 | return r |
|
1374 | 1374 | |
|
1375 | 1375 | def _escapepath(self, p): |
|
1376 | 1376 | if WINDOWS: |
|
1377 | 1377 | return b''.join( |
|
1378 | 1378 | c.isalpha() |
|
1379 | 1379 | and b'[%s%s]' % (c.lower(), c.upper()) |
|
1380 | 1380 | or c in b'/\\' |
|
1381 | 1381 | and br'[/\\]' |
|
1382 | 1382 | or c.isdigit() |
|
1383 | 1383 | and c |
|
1384 | 1384 | or b'\\' + c |
|
1385 | 1385 | for c in [p[i : i + 1] for i in range(len(p))] |
|
1386 | 1386 | ) |
|
1387 | 1387 | else: |
|
1388 | 1388 | return re.escape(p) |
|
1389 | 1389 | |
|
1390 | 1390 | def _localip(self): |
|
1391 | 1391 | if self._useipv6: |
|
1392 | 1392 | return b'::1' |
|
1393 | 1393 | else: |
|
1394 | 1394 | return b'127.0.0.1' |
|
1395 | 1395 | |
|
1396 | 1396 | def _genrestoreenv(self, testenv): |
|
1397 | 1397 | """Generate a script that can be used by tests to restore the original |
|
1398 | 1398 | environment.""" |
|
1399 | 1399 | # Put the restoreenv script inside self._threadtmp |
|
1400 | 1400 | scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh') |
|
1401 | 1401 | testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath) |
|
1402 | 1402 | |
|
1403 | 1403 | # Only restore environment variable names that the shell allows |
|
1404 | 1404 | # us to export. |
|
1405 | 1405 | name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$') |
|
1406 | 1406 | |
|
1407 | 1407 | # Do not restore these variables; otherwise tests would fail. |
|
1408 | 1408 | reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'} |
|
1409 | 1409 | |
|
1410 | 1410 | with open(scriptpath, 'w') as envf: |
|
1411 | 1411 | for name, value in origenviron.items(): |
|
1412 | 1412 | if not name_regex.match(name): |
|
1413 | 1413 | # Skip environment variables with unusual names not |
|
1414 | 1414 | # allowed by most shells. |
|
1415 | 1415 | continue |
|
1416 | 1416 | if name in reqnames: |
|
1417 | 1417 | continue |
|
1418 | 1418 | envf.write('%s=%s\n' % (name, shellquote(value))) |
|
1419 | 1419 | |
|
1420 | 1420 | for name in testenv: |
|
1421 | 1421 | if name in origenviron or name in reqnames: |
|
1422 | 1422 | continue |
|
1423 | 1423 | envf.write('unset %s\n' % (name,)) |
|
1424 | 1424 | |
|
1425 | 1425 | def _getenv(self): |
|
1426 | 1426 | """Obtain environment variables to use during test execution.""" |
|
1427 | 1427 | |
|
1428 | 1428 | def defineport(i): |
|
1429 | 1429 | offset = '' if i == 0 else '%s' % i |
|
1430 | 1430 | env["HGPORT%s" % offset] = '%s' % (self._startport + i) |
|
1431 | 1431 | |
|
1432 | 1432 | env = os.environ.copy() |
|
1433 | 1433 | env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or '' |
|
1434 | 1434 | env['HGEMITWARNINGS'] = '1' |
|
1435 | 1435 | env['TESTTMP'] = _bytes2sys(self._testtmp) |
|
1436 | 1436 | uid_file = os.path.join(_bytes2sys(self._testtmp), 'UID') |
|
1437 | 1437 | env['HGTEST_UUIDFILE'] = uid_file |
|
1438 | 1438 | env['TESTNAME'] = self.name |
|
1439 | 1439 | env['HOME'] = _bytes2sys(self._testtmp) |
|
1440 | 1440 | if WINDOWS: |
|
1441 | 1441 | env['REALUSERPROFILE'] = env['USERPROFILE'] |
|
1442 | 1442 | # py3.8+ ignores HOME: https://bugs.python.org/issue36264 |
|
1443 | 1443 | env['USERPROFILE'] = env['HOME'] |
|
1444 | 1444 | formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1]) |
|
1445 | 1445 | env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout |
|
1446 | 1446 | env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout) |
|
1447 | 1447 | # This number should match portneeded in _getport |
|
1448 | 1448 | for port in xrange(3): |
|
1449 | 1449 | # This list should be parallel to _portmap in _getreplacements |
|
1450 | 1450 | defineport(port) |
|
1451 | 1451 | env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc')) |
|
1452 | 1452 | env["DAEMON_PIDS"] = _bytes2sys( |
|
1453 | 1453 | os.path.join(self._threadtmp, b'daemon.pids') |
|
1454 | 1454 | ) |
|
1455 | 1455 | env["HGEDITOR"] = ( |
|
1456 | 1456 | '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"' |
|
1457 | 1457 | ) |
|
1458 | 1458 | env["HGUSER"] = "test" |
|
1459 | 1459 | env["HGENCODING"] = "ascii" |
|
1460 | 1460 | env["HGENCODINGMODE"] = "strict" |
|
1461 | 1461 | env["HGHOSTNAME"] = "test-hostname" |
|
1462 | 1462 | env['HGIPV6'] = str(int(self._useipv6)) |
|
1463 | 1463 | # See contrib/catapipe.py for how to use this functionality. |
|
1464 | 1464 | if 'HGTESTCATAPULTSERVERPIPE' not in env: |
|
1465 | 1465 | # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the |
|
1466 | 1466 | # non-test one in as a default, otherwise set to devnull |
|
1467 | 1467 | env['HGTESTCATAPULTSERVERPIPE'] = env.get( |
|
1468 | 1468 | 'HGCATAPULTSERVERPIPE', os.devnull |
|
1469 | 1469 | ) |
|
1470 | 1470 | |
|
1471 | 1471 | extraextensions = [] |
|
1472 | 1472 | for opt in self._extraconfigopts: |
|
1473 | 1473 | section, key = opt.split('.', 1) |
|
1474 | 1474 | if section != 'extensions': |
|
1475 | 1475 | continue |
|
1476 | 1476 | name = key.split('=', 1)[0] |
|
1477 | 1477 | extraextensions.append(name) |
|
1478 | 1478 | |
|
1479 | 1479 | if extraextensions: |
|
1480 | 1480 | env['HGTESTEXTRAEXTENSIONS'] = ' '.join(extraextensions) |
|
1481 | 1481 | |
|
1482 | 1482 | # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw |
|
1483 | 1483 | # IP addresses. |
|
1484 | 1484 | env['LOCALIP'] = _bytes2sys(self._localip()) |
|
1485 | 1485 | |
|
1486 | 1486 | # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c, |
|
1487 | 1487 | # but this is needed for testing python instances like dummyssh, |
|
1488 | 1488 | # dummysmtpd.py, and dumbhttp.py. |
|
1489 | 1489 | if PYTHON3 and WINDOWS: |
|
1490 | 1490 | env['PYTHONLEGACYWINDOWSSTDIO'] = '1' |
|
1491 | 1491 | |
|
1492 | 1492 | # Modified HOME in test environment can confuse Rust tools. So set |
|
1493 | 1493 | # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is |
|
1494 | 1494 | # present and these variables aren't already defined. |
|
1495 | 1495 | cargo_home_path = os.path.expanduser('~/.cargo') |
|
1496 | 1496 | rustup_home_path = os.path.expanduser('~/.rustup') |
|
1497 | 1497 | |
|
1498 | 1498 | if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb: |
|
1499 | 1499 | env['CARGO_HOME'] = cargo_home_path |
|
1500 | 1500 | if ( |
|
1501 | 1501 | os.path.exists(rustup_home_path) |
|
1502 | 1502 | and b'RUSTUP_HOME' not in osenvironb |
|
1503 | 1503 | ): |
|
1504 | 1504 | env['RUSTUP_HOME'] = rustup_home_path |
|
1505 | 1505 | |
|
1506 | 1506 | # Reset some environment variables to well-known values so that |
|
1507 | 1507 | # the tests produce repeatable output. |
|
1508 | 1508 | env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C' |
|
1509 | 1509 | env['TZ'] = 'GMT' |
|
1510 | 1510 | env["EMAIL"] = "Foo Bar <foo.bar@example.com>" |
|
1511 | 1511 | env['COLUMNS'] = '80' |
|
1512 | 1512 | env['TERM'] = 'xterm' |
|
1513 | 1513 | |
|
1514 | 1514 | dropped = [ |
|
1515 | 1515 | 'CDPATH', |
|
1516 | 1516 | 'CHGDEBUG', |
|
1517 | 1517 | 'EDITOR', |
|
1518 | 1518 | 'GREP_OPTIONS', |
|
1519 | 1519 | 'HG', |
|
1520 | 1520 | 'HGMERGE', |
|
1521 | 1521 | 'HGPLAIN', |
|
1522 | 1522 | 'HGPLAINEXCEPT', |
|
1523 | 1523 | 'HGPROF', |
|
1524 | 1524 | 'http_proxy', |
|
1525 | 1525 | 'no_proxy', |
|
1526 | 1526 | 'NO_PROXY', |
|
1527 | 1527 | 'PAGER', |
|
1528 | 1528 | 'VISUAL', |
|
1529 | 1529 | ] |
|
1530 | 1530 | |
|
1531 | 1531 | for k in dropped: |
|
1532 | 1532 | if k in env: |
|
1533 | 1533 | del env[k] |
|
1534 | 1534 | |
|
1535 | 1535 | # unset env related to hooks |
|
1536 | 1536 | for k in list(env): |
|
1537 | 1537 | if k.startswith('HG_'): |
|
1538 | 1538 | del env[k] |
|
1539 | 1539 | |
|
1540 | 1540 | if self._usechg: |
|
1541 | 1541 | env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server') |
|
1542 | 1542 | if self._chgdebug: |
|
1543 | 1543 | env['CHGDEBUG'] = 'true' |
|
1544 | 1544 | |
|
1545 | 1545 | return env |
|
1546 | 1546 | |
|
1547 | 1547 | def _createhgrc(self, path): |
|
1548 | 1548 | """Create an hgrc file for this test.""" |
|
1549 | 1549 | with open(path, 'wb') as hgrc: |
|
1550 | 1550 | hgrc.write(b'[ui]\n') |
|
1551 | 1551 | hgrc.write(b'slash = True\n') |
|
1552 | 1552 | hgrc.write(b'interactive = False\n') |
|
1553 | 1553 | hgrc.write(b'detailed-exit-code = True\n') |
|
1554 | 1554 | hgrc.write(b'merge = internal:merge\n') |
|
1555 | 1555 | hgrc.write(b'mergemarkers = detailed\n') |
|
1556 | 1556 | hgrc.write(b'promptecho = True\n') |
|
1557 | dummyssh = os.path.join(self._testdir, b'dummyssh') | |
|
1558 | hgrc.write(b'ssh = "%s" "%s"\n' % (PYTHON, dummyssh)) | |
|
1557 | 1559 | hgrc.write(b'timeout.warn=15\n') |
|
1558 | 1560 | hgrc.write(b'[chgserver]\n') |
|
1559 | 1561 | hgrc.write(b'idletimeout=60\n') |
|
1560 | 1562 | hgrc.write(b'[defaults]\n') |
|
1561 | 1563 | hgrc.write(b'[devel]\n') |
|
1562 | 1564 | hgrc.write(b'all-warnings = true\n') |
|
1563 | 1565 | hgrc.write(b'default-date = 0 0\n') |
|
1564 | 1566 | hgrc.write(b'[largefiles]\n') |
|
1565 | 1567 | hgrc.write( |
|
1566 | 1568 | b'usercache = %s\n' |
|
1567 | 1569 | % (os.path.join(self._testtmp, b'.cache/largefiles')) |
|
1568 | 1570 | ) |
|
1569 | 1571 | hgrc.write(b'[lfs]\n') |
|
1570 | 1572 | hgrc.write( |
|
1571 | 1573 | b'usercache = %s\n' |
|
1572 | 1574 | % (os.path.join(self._testtmp, b'.cache/lfs')) |
|
1573 | 1575 | ) |
|
1574 | 1576 | hgrc.write(b'[web]\n') |
|
1575 | 1577 | hgrc.write(b'address = localhost\n') |
|
1576 | 1578 | hgrc.write(b'ipv6 = %r\n' % self._useipv6) |
|
1577 | 1579 | hgrc.write(b'server-header = testing stub value\n') |
|
1578 | 1580 | |
|
1579 | 1581 | for opt in self._extraconfigopts: |
|
1580 | 1582 | section, key = _sys2bytes(opt).split(b'.', 1) |
|
1581 | 1583 | assert b'=' in key, ( |
|
1582 | 1584 | 'extra config opt %s must ' 'have an = for assignment' % opt |
|
1583 | 1585 | ) |
|
1584 | 1586 | hgrc.write(b'[%s]\n%s\n' % (section, key)) |
|
1585 | 1587 | |
|
1586 | 1588 | def fail(self, msg): |
|
1587 | 1589 | # unittest differentiates between errored and failed. |
|
1588 | 1590 | # Failed is denoted by AssertionError (by default at least). |
|
1589 | 1591 | raise AssertionError(msg) |
|
1590 | 1592 | |
|
1591 | 1593 | def _runcommand(self, cmd, env, normalizenewlines=False): |
|
1592 | 1594 | """Run command in a sub-process, capturing the output (stdout and |
|
1593 | 1595 | stderr). |
|
1594 | 1596 | |
|
1595 | 1597 | Return a tuple (exitcode, output). output is None in debug mode. |
|
1596 | 1598 | """ |
|
1597 | 1599 | if self._debug: |
|
1598 | 1600 | proc = subprocess.Popen( |
|
1599 | 1601 | _bytes2sys(cmd), |
|
1600 | 1602 | shell=True, |
|
1601 | 1603 | close_fds=closefds, |
|
1602 | 1604 | cwd=_bytes2sys(self._testtmp), |
|
1603 | 1605 | env=env, |
|
1604 | 1606 | ) |
|
1605 | 1607 | ret = proc.wait() |
|
1606 | 1608 | return (ret, None) |
|
1607 | 1609 | |
|
1608 | 1610 | proc = Popen4(cmd, self._testtmp, self._timeout, env) |
|
1609 | 1611 | |
|
1610 | 1612 | def cleanup(): |
|
1611 | 1613 | terminate(proc) |
|
1612 | 1614 | ret = proc.wait() |
|
1613 | 1615 | if ret == 0: |
|
1614 | 1616 | ret = signal.SIGTERM << 8 |
|
1615 | 1617 | killdaemons(env['DAEMON_PIDS']) |
|
1616 | 1618 | return ret |
|
1617 | 1619 | |
|
1618 | 1620 | proc.tochild.close() |
|
1619 | 1621 | |
|
1620 | 1622 | try: |
|
1621 | 1623 | output = proc.fromchild.read() |
|
1622 | 1624 | except KeyboardInterrupt: |
|
1623 | 1625 | vlog('# Handling keyboard interrupt') |
|
1624 | 1626 | cleanup() |
|
1625 | 1627 | raise |
|
1626 | 1628 | |
|
1627 | 1629 | ret = proc.wait() |
|
1628 | 1630 | if wifexited(ret): |
|
1629 | 1631 | ret = os.WEXITSTATUS(ret) |
|
1630 | 1632 | |
|
1631 | 1633 | if proc.timeout: |
|
1632 | 1634 | ret = 'timeout' |
|
1633 | 1635 | |
|
1634 | 1636 | if ret: |
|
1635 | 1637 | killdaemons(env['DAEMON_PIDS']) |
|
1636 | 1638 | |
|
1637 | 1639 | for s, r in self._getreplacements(): |
|
1638 | 1640 | output = re.sub(s, r, output) |
|
1639 | 1641 | |
|
1640 | 1642 | if normalizenewlines: |
|
1641 | 1643 | output = output.replace(b'\r\n', b'\n') |
|
1642 | 1644 | |
|
1643 | 1645 | return ret, output.splitlines(True) |
|
1644 | 1646 | |
|
1645 | 1647 | |
|
1646 | 1648 | class PythonTest(Test): |
|
1647 | 1649 | """A Python-based test.""" |
|
1648 | 1650 | |
|
1649 | 1651 | @property |
|
1650 | 1652 | def refpath(self): |
|
1651 | 1653 | return os.path.join(self._testdir, b'%s.out' % self.bname) |
|
1652 | 1654 | |
|
1653 | 1655 | def _run(self, env): |
|
1654 | 1656 | # Quote the python(3) executable for Windows |
|
1655 | 1657 | cmd = b'"%s" "%s"' % (PYTHON, self.path) |
|
1656 | 1658 | vlog("# Running", cmd.decode("utf-8")) |
|
1657 | 1659 | result = self._runcommand(cmd, env, normalizenewlines=WINDOWS) |
|
1658 | 1660 | if self._aborted: |
|
1659 | 1661 | raise KeyboardInterrupt() |
|
1660 | 1662 | |
|
1661 | 1663 | return result |
|
1662 | 1664 | |
|
1663 | 1665 | |
|
1664 | 1666 | # Some glob patterns apply only in some circumstances, so the script |
|
1665 | 1667 | # might want to remove (glob) annotations that otherwise should be |
|
1666 | 1668 | # retained. |
|
1667 | 1669 | checkcodeglobpats = [ |
|
1668 | 1670 | # On Windows it looks like \ doesn't require a (glob), but we know |
|
1669 | 1671 | # better. |
|
1670 | 1672 | re.compile(br'^pushing to \$TESTTMP/.*[^)]$'), |
|
1671 | 1673 | re.compile(br'^moving \S+/.*[^)]$'), |
|
1672 | 1674 | re.compile(br'^pulling from \$TESTTMP/.*[^)]$'), |
|
1673 | 1675 | # Not all platforms have 127.0.0.1 as loopback (though most do), |
|
1674 | 1676 | # so we always glob that too. |
|
1675 | 1677 | re.compile(br'.*\$LOCALIP.*$'), |
|
1676 | 1678 | ] |
|
1677 | 1679 | |
|
1678 | 1680 | bchr = chr |
|
1679 | 1681 | if PYTHON3: |
|
1680 | 1682 | bchr = lambda x: bytes([x]) |
|
1681 | 1683 | |
|
1682 | 1684 | WARN_UNDEFINED = 1 |
|
1683 | 1685 | WARN_YES = 2 |
|
1684 | 1686 | WARN_NO = 3 |
|
1685 | 1687 | |
|
1686 | 1688 | MARK_OPTIONAL = b" (?)\n" |
|
1687 | 1689 | |
|
1688 | 1690 | |
|
1689 | 1691 | def isoptional(line): |
|
1690 | 1692 | return line.endswith(MARK_OPTIONAL) |
|
1691 | 1693 | |
|
1692 | 1694 | |
|
1693 | 1695 | class TTest(Test): |
|
1694 | 1696 | """A "t test" is a test backed by a .t file.""" |
|
1695 | 1697 | |
|
1696 | 1698 | SKIPPED_PREFIX = b'skipped: ' |
|
1697 | 1699 | FAILED_PREFIX = b'hghave check failed: ' |
|
1698 | 1700 | NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search |
|
1699 | 1701 | |
|
1700 | 1702 | ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub |
|
1701 | 1703 | ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)} |
|
1702 | 1704 | ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'}) |
|
1703 | 1705 | |
|
1704 | 1706 | def __init__(self, path, *args, **kwds): |
|
1705 | 1707 | # accept an extra "case" parameter |
|
1706 | 1708 | case = kwds.pop('case', []) |
|
1707 | 1709 | self._case = case |
|
1708 | 1710 | self._allcases = {x for y in parsettestcases(path) for x in y} |
|
1709 | 1711 | super(TTest, self).__init__(path, *args, **kwds) |
|
1710 | 1712 | if case: |
|
1711 | 1713 | casepath = b'#'.join(case) |
|
1712 | 1714 | self.name = '%s#%s' % (self.name, _bytes2sys(casepath)) |
|
1713 | 1715 | self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath) |
|
1714 | 1716 | self._tmpname += b'-%s' % casepath.replace(b'#', b'-') |
|
1715 | 1717 | self._have = {} |
|
1716 | 1718 | |
|
1717 | 1719 | @property |
|
1718 | 1720 | def refpath(self): |
|
1719 | 1721 | return os.path.join(self._testdir, self.bname) |
|
1720 | 1722 | |
|
1721 | 1723 | def _run(self, env): |
|
1722 | 1724 | with open(self.path, 'rb') as f: |
|
1723 | 1725 | lines = f.readlines() |
|
1724 | 1726 | |
|
1725 | 1727 | # .t file is both reference output and the test input, keep reference |
|
1726 | 1728 | # output updated with the the test input. This avoids some race |
|
1727 | 1729 | # conditions where the reference output does not match the actual test. |
|
1728 | 1730 | if self._refout is not None: |
|
1729 | 1731 | self._refout = lines |
|
1730 | 1732 | |
|
1731 | 1733 | salt, script, after, expected = self._parsetest(lines) |
|
1732 | 1734 | |
|
1733 | 1735 | # Write out the generated script. |
|
1734 | 1736 | fname = b'%s.sh' % self._testtmp |
|
1735 | 1737 | with open(fname, 'wb') as f: |
|
1736 | 1738 | for l in script: |
|
1737 | 1739 | f.write(l) |
|
1738 | 1740 | |
|
1739 | 1741 | cmd = b'%s "%s"' % (self._shell, fname) |
|
1740 | 1742 | vlog("# Running", cmd.decode("utf-8")) |
|
1741 | 1743 | |
|
1742 | 1744 | exitcode, output = self._runcommand(cmd, env) |
|
1743 | 1745 | |
|
1744 | 1746 | if self._aborted: |
|
1745 | 1747 | raise KeyboardInterrupt() |
|
1746 | 1748 | |
|
1747 | 1749 | # Do not merge output if skipped. Return hghave message instead. |
|
1748 | 1750 | # Similarly, with --debug, output is None. |
|
1749 | 1751 | if exitcode == self.SKIPPED_STATUS or output is None: |
|
1750 | 1752 | return exitcode, output |
|
1751 | 1753 | |
|
1752 | 1754 | return self._processoutput(exitcode, output, salt, after, expected) |
|
1753 | 1755 | |
|
1754 | 1756 | def _hghave(self, reqs): |
|
1755 | 1757 | allreqs = b' '.join(reqs) |
|
1756 | 1758 | |
|
1757 | 1759 | self._detectslow(reqs) |
|
1758 | 1760 | |
|
1759 | 1761 | if allreqs in self._have: |
|
1760 | 1762 | return self._have.get(allreqs) |
|
1761 | 1763 | |
|
1762 | 1764 | # TODO do something smarter when all other uses of hghave are gone. |
|
1763 | 1765 | runtestdir = osenvironb[b'RUNTESTDIR'] |
|
1764 | 1766 | tdir = runtestdir.replace(b'\\', b'/') |
|
1765 | 1767 | proc = Popen4( |
|
1766 | 1768 | b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs), |
|
1767 | 1769 | self._testtmp, |
|
1768 | 1770 | 0, |
|
1769 | 1771 | self._getenv(), |
|
1770 | 1772 | ) |
|
1771 | 1773 | stdout, stderr = proc.communicate() |
|
1772 | 1774 | ret = proc.wait() |
|
1773 | 1775 | if wifexited(ret): |
|
1774 | 1776 | ret = os.WEXITSTATUS(ret) |
|
1775 | 1777 | if ret == 2: |
|
1776 | 1778 | print(stdout.decode('utf-8')) |
|
1777 | 1779 | sys.exit(1) |
|
1778 | 1780 | |
|
1779 | 1781 | if ret != 0: |
|
1780 | 1782 | self._have[allreqs] = (False, stdout) |
|
1781 | 1783 | return False, stdout |
|
1782 | 1784 | |
|
1783 | 1785 | self._have[allreqs] = (True, None) |
|
1784 | 1786 | return True, None |
|
1785 | 1787 | |
|
1786 | 1788 | def _detectslow(self, reqs): |
|
1787 | 1789 | """update the timeout of slow test when appropriate""" |
|
1788 | 1790 | if b'slow' in reqs: |
|
1789 | 1791 | self._timeout = self._slowtimeout |
|
1790 | 1792 | |
|
1791 | 1793 | def _iftest(self, args): |
|
1792 | 1794 | # implements "#if" |
|
1793 | 1795 | reqs = [] |
|
1794 | 1796 | for arg in args: |
|
1795 | 1797 | if arg.startswith(b'no-') and arg[3:] in self._allcases: |
|
1796 | 1798 | if arg[3:] in self._case: |
|
1797 | 1799 | return False |
|
1798 | 1800 | elif arg in self._allcases: |
|
1799 | 1801 | if arg not in self._case: |
|
1800 | 1802 | return False |
|
1801 | 1803 | else: |
|
1802 | 1804 | reqs.append(arg) |
|
1803 | 1805 | self._detectslow(reqs) |
|
1804 | 1806 | return self._hghave(reqs)[0] |
|
1805 | 1807 | |
|
1806 | 1808 | def _parsetest(self, lines): |
|
1807 | 1809 | # We generate a shell script which outputs unique markers to line |
|
1808 | 1810 | # up script results with our source. These markers include input |
|
1809 | 1811 | # line number and the last return code. |
|
1810 | 1812 | salt = b"SALT%d" % time.time() |
|
1811 | 1813 | |
|
1812 | 1814 | def addsalt(line, inpython): |
|
1813 | 1815 | if inpython: |
|
1814 | 1816 | script.append(b'%s %d 0\n' % (salt, line)) |
|
1815 | 1817 | else: |
|
1816 | 1818 | script.append(b'echo %s %d $?\n' % (salt, line)) |
|
1817 | 1819 | |
|
1818 | 1820 | activetrace = [] |
|
1819 | 1821 | session = str(uuid.uuid4()) |
|
1820 | 1822 | if PYTHON3: |
|
1821 | 1823 | session = session.encode('ascii') |
|
1822 | 1824 | hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv( |
|
1823 | 1825 | 'HGCATAPULTSERVERPIPE' |
|
1824 | 1826 | ) |
|
1825 | 1827 | |
|
1826 | 1828 | def toggletrace(cmd=None): |
|
1827 | 1829 | if not hgcatapult or hgcatapult == os.devnull: |
|
1828 | 1830 | return |
|
1829 | 1831 | |
|
1830 | 1832 | if activetrace: |
|
1831 | 1833 | script.append( |
|
1832 | 1834 | b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' |
|
1833 | 1835 | % (session, activetrace[0]) |
|
1834 | 1836 | ) |
|
1835 | 1837 | if cmd is None: |
|
1836 | 1838 | return |
|
1837 | 1839 | |
|
1838 | 1840 | if isinstance(cmd, str): |
|
1839 | 1841 | quoted = shellquote(cmd.strip()) |
|
1840 | 1842 | else: |
|
1841 | 1843 | quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8') |
|
1842 | 1844 | quoted = quoted.replace(b'\\', b'\\\\') |
|
1843 | 1845 | script.append( |
|
1844 | 1846 | b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n' |
|
1845 | 1847 | % (session, quoted) |
|
1846 | 1848 | ) |
|
1847 | 1849 | activetrace[0:] = [quoted] |
|
1848 | 1850 | |
|
1849 | 1851 | script = [] |
|
1850 | 1852 | |
|
1851 | 1853 | # After we run the shell script, we re-unify the script output |
|
1852 | 1854 | # with non-active parts of the source, with synchronization by our |
|
1853 | 1855 | # SALT line number markers. The after table contains the non-active |
|
1854 | 1856 | # components, ordered by line number. |
|
1855 | 1857 | after = {} |
|
1856 | 1858 | |
|
1857 | 1859 | # Expected shell script output. |
|
1858 | 1860 | expected = {} |
|
1859 | 1861 | |
|
1860 | 1862 | pos = prepos = -1 |
|
1861 | 1863 | |
|
1862 | 1864 | # True or False when in a true or false conditional section |
|
1863 | 1865 | skipping = None |
|
1864 | 1866 | |
|
1865 | 1867 | # We keep track of whether or not we're in a Python block so we |
|
1866 | 1868 | # can generate the surrounding doctest magic. |
|
1867 | 1869 | inpython = False |
|
1868 | 1870 | |
|
1869 | 1871 | if self._debug: |
|
1870 | 1872 | script.append(b'set -x\n') |
|
1871 | 1873 | if os.getenv('MSYSTEM'): |
|
1872 | 1874 | script.append(b'alias pwd="pwd -W"\n') |
|
1873 | 1875 | |
|
1874 | 1876 | if hgcatapult and hgcatapult != os.devnull: |
|
1875 | 1877 | if PYTHON3: |
|
1876 | 1878 | hgcatapult = hgcatapult.encode('utf8') |
|
1877 | 1879 | cataname = self.name.encode('utf8') |
|
1878 | 1880 | else: |
|
1879 | 1881 | cataname = self.name |
|
1880 | 1882 | |
|
1881 | 1883 | # Kludge: use a while loop to keep the pipe from getting |
|
1882 | 1884 | # closed by our echo commands. The still-running file gets |
|
1883 | 1885 | # reaped at the end of the script, which causes the while |
|
1884 | 1886 | # loop to exit and closes the pipe. Sigh. |
|
1885 | 1887 | script.append( |
|
1886 | 1888 | b'rtendtracing() {\n' |
|
1887 | 1889 | b' echo END %(session)s %(name)s >> %(catapult)s\n' |
|
1888 | 1890 | b' rm -f "$TESTTMP/.still-running"\n' |
|
1889 | 1891 | b'}\n' |
|
1890 | 1892 | b'trap "rtendtracing" 0\n' |
|
1891 | 1893 | b'touch "$TESTTMP/.still-running"\n' |
|
1892 | 1894 | b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done ' |
|
1893 | 1895 | b'> %(catapult)s &\n' |
|
1894 | 1896 | b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n' |
|
1895 | 1897 | b'echo START %(session)s %(name)s >> %(catapult)s\n' |
|
1896 | 1898 | % { |
|
1897 | 1899 | b'name': cataname, |
|
1898 | 1900 | b'session': session, |
|
1899 | 1901 | b'catapult': hgcatapult, |
|
1900 | 1902 | } |
|
1901 | 1903 | ) |
|
1902 | 1904 | |
|
1903 | 1905 | if self._case: |
|
1904 | 1906 | casestr = b'#'.join(self._case) |
|
1905 | 1907 | if isinstance(casestr, str): |
|
1906 | 1908 | quoted = shellquote(casestr) |
|
1907 | 1909 | else: |
|
1908 | 1910 | quoted = shellquote(casestr.decode('utf8')).encode('utf8') |
|
1909 | 1911 | script.append(b'TESTCASE=%s\n' % quoted) |
|
1910 | 1912 | script.append(b'export TESTCASE\n') |
|
1911 | 1913 | |
|
1912 | 1914 | n = 0 |
|
1913 | 1915 | for n, l in enumerate(lines): |
|
1914 | 1916 | if not l.endswith(b'\n'): |
|
1915 | 1917 | l += b'\n' |
|
1916 | 1918 | if l.startswith(b'#require'): |
|
1917 | 1919 | lsplit = l.split() |
|
1918 | 1920 | if len(lsplit) < 2 or lsplit[0] != b'#require': |
|
1919 | 1921 | after.setdefault(pos, []).append( |
|
1920 | 1922 | b' !!! invalid #require\n' |
|
1921 | 1923 | ) |
|
1922 | 1924 | if not skipping: |
|
1923 | 1925 | haveresult, message = self._hghave(lsplit[1:]) |
|
1924 | 1926 | if not haveresult: |
|
1925 | 1927 | script = [b'echo "%s"\nexit 80\n' % message] |
|
1926 | 1928 | break |
|
1927 | 1929 | after.setdefault(pos, []).append(l) |
|
1928 | 1930 | elif l.startswith(b'#if'): |
|
1929 | 1931 | lsplit = l.split() |
|
1930 | 1932 | if len(lsplit) < 2 or lsplit[0] != b'#if': |
|
1931 | 1933 | after.setdefault(pos, []).append(b' !!! invalid #if\n') |
|
1932 | 1934 | if skipping is not None: |
|
1933 | 1935 | after.setdefault(pos, []).append(b' !!! nested #if\n') |
|
1934 | 1936 | skipping = not self._iftest(lsplit[1:]) |
|
1935 | 1937 | after.setdefault(pos, []).append(l) |
|
1936 | 1938 | elif l.startswith(b'#else'): |
|
1937 | 1939 | if skipping is None: |
|
1938 | 1940 | after.setdefault(pos, []).append(b' !!! missing #if\n') |
|
1939 | 1941 | skipping = not skipping |
|
1940 | 1942 | after.setdefault(pos, []).append(l) |
|
1941 | 1943 | elif l.startswith(b'#endif'): |
|
1942 | 1944 | if skipping is None: |
|
1943 | 1945 | after.setdefault(pos, []).append(b' !!! missing #if\n') |
|
1944 | 1946 | skipping = None |
|
1945 | 1947 | after.setdefault(pos, []).append(l) |
|
1946 | 1948 | elif skipping: |
|
1947 | 1949 | after.setdefault(pos, []).append(l) |
|
1948 | 1950 | elif l.startswith(b' >>> '): # python inlines |
|
1949 | 1951 | after.setdefault(pos, []).append(l) |
|
1950 | 1952 | prepos = pos |
|
1951 | 1953 | pos = n |
|
1952 | 1954 | if not inpython: |
|
1953 | 1955 | # We've just entered a Python block. Add the header. |
|
1954 | 1956 | inpython = True |
|
1955 | 1957 | addsalt(prepos, False) # Make sure we report the exit code. |
|
1956 | 1958 | script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON) |
|
1957 | 1959 | addsalt(n, True) |
|
1958 | 1960 | script.append(l[2:]) |
|
1959 | 1961 | elif l.startswith(b' ... '): # python inlines |
|
1960 | 1962 | after.setdefault(prepos, []).append(l) |
|
1961 | 1963 | script.append(l[2:]) |
|
1962 | 1964 | elif l.startswith(b' $ '): # commands |
|
1963 | 1965 | if inpython: |
|
1964 | 1966 | script.append(b'EOF\n') |
|
1965 | 1967 | inpython = False |
|
1966 | 1968 | after.setdefault(pos, []).append(l) |
|
1967 | 1969 | prepos = pos |
|
1968 | 1970 | pos = n |
|
1969 | 1971 | addsalt(n, False) |
|
1970 | 1972 | rawcmd = l[4:] |
|
1971 | 1973 | cmd = rawcmd.split() |
|
1972 | 1974 | toggletrace(rawcmd) |
|
1973 | 1975 | if len(cmd) == 2 and cmd[0] == b'cd': |
|
1974 | 1976 | rawcmd = b'cd %s || exit 1\n' % cmd[1] |
|
1975 | 1977 | script.append(rawcmd) |
|
1976 | 1978 | elif l.startswith(b' > '): # continuations |
|
1977 | 1979 | after.setdefault(prepos, []).append(l) |
|
1978 | 1980 | script.append(l[4:]) |
|
1979 | 1981 | elif l.startswith(b' '): # results |
|
1980 | 1982 | # Queue up a list of expected results. |
|
1981 | 1983 | expected.setdefault(pos, []).append(l[2:]) |
|
1982 | 1984 | else: |
|
1983 | 1985 | if inpython: |
|
1984 | 1986 | script.append(b'EOF\n') |
|
1985 | 1987 | inpython = False |
|
1986 | 1988 | # Non-command/result. Queue up for merged output. |
|
1987 | 1989 | after.setdefault(pos, []).append(l) |
|
1988 | 1990 | |
|
1989 | 1991 | if inpython: |
|
1990 | 1992 | script.append(b'EOF\n') |
|
1991 | 1993 | if skipping is not None: |
|
1992 | 1994 | after.setdefault(pos, []).append(b' !!! missing #endif\n') |
|
1993 | 1995 | addsalt(n + 1, False) |
|
1994 | 1996 | # Need to end any current per-command trace |
|
1995 | 1997 | if activetrace: |
|
1996 | 1998 | toggletrace() |
|
1997 | 1999 | return salt, script, after, expected |
|
1998 | 2000 | |
|
1999 | 2001 | def _processoutput(self, exitcode, output, salt, after, expected): |
|
2000 | 2002 | # Merge the script output back into a unified test. |
|
2001 | 2003 | warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not |
|
2002 | 2004 | if exitcode != 0: |
|
2003 | 2005 | warnonly = WARN_NO |
|
2004 | 2006 | |
|
2005 | 2007 | pos = -1 |
|
2006 | 2008 | postout = [] |
|
2007 | 2009 | for out_rawline in output: |
|
2008 | 2010 | out_line, cmd_line = out_rawline, None |
|
2009 | 2011 | if salt in out_rawline: |
|
2010 | 2012 | out_line, cmd_line = out_rawline.split(salt, 1) |
|
2011 | 2013 | |
|
2012 | 2014 | pos, postout, warnonly = self._process_out_line( |
|
2013 | 2015 | out_line, pos, postout, expected, warnonly |
|
2014 | 2016 | ) |
|
2015 | 2017 | pos, postout = self._process_cmd_line(cmd_line, pos, postout, after) |
|
2016 | 2018 | |
|
2017 | 2019 | if pos in after: |
|
2018 | 2020 | postout += after.pop(pos) |
|
2019 | 2021 | |
|
2020 | 2022 | if warnonly == WARN_YES: |
|
2021 | 2023 | exitcode = False # Set exitcode to warned. |
|
2022 | 2024 | |
|
2023 | 2025 | return exitcode, postout |
|
2024 | 2026 | |
|
2025 | 2027 | def _process_out_line(self, out_line, pos, postout, expected, warnonly): |
|
2026 | 2028 | while out_line: |
|
2027 | 2029 | if not out_line.endswith(b'\n'): |
|
2028 | 2030 | out_line += b' (no-eol)\n' |
|
2029 | 2031 | |
|
2030 | 2032 | # Find the expected output at the current position. |
|
2031 | 2033 | els = [None] |
|
2032 | 2034 | if expected.get(pos, None): |
|
2033 | 2035 | els = expected[pos] |
|
2034 | 2036 | |
|
2035 | 2037 | optional = [] |
|
2036 | 2038 | for i, el in enumerate(els): |
|
2037 | 2039 | r = False |
|
2038 | 2040 | if el: |
|
2039 | 2041 | r, exact = self.linematch(el, out_line) |
|
2040 | 2042 | if isinstance(r, str): |
|
2041 | 2043 | if r == '-glob': |
|
2042 | 2044 | out_line = ''.join(el.rsplit(' (glob)', 1)) |
|
2043 | 2045 | r = '' # Warn only this line. |
|
2044 | 2046 | elif r == "retry": |
|
2045 | 2047 | postout.append(b' ' + el) |
|
2046 | 2048 | else: |
|
2047 | 2049 | log('\ninfo, unknown linematch result: %r\n' % r) |
|
2048 | 2050 | r = False |
|
2049 | 2051 | if r: |
|
2050 | 2052 | els.pop(i) |
|
2051 | 2053 | break |
|
2052 | 2054 | if el: |
|
2053 | 2055 | if isoptional(el): |
|
2054 | 2056 | optional.append(i) |
|
2055 | 2057 | else: |
|
2056 | 2058 | m = optline.match(el) |
|
2057 | 2059 | if m: |
|
2058 | 2060 | conditions = [c for c in m.group(2).split(b' ')] |
|
2059 | 2061 | |
|
2060 | 2062 | if not self._iftest(conditions): |
|
2061 | 2063 | optional.append(i) |
|
2062 | 2064 | if exact: |
|
2063 | 2065 | # Don't allow line to be matches against a later |
|
2064 | 2066 | # line in the output |
|
2065 | 2067 | els.pop(i) |
|
2066 | 2068 | break |
|
2067 | 2069 | |
|
2068 | 2070 | if r: |
|
2069 | 2071 | if r == "retry": |
|
2070 | 2072 | continue |
|
2071 | 2073 | # clean up any optional leftovers |
|
2072 | 2074 | for i in optional: |
|
2073 | 2075 | postout.append(b' ' + els[i]) |
|
2074 | 2076 | for i in reversed(optional): |
|
2075 | 2077 | del els[i] |
|
2076 | 2078 | postout.append(b' ' + el) |
|
2077 | 2079 | else: |
|
2078 | 2080 | if self.NEEDESCAPE(out_line): |
|
2079 | 2081 | out_line = TTest._stringescape( |
|
2080 | 2082 | b'%s (esc)\n' % out_line.rstrip(b'\n') |
|
2081 | 2083 | ) |
|
2082 | 2084 | postout.append(b' ' + out_line) # Let diff deal with it. |
|
2083 | 2085 | if r != '': # If line failed. |
|
2084 | 2086 | warnonly = WARN_NO |
|
2085 | 2087 | elif warnonly == WARN_UNDEFINED: |
|
2086 | 2088 | warnonly = WARN_YES |
|
2087 | 2089 | break |
|
2088 | 2090 | else: |
|
2089 | 2091 | # clean up any optional leftovers |
|
2090 | 2092 | while expected.get(pos, None): |
|
2091 | 2093 | el = expected[pos].pop(0) |
|
2092 | 2094 | if el: |
|
2093 | 2095 | if not isoptional(el): |
|
2094 | 2096 | m = optline.match(el) |
|
2095 | 2097 | if m: |
|
2096 | 2098 | conditions = [c for c in m.group(2).split(b' ')] |
|
2097 | 2099 | |
|
2098 | 2100 | if self._iftest(conditions): |
|
2099 | 2101 | # Don't append as optional line |
|
2100 | 2102 | continue |
|
2101 | 2103 | else: |
|
2102 | 2104 | continue |
|
2103 | 2105 | postout.append(b' ' + el) |
|
2104 | 2106 | return pos, postout, warnonly |
|
2105 | 2107 | |
|
2106 | 2108 | def _process_cmd_line(self, cmd_line, pos, postout, after): |
|
2107 | 2109 | """process a "command" part of a line from unified test output""" |
|
2108 | 2110 | if cmd_line: |
|
2109 | 2111 | # Add on last return code. |
|
2110 | 2112 | ret = int(cmd_line.split()[1]) |
|
2111 | 2113 | if ret != 0: |
|
2112 | 2114 | postout.append(b' [%d]\n' % ret) |
|
2113 | 2115 | if pos in after: |
|
2114 | 2116 | # Merge in non-active test bits. |
|
2115 | 2117 | postout += after.pop(pos) |
|
2116 | 2118 | pos = int(cmd_line.split()[0]) |
|
2117 | 2119 | return pos, postout |
|
2118 | 2120 | |
|
2119 | 2121 | @staticmethod |
|
2120 | 2122 | def rematch(el, l): |
|
2121 | 2123 | try: |
|
2122 | 2124 | # parse any flags at the beginning of the regex. Only 'i' is |
|
2123 | 2125 | # supported right now, but this should be easy to extend. |
|
2124 | 2126 | flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2] |
|
2125 | 2127 | flags = flags or b'' |
|
2126 | 2128 | el = flags + b'(?:' + el + b')' |
|
2127 | 2129 | # use \Z to ensure that the regex matches to the end of the string |
|
2128 | 2130 | if WINDOWS: |
|
2129 | 2131 | return re.match(el + br'\r?\n\Z', l) |
|
2130 | 2132 | return re.match(el + br'\n\Z', l) |
|
2131 | 2133 | except re.error: |
|
2132 | 2134 | # el is an invalid regex |
|
2133 | 2135 | return False |
|
2134 | 2136 | |
|
2135 | 2137 | @staticmethod |
|
2136 | 2138 | def globmatch(el, l): |
|
2137 | 2139 | # The only supported special characters are * and ? plus / which also |
|
2138 | 2140 | # matches \ on windows. Escaping of these characters is supported. |
|
2139 | 2141 | if el + b'\n' == l: |
|
2140 | 2142 | if os.altsep: |
|
2141 | 2143 | # matching on "/" is not needed for this line |
|
2142 | 2144 | for pat in checkcodeglobpats: |
|
2143 | 2145 | if pat.match(el): |
|
2144 | 2146 | return True |
|
2145 | 2147 | return b'-glob' |
|
2146 | 2148 | return True |
|
2147 | 2149 | el = el.replace(b'$LOCALIP', b'*') |
|
2148 | 2150 | i, n = 0, len(el) |
|
2149 | 2151 | res = b'' |
|
2150 | 2152 | while i < n: |
|
2151 | 2153 | c = el[i : i + 1] |
|
2152 | 2154 | i += 1 |
|
2153 | 2155 | if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/': |
|
2154 | 2156 | res += el[i - 1 : i + 1] |
|
2155 | 2157 | i += 1 |
|
2156 | 2158 | elif c == b'*': |
|
2157 | 2159 | res += b'.*' |
|
2158 | 2160 | elif c == b'?': |
|
2159 | 2161 | res += b'.' |
|
2160 | 2162 | elif c == b'/' and os.altsep: |
|
2161 | 2163 | res += b'[/\\\\]' |
|
2162 | 2164 | else: |
|
2163 | 2165 | res += re.escape(c) |
|
2164 | 2166 | return TTest.rematch(res, l) |
|
2165 | 2167 | |
|
2166 | 2168 | def linematch(self, el, l): |
|
2167 | 2169 | if el == l: # perfect match (fast) |
|
2168 | 2170 | return True, True |
|
2169 | 2171 | retry = False |
|
2170 | 2172 | if isoptional(el): |
|
2171 | 2173 | retry = "retry" |
|
2172 | 2174 | el = el[: -len(MARK_OPTIONAL)] + b"\n" |
|
2173 | 2175 | else: |
|
2174 | 2176 | m = optline.match(el) |
|
2175 | 2177 | if m: |
|
2176 | 2178 | conditions = [c for c in m.group(2).split(b' ')] |
|
2177 | 2179 | |
|
2178 | 2180 | el = m.group(1) + b"\n" |
|
2179 | 2181 | if not self._iftest(conditions): |
|
2180 | 2182 | # listed feature missing, should not match |
|
2181 | 2183 | return "retry", False |
|
2182 | 2184 | |
|
2183 | 2185 | if el.endswith(b" (esc)\n"): |
|
2184 | 2186 | if PYTHON3: |
|
2185 | 2187 | el = el[:-7].decode('unicode_escape') + '\n' |
|
2186 | 2188 | el = el.encode('latin-1') |
|
2187 | 2189 | else: |
|
2188 | 2190 | el = el[:-7].decode('string-escape') + '\n' |
|
2189 | 2191 | if el == l or WINDOWS and el[:-1] + b'\r\n' == l: |
|
2190 | 2192 | return True, True |
|
2191 | 2193 | if el.endswith(b" (re)\n"): |
|
2192 | 2194 | return (TTest.rematch(el[:-6], l) or retry), False |
|
2193 | 2195 | if el.endswith(b" (glob)\n"): |
|
2194 | 2196 | # ignore '(glob)' added to l by 'replacements' |
|
2195 | 2197 | if l.endswith(b" (glob)\n"): |
|
2196 | 2198 | l = l[:-8] + b"\n" |
|
2197 | 2199 | return (TTest.globmatch(el[:-8], l) or retry), False |
|
2198 | 2200 | if os.altsep: |
|
2199 | 2201 | _l = l.replace(b'\\', b'/') |
|
2200 | 2202 | if el == _l or WINDOWS and el[:-1] + b'\r\n' == _l: |
|
2201 | 2203 | return True, True |
|
2202 | 2204 | return retry, True |
|
2203 | 2205 | |
|
2204 | 2206 | @staticmethod |
|
2205 | 2207 | def parsehghaveoutput(lines): |
|
2206 | 2208 | """Parse hghave log lines. |
|
2207 | 2209 | |
|
2208 | 2210 | Return tuple of lists (missing, failed): |
|
2209 | 2211 | * the missing/unknown features |
|
2210 | 2212 | * the features for which existence check failed""" |
|
2211 | 2213 | missing = [] |
|
2212 | 2214 | failed = [] |
|
2213 | 2215 | for line in lines: |
|
2214 | 2216 | if line.startswith(TTest.SKIPPED_PREFIX): |
|
2215 | 2217 | line = line.splitlines()[0] |
|
2216 | 2218 | missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :])) |
|
2217 | 2219 | elif line.startswith(TTest.FAILED_PREFIX): |
|
2218 | 2220 | line = line.splitlines()[0] |
|
2219 | 2221 | failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :])) |
|
2220 | 2222 | |
|
2221 | 2223 | return missing, failed |
|
2222 | 2224 | |
|
2223 | 2225 | @staticmethod |
|
2224 | 2226 | def _escapef(m): |
|
2225 | 2227 | return TTest.ESCAPEMAP[m.group(0)] |
|
2226 | 2228 | |
|
2227 | 2229 | @staticmethod |
|
2228 | 2230 | def _stringescape(s): |
|
2229 | 2231 | return TTest.ESCAPESUB(TTest._escapef, s) |
|
2230 | 2232 | |
|
2231 | 2233 | |
|
2232 | 2234 | iolock = threading.RLock() |
|
2233 | 2235 | firstlock = threading.RLock() |
|
2234 | 2236 | firsterror = False |
|
2235 | 2237 | |
|
2236 | 2238 | |
|
2237 | 2239 | class TestResult(unittest._TextTestResult): |
|
2238 | 2240 | """Holds results when executing via unittest.""" |
|
2239 | 2241 | |
|
2240 | 2242 | # Don't worry too much about accessing the non-public _TextTestResult. |
|
2241 | 2243 | # It is relatively common in Python testing tools. |
|
2242 | 2244 | def __init__(self, options, *args, **kwargs): |
|
2243 | 2245 | super(TestResult, self).__init__(*args, **kwargs) |
|
2244 | 2246 | |
|
2245 | 2247 | self._options = options |
|
2246 | 2248 | |
|
2247 | 2249 | # unittest.TestResult didn't have skipped until 2.7. We need to |
|
2248 | 2250 | # polyfill it. |
|
2249 | 2251 | self.skipped = [] |
|
2250 | 2252 | |
|
2251 | 2253 | # We have a custom "ignored" result that isn't present in any Python |
|
2252 | 2254 | # unittest implementation. It is very similar to skipped. It may make |
|
2253 | 2255 | # sense to map it into skip some day. |
|
2254 | 2256 | self.ignored = [] |
|
2255 | 2257 | |
|
2256 | 2258 | self.times = [] |
|
2257 | 2259 | self._firststarttime = None |
|
2258 | 2260 | # Data stored for the benefit of generating xunit reports. |
|
2259 | 2261 | self.successes = [] |
|
2260 | 2262 | self.faildata = {} |
|
2261 | 2263 | |
|
2262 | 2264 | if options.color == 'auto': |
|
2263 | 2265 | isatty = self.stream.isatty() |
|
2264 | 2266 | # For some reason, redirecting stdout on Windows disables the ANSI |
|
2265 | 2267 | # color processing of stderr, which is what is used to print the |
|
2266 | 2268 | # output. Therefore, both must be tty on Windows to enable color. |
|
2267 | 2269 | if WINDOWS: |
|
2268 | 2270 | isatty = isatty and sys.stdout.isatty() |
|
2269 | 2271 | self.color = pygmentspresent and isatty |
|
2270 | 2272 | elif options.color == 'never': |
|
2271 | 2273 | self.color = False |
|
2272 | 2274 | else: # 'always', for testing purposes |
|
2273 | 2275 | self.color = pygmentspresent |
|
2274 | 2276 | |
|
2275 | 2277 | def onStart(self, test): |
|
2276 | 2278 | """Can be overriden by custom TestResult""" |
|
2277 | 2279 | |
|
2278 | 2280 | def onEnd(self): |
|
2279 | 2281 | """Can be overriden by custom TestResult""" |
|
2280 | 2282 | |
|
2281 | 2283 | def addFailure(self, test, reason): |
|
2282 | 2284 | self.failures.append((test, reason)) |
|
2283 | 2285 | |
|
2284 | 2286 | if self._options.first: |
|
2285 | 2287 | self.stop() |
|
2286 | 2288 | else: |
|
2287 | 2289 | with iolock: |
|
2288 | 2290 | if reason == "timed out": |
|
2289 | 2291 | self.stream.write('t') |
|
2290 | 2292 | else: |
|
2291 | 2293 | if not self._options.nodiff: |
|
2292 | 2294 | self.stream.write('\n') |
|
2293 | 2295 | # Exclude the '\n' from highlighting to lex correctly |
|
2294 | 2296 | formatted = 'ERROR: %s output changed\n' % test |
|
2295 | 2297 | self.stream.write(highlightmsg(formatted, self.color)) |
|
2296 | 2298 | self.stream.write('!') |
|
2297 | 2299 | |
|
2298 | 2300 | self.stream.flush() |
|
2299 | 2301 | |
|
2300 | 2302 | def addSuccess(self, test): |
|
2301 | 2303 | with iolock: |
|
2302 | 2304 | super(TestResult, self).addSuccess(test) |
|
2303 | 2305 | self.successes.append(test) |
|
2304 | 2306 | |
|
2305 | 2307 | def addError(self, test, err): |
|
2306 | 2308 | super(TestResult, self).addError(test, err) |
|
2307 | 2309 | if self._options.first: |
|
2308 | 2310 | self.stop() |
|
2309 | 2311 | |
|
2310 | 2312 | # Polyfill. |
|
2311 | 2313 | def addSkip(self, test, reason): |
|
2312 | 2314 | self.skipped.append((test, reason)) |
|
2313 | 2315 | with iolock: |
|
2314 | 2316 | if self.showAll: |
|
2315 | 2317 | self.stream.writeln('skipped %s' % reason) |
|
2316 | 2318 | else: |
|
2317 | 2319 | self.stream.write('s') |
|
2318 | 2320 | self.stream.flush() |
|
2319 | 2321 | |
|
2320 | 2322 | def addIgnore(self, test, reason): |
|
2321 | 2323 | self.ignored.append((test, reason)) |
|
2322 | 2324 | with iolock: |
|
2323 | 2325 | if self.showAll: |
|
2324 | 2326 | self.stream.writeln('ignored %s' % reason) |
|
2325 | 2327 | else: |
|
2326 | 2328 | if reason not in ('not retesting', "doesn't match keyword"): |
|
2327 | 2329 | self.stream.write('i') |
|
2328 | 2330 | else: |
|
2329 | 2331 | self.testsRun += 1 |
|
2330 | 2332 | self.stream.flush() |
|
2331 | 2333 | |
|
2332 | 2334 | def addOutputMismatch(self, test, ret, got, expected): |
|
2333 | 2335 | """Record a mismatch in test output for a particular test.""" |
|
2334 | 2336 | if self.shouldStop or firsterror: |
|
2335 | 2337 | # don't print, some other test case already failed and |
|
2336 | 2338 | # printed, we're just stale and probably failed due to our |
|
2337 | 2339 | # temp dir getting cleaned up. |
|
2338 | 2340 | return |
|
2339 | 2341 | |
|
2340 | 2342 | accepted = False |
|
2341 | 2343 | lines = [] |
|
2342 | 2344 | |
|
2343 | 2345 | with iolock: |
|
2344 | 2346 | if self._options.nodiff: |
|
2345 | 2347 | pass |
|
2346 | 2348 | elif self._options.view: |
|
2347 | 2349 | v = self._options.view |
|
2348 | 2350 | subprocess.call( |
|
2349 | 2351 | r'"%s" "%s" "%s"' |
|
2350 | 2352 | % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)), |
|
2351 | 2353 | shell=True, |
|
2352 | 2354 | ) |
|
2353 | 2355 | else: |
|
2354 | 2356 | servefail, lines = getdiff( |
|
2355 | 2357 | expected, got, test.refpath, test.errpath |
|
2356 | 2358 | ) |
|
2357 | 2359 | self.stream.write('\n') |
|
2358 | 2360 | for line in lines: |
|
2359 | 2361 | line = highlightdiff(line, self.color) |
|
2360 | 2362 | if PYTHON3: |
|
2361 | 2363 | self.stream.flush() |
|
2362 | 2364 | self.stream.buffer.write(line) |
|
2363 | 2365 | self.stream.buffer.flush() |
|
2364 | 2366 | else: |
|
2365 | 2367 | self.stream.write(line) |
|
2366 | 2368 | self.stream.flush() |
|
2367 | 2369 | |
|
2368 | 2370 | if servefail: |
|
2369 | 2371 | raise test.failureException( |
|
2370 | 2372 | 'server failed to start (HGPORT=%s)' % test._startport |
|
2371 | 2373 | ) |
|
2372 | 2374 | |
|
2373 | 2375 | # handle interactive prompt without releasing iolock |
|
2374 | 2376 | if self._options.interactive: |
|
2375 | 2377 | if test.readrefout() != expected: |
|
2376 | 2378 | self.stream.write( |
|
2377 | 2379 | 'Reference output has changed (run again to prompt ' |
|
2378 | 2380 | 'changes)' |
|
2379 | 2381 | ) |
|
2380 | 2382 | else: |
|
2381 | 2383 | self.stream.write('Accept this change? [y/N] ') |
|
2382 | 2384 | self.stream.flush() |
|
2383 | 2385 | answer = sys.stdin.readline().strip() |
|
2384 | 2386 | if answer.lower() in ('y', 'yes'): |
|
2385 | 2387 | if test.path.endswith(b'.t'): |
|
2386 | 2388 | rename(test.errpath, test.path) |
|
2387 | 2389 | else: |
|
2388 | 2390 | rename(test.errpath, b'%s.out' % test.path) |
|
2389 | 2391 | accepted = True |
|
2390 | 2392 | if not accepted: |
|
2391 | 2393 | self.faildata[test.name] = b''.join(lines) |
|
2392 | 2394 | |
|
2393 | 2395 | return accepted |
|
2394 | 2396 | |
|
2395 | 2397 | def startTest(self, test): |
|
2396 | 2398 | super(TestResult, self).startTest(test) |
|
2397 | 2399 | |
|
2398 | 2400 | # os.times module computes the user time and system time spent by |
|
2399 | 2401 | # child's processes along with real elapsed time taken by a process. |
|
2400 | 2402 | # This module has one limitation. It can only work for Linux user |
|
2401 | 2403 | # and not for Windows. Hence why we fall back to another function |
|
2402 | 2404 | # for wall time calculations. |
|
2403 | 2405 | test.started_times = os.times() |
|
2404 | 2406 | # TODO use a monotonic clock once support for Python 2.7 is dropped. |
|
2405 | 2407 | test.started_time = time.time() |
|
2406 | 2408 | if self._firststarttime is None: # thread racy but irrelevant |
|
2407 | 2409 | self._firststarttime = test.started_time |
|
2408 | 2410 | |
|
2409 | 2411 | def stopTest(self, test, interrupted=False): |
|
2410 | 2412 | super(TestResult, self).stopTest(test) |
|
2411 | 2413 | |
|
2412 | 2414 | test.stopped_times = os.times() |
|
2413 | 2415 | stopped_time = time.time() |
|
2414 | 2416 | |
|
2415 | 2417 | starttime = test.started_times |
|
2416 | 2418 | endtime = test.stopped_times |
|
2417 | 2419 | origin = self._firststarttime |
|
2418 | 2420 | self.times.append( |
|
2419 | 2421 | ( |
|
2420 | 2422 | test.name, |
|
2421 | 2423 | endtime[2] - starttime[2], # user space CPU time |
|
2422 | 2424 | endtime[3] - starttime[3], # sys space CPU time |
|
2423 | 2425 | stopped_time - test.started_time, # real time |
|
2424 | 2426 | test.started_time - origin, # start date in run context |
|
2425 | 2427 | stopped_time - origin, # end date in run context |
|
2426 | 2428 | ) |
|
2427 | 2429 | ) |
|
2428 | 2430 | |
|
2429 | 2431 | if interrupted: |
|
2430 | 2432 | with iolock: |
|
2431 | 2433 | self.stream.writeln( |
|
2432 | 2434 | 'INTERRUPTED: %s (after %d seconds)' |
|
2433 | 2435 | % (test.name, self.times[-1][3]) |
|
2434 | 2436 | ) |
|
2435 | 2437 | |
|
2436 | 2438 | |
|
2437 | 2439 | def getTestResult(): |
|
2438 | 2440 | """ |
|
2439 | 2441 | Returns the relevant test result |
|
2440 | 2442 | """ |
|
2441 | 2443 | if "CUSTOM_TEST_RESULT" in os.environ: |
|
2442 | 2444 | testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"]) |
|
2443 | 2445 | return testresultmodule.TestResult |
|
2444 | 2446 | else: |
|
2445 | 2447 | return TestResult |
|
2446 | 2448 | |
|
2447 | 2449 | |
|
2448 | 2450 | class TestSuite(unittest.TestSuite): |
|
2449 | 2451 | """Custom unittest TestSuite that knows how to execute Mercurial tests.""" |
|
2450 | 2452 | |
|
2451 | 2453 | def __init__( |
|
2452 | 2454 | self, |
|
2453 | 2455 | testdir, |
|
2454 | 2456 | jobs=1, |
|
2455 | 2457 | whitelist=None, |
|
2456 | 2458 | blacklist=None, |
|
2457 | 2459 | keywords=None, |
|
2458 | 2460 | loop=False, |
|
2459 | 2461 | runs_per_test=1, |
|
2460 | 2462 | loadtest=None, |
|
2461 | 2463 | showchannels=False, |
|
2462 | 2464 | *args, |
|
2463 | 2465 | **kwargs |
|
2464 | 2466 | ): |
|
2465 | 2467 | """Create a new instance that can run tests with a configuration. |
|
2466 | 2468 | |
|
2467 | 2469 | testdir specifies the directory where tests are executed from. This |
|
2468 | 2470 | is typically the ``tests`` directory from Mercurial's source |
|
2469 | 2471 | repository. |
|
2470 | 2472 | |
|
2471 | 2473 | jobs specifies the number of jobs to run concurrently. Each test |
|
2472 | 2474 | executes on its own thread. Tests actually spawn new processes, so |
|
2473 | 2475 | state mutation should not be an issue. |
|
2474 | 2476 | |
|
2475 | 2477 | If there is only one job, it will use the main thread. |
|
2476 | 2478 | |
|
2477 | 2479 | whitelist and blacklist denote tests that have been whitelisted and |
|
2478 | 2480 | blacklisted, respectively. These arguments don't belong in TestSuite. |
|
2479 | 2481 | Instead, whitelist and blacklist should be handled by the thing that |
|
2480 | 2482 | populates the TestSuite with tests. They are present to preserve |
|
2481 | 2483 | backwards compatible behavior which reports skipped tests as part |
|
2482 | 2484 | of the results. |
|
2483 | 2485 | |
|
2484 | 2486 | keywords denotes key words that will be used to filter which tests |
|
2485 | 2487 | to execute. This arguably belongs outside of TestSuite. |
|
2486 | 2488 | |
|
2487 | 2489 | loop denotes whether to loop over tests forever. |
|
2488 | 2490 | """ |
|
2489 | 2491 | super(TestSuite, self).__init__(*args, **kwargs) |
|
2490 | 2492 | |
|
2491 | 2493 | self._jobs = jobs |
|
2492 | 2494 | self._whitelist = whitelist |
|
2493 | 2495 | self._blacklist = blacklist |
|
2494 | 2496 | self._keywords = keywords |
|
2495 | 2497 | self._loop = loop |
|
2496 | 2498 | self._runs_per_test = runs_per_test |
|
2497 | 2499 | self._loadtest = loadtest |
|
2498 | 2500 | self._showchannels = showchannels |
|
2499 | 2501 | |
|
2500 | 2502 | def run(self, result): |
|
2501 | 2503 | # We have a number of filters that need to be applied. We do this |
|
2502 | 2504 | # here instead of inside Test because it makes the running logic for |
|
2503 | 2505 | # Test simpler. |
|
2504 | 2506 | tests = [] |
|
2505 | 2507 | num_tests = [0] |
|
2506 | 2508 | for test in self._tests: |
|
2507 | 2509 | |
|
2508 | 2510 | def get(): |
|
2509 | 2511 | num_tests[0] += 1 |
|
2510 | 2512 | if getattr(test, 'should_reload', False): |
|
2511 | 2513 | return self._loadtest(test, num_tests[0]) |
|
2512 | 2514 | return test |
|
2513 | 2515 | |
|
2514 | 2516 | if not os.path.exists(test.path): |
|
2515 | 2517 | result.addSkip(test, "Doesn't exist") |
|
2516 | 2518 | continue |
|
2517 | 2519 | |
|
2518 | 2520 | is_whitelisted = self._whitelist and ( |
|
2519 | 2521 | test.relpath in self._whitelist or test.bname in self._whitelist |
|
2520 | 2522 | ) |
|
2521 | 2523 | if not is_whitelisted: |
|
2522 | 2524 | is_blacklisted = self._blacklist and ( |
|
2523 | 2525 | test.relpath in self._blacklist |
|
2524 | 2526 | or test.bname in self._blacklist |
|
2525 | 2527 | ) |
|
2526 | 2528 | if is_blacklisted: |
|
2527 | 2529 | result.addSkip(test, 'blacklisted') |
|
2528 | 2530 | continue |
|
2529 | 2531 | if self._keywords: |
|
2530 | 2532 | with open(test.path, 'rb') as f: |
|
2531 | 2533 | t = f.read().lower() + test.bname.lower() |
|
2532 | 2534 | ignored = False |
|
2533 | 2535 | for k in self._keywords.lower().split(): |
|
2534 | 2536 | if k not in t: |
|
2535 | 2537 | result.addIgnore(test, "doesn't match keyword") |
|
2536 | 2538 | ignored = True |
|
2537 | 2539 | break |
|
2538 | 2540 | |
|
2539 | 2541 | if ignored: |
|
2540 | 2542 | continue |
|
2541 | 2543 | for _ in xrange(self._runs_per_test): |
|
2542 | 2544 | tests.append(get()) |
|
2543 | 2545 | |
|
2544 | 2546 | runtests = list(tests) |
|
2545 | 2547 | done = queue.Queue() |
|
2546 | 2548 | running = 0 |
|
2547 | 2549 | |
|
2548 | 2550 | channels = [""] * self._jobs |
|
2549 | 2551 | |
|
2550 | 2552 | def job(test, result): |
|
2551 | 2553 | for n, v in enumerate(channels): |
|
2552 | 2554 | if not v: |
|
2553 | 2555 | channel = n |
|
2554 | 2556 | break |
|
2555 | 2557 | else: |
|
2556 | 2558 | raise ValueError('Could not find output channel') |
|
2557 | 2559 | channels[channel] = "=" + test.name[5:].split(".")[0] |
|
2558 | 2560 | try: |
|
2559 | 2561 | test(result) |
|
2560 | 2562 | done.put(None) |
|
2561 | 2563 | except KeyboardInterrupt: |
|
2562 | 2564 | pass |
|
2563 | 2565 | except: # re-raises |
|
2564 | 2566 | done.put(('!', test, 'run-test raised an error, see traceback')) |
|
2565 | 2567 | raise |
|
2566 | 2568 | finally: |
|
2567 | 2569 | try: |
|
2568 | 2570 | channels[channel] = '' |
|
2569 | 2571 | except IndexError: |
|
2570 | 2572 | pass |
|
2571 | 2573 | |
|
2572 | 2574 | def stat(): |
|
2573 | 2575 | count = 0 |
|
2574 | 2576 | while channels: |
|
2575 | 2577 | d = '\n%03s ' % count |
|
2576 | 2578 | for n, v in enumerate(channels): |
|
2577 | 2579 | if v: |
|
2578 | 2580 | d += v[0] |
|
2579 | 2581 | channels[n] = v[1:] or '.' |
|
2580 | 2582 | else: |
|
2581 | 2583 | d += ' ' |
|
2582 | 2584 | d += ' ' |
|
2583 | 2585 | with iolock: |
|
2584 | 2586 | sys.stdout.write(d + ' ') |
|
2585 | 2587 | sys.stdout.flush() |
|
2586 | 2588 | for x in xrange(10): |
|
2587 | 2589 | if channels: |
|
2588 | 2590 | time.sleep(0.1) |
|
2589 | 2591 | count += 1 |
|
2590 | 2592 | |
|
2591 | 2593 | stoppedearly = False |
|
2592 | 2594 | |
|
2593 | 2595 | if self._showchannels: |
|
2594 | 2596 | statthread = threading.Thread(target=stat, name="stat") |
|
2595 | 2597 | statthread.start() |
|
2596 | 2598 | |
|
2597 | 2599 | try: |
|
2598 | 2600 | while tests or running: |
|
2599 | 2601 | if not done.empty() or running == self._jobs or not tests: |
|
2600 | 2602 | try: |
|
2601 | 2603 | done.get(True, 1) |
|
2602 | 2604 | running -= 1 |
|
2603 | 2605 | if result and result.shouldStop: |
|
2604 | 2606 | stoppedearly = True |
|
2605 | 2607 | break |
|
2606 | 2608 | except queue.Empty: |
|
2607 | 2609 | continue |
|
2608 | 2610 | if tests and not running == self._jobs: |
|
2609 | 2611 | test = tests.pop(0) |
|
2610 | 2612 | if self._loop: |
|
2611 | 2613 | if getattr(test, 'should_reload', False): |
|
2612 | 2614 | num_tests[0] += 1 |
|
2613 | 2615 | tests.append(self._loadtest(test, num_tests[0])) |
|
2614 | 2616 | else: |
|
2615 | 2617 | tests.append(test) |
|
2616 | 2618 | if self._jobs == 1: |
|
2617 | 2619 | job(test, result) |
|
2618 | 2620 | else: |
|
2619 | 2621 | t = threading.Thread( |
|
2620 | 2622 | target=job, name=test.name, args=(test, result) |
|
2621 | 2623 | ) |
|
2622 | 2624 | t.start() |
|
2623 | 2625 | running += 1 |
|
2624 | 2626 | |
|
2625 | 2627 | # If we stop early we still need to wait on started tests to |
|
2626 | 2628 | # finish. Otherwise, there is a race between the test completing |
|
2627 | 2629 | # and the test's cleanup code running. This could result in the |
|
2628 | 2630 | # test reporting incorrect. |
|
2629 | 2631 | if stoppedearly: |
|
2630 | 2632 | while running: |
|
2631 | 2633 | try: |
|
2632 | 2634 | done.get(True, 1) |
|
2633 | 2635 | running -= 1 |
|
2634 | 2636 | except queue.Empty: |
|
2635 | 2637 | continue |
|
2636 | 2638 | except KeyboardInterrupt: |
|
2637 | 2639 | for test in runtests: |
|
2638 | 2640 | test.abort() |
|
2639 | 2641 | |
|
2640 | 2642 | channels = [] |
|
2641 | 2643 | |
|
2642 | 2644 | return result |
|
2643 | 2645 | |
|
2644 | 2646 | |
|
2645 | 2647 | # Save the most recent 5 wall-clock runtimes of each test to a |
|
2646 | 2648 | # human-readable text file named .testtimes. Tests are sorted |
|
2647 | 2649 | # alphabetically, while times for each test are listed from oldest to |
|
2648 | 2650 | # newest. |
|
2649 | 2651 | |
|
2650 | 2652 | |
|
2651 | 2653 | def loadtimes(outputdir): |
|
2652 | 2654 | times = [] |
|
2653 | 2655 | try: |
|
2654 | 2656 | with open(os.path.join(outputdir, b'.testtimes')) as fp: |
|
2655 | 2657 | for line in fp: |
|
2656 | 2658 | m = re.match('(.*?) ([0-9. ]+)', line) |
|
2657 | 2659 | times.append( |
|
2658 | 2660 | (m.group(1), [float(t) for t in m.group(2).split()]) |
|
2659 | 2661 | ) |
|
2660 | 2662 | except IOError as err: |
|
2661 | 2663 | if err.errno != errno.ENOENT: |
|
2662 | 2664 | raise |
|
2663 | 2665 | return times |
|
2664 | 2666 | |
|
2665 | 2667 | |
|
2666 | 2668 | def savetimes(outputdir, result): |
|
2667 | 2669 | saved = dict(loadtimes(outputdir)) |
|
2668 | 2670 | maxruns = 5 |
|
2669 | 2671 | skipped = {str(t[0]) for t in result.skipped} |
|
2670 | 2672 | for tdata in result.times: |
|
2671 | 2673 | test, real = tdata[0], tdata[3] |
|
2672 | 2674 | if test not in skipped: |
|
2673 | 2675 | ts = saved.setdefault(test, []) |
|
2674 | 2676 | ts.append(real) |
|
2675 | 2677 | ts[:] = ts[-maxruns:] |
|
2676 | 2678 | |
|
2677 | 2679 | fd, tmpname = tempfile.mkstemp( |
|
2678 | 2680 | prefix=b'.testtimes', dir=outputdir, text=True |
|
2679 | 2681 | ) |
|
2680 | 2682 | with os.fdopen(fd, 'w') as fp: |
|
2681 | 2683 | for name, ts in sorted(saved.items()): |
|
2682 | 2684 | fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts]))) |
|
2683 | 2685 | timepath = os.path.join(outputdir, b'.testtimes') |
|
2684 | 2686 | try: |
|
2685 | 2687 | os.unlink(timepath) |
|
2686 | 2688 | except OSError: |
|
2687 | 2689 | pass |
|
2688 | 2690 | try: |
|
2689 | 2691 | os.rename(tmpname, timepath) |
|
2690 | 2692 | except OSError: |
|
2691 | 2693 | pass |
|
2692 | 2694 | |
|
2693 | 2695 | |
|
2694 | 2696 | class TextTestRunner(unittest.TextTestRunner): |
|
2695 | 2697 | """Custom unittest test runner that uses appropriate settings.""" |
|
2696 | 2698 | |
|
2697 | 2699 | def __init__(self, runner, *args, **kwargs): |
|
2698 | 2700 | super(TextTestRunner, self).__init__(*args, **kwargs) |
|
2699 | 2701 | |
|
2700 | 2702 | self._runner = runner |
|
2701 | 2703 | |
|
2702 | 2704 | self._result = getTestResult()( |
|
2703 | 2705 | self._runner.options, self.stream, self.descriptions, self.verbosity |
|
2704 | 2706 | ) |
|
2705 | 2707 | |
|
2706 | 2708 | def listtests(self, test): |
|
2707 | 2709 | test = sorted(test, key=lambda t: t.name) |
|
2708 | 2710 | |
|
2709 | 2711 | self._result.onStart(test) |
|
2710 | 2712 | |
|
2711 | 2713 | for t in test: |
|
2712 | 2714 | print(t.name) |
|
2713 | 2715 | self._result.addSuccess(t) |
|
2714 | 2716 | |
|
2715 | 2717 | if self._runner.options.xunit: |
|
2716 | 2718 | with open(self._runner.options.xunit, "wb") as xuf: |
|
2717 | 2719 | self._writexunit(self._result, xuf) |
|
2718 | 2720 | |
|
2719 | 2721 | if self._runner.options.json: |
|
2720 | 2722 | jsonpath = os.path.join(self._runner._outputdir, b'report.json') |
|
2721 | 2723 | with open(jsonpath, 'w') as fp: |
|
2722 | 2724 | self._writejson(self._result, fp) |
|
2723 | 2725 | |
|
2724 | 2726 | return self._result |
|
2725 | 2727 | |
|
2726 | 2728 | def run(self, test): |
|
2727 | 2729 | self._result.onStart(test) |
|
2728 | 2730 | test(self._result) |
|
2729 | 2731 | |
|
2730 | 2732 | failed = len(self._result.failures) |
|
2731 | 2733 | skipped = len(self._result.skipped) |
|
2732 | 2734 | ignored = len(self._result.ignored) |
|
2733 | 2735 | |
|
2734 | 2736 | with iolock: |
|
2735 | 2737 | self.stream.writeln('') |
|
2736 | 2738 | |
|
2737 | 2739 | if not self._runner.options.noskips: |
|
2738 | 2740 | for test, msg in sorted( |
|
2739 | 2741 | self._result.skipped, key=lambda s: s[0].name |
|
2740 | 2742 | ): |
|
2741 | 2743 | formatted = 'Skipped %s: %s\n' % (test.name, msg) |
|
2742 | 2744 | msg = highlightmsg(formatted, self._result.color) |
|
2743 | 2745 | self.stream.write(msg) |
|
2744 | 2746 | for test, msg in sorted( |
|
2745 | 2747 | self._result.failures, key=lambda f: f[0].name |
|
2746 | 2748 | ): |
|
2747 | 2749 | formatted = 'Failed %s: %s\n' % (test.name, msg) |
|
2748 | 2750 | self.stream.write(highlightmsg(formatted, self._result.color)) |
|
2749 | 2751 | for test, msg in sorted( |
|
2750 | 2752 | self._result.errors, key=lambda e: e[0].name |
|
2751 | 2753 | ): |
|
2752 | 2754 | self.stream.writeln('Errored %s: %s' % (test.name, msg)) |
|
2753 | 2755 | |
|
2754 | 2756 | if self._runner.options.xunit: |
|
2755 | 2757 | with open(self._runner.options.xunit, "wb") as xuf: |
|
2756 | 2758 | self._writexunit(self._result, xuf) |
|
2757 | 2759 | |
|
2758 | 2760 | if self._runner.options.json: |
|
2759 | 2761 | jsonpath = os.path.join(self._runner._outputdir, b'report.json') |
|
2760 | 2762 | with open(jsonpath, 'w') as fp: |
|
2761 | 2763 | self._writejson(self._result, fp) |
|
2762 | 2764 | |
|
2763 | 2765 | self._runner._checkhglib('Tested') |
|
2764 | 2766 | |
|
2765 | 2767 | savetimes(self._runner._outputdir, self._result) |
|
2766 | 2768 | |
|
2767 | 2769 | if failed and self._runner.options.known_good_rev: |
|
2768 | 2770 | self._bisecttests(t for t, m in self._result.failures) |
|
2769 | 2771 | self.stream.writeln( |
|
2770 | 2772 | '# Ran %d tests, %d skipped, %d failed.' |
|
2771 | 2773 | % (self._result.testsRun, skipped + ignored, failed) |
|
2772 | 2774 | ) |
|
2773 | 2775 | if failed: |
|
2774 | 2776 | self.stream.writeln( |
|
2775 | 2777 | 'python hash seed: %s' % os.environ['PYTHONHASHSEED'] |
|
2776 | 2778 | ) |
|
2777 | 2779 | if self._runner.options.time: |
|
2778 | 2780 | self.printtimes(self._result.times) |
|
2779 | 2781 | |
|
2780 | 2782 | if self._runner.options.exceptions: |
|
2781 | 2783 | exceptions = aggregateexceptions( |
|
2782 | 2784 | os.path.join(self._runner._outputdir, b'exceptions') |
|
2783 | 2785 | ) |
|
2784 | 2786 | |
|
2785 | 2787 | self.stream.writeln('Exceptions Report:') |
|
2786 | 2788 | self.stream.writeln( |
|
2787 | 2789 | '%d total from %d frames' |
|
2788 | 2790 | % (exceptions['total'], len(exceptions['exceptioncounts'])) |
|
2789 | 2791 | ) |
|
2790 | 2792 | combined = exceptions['combined'] |
|
2791 | 2793 | for key in sorted(combined, key=combined.get, reverse=True): |
|
2792 | 2794 | frame, line, exc = key |
|
2793 | 2795 | totalcount, testcount, leastcount, leasttest = combined[key] |
|
2794 | 2796 | |
|
2795 | 2797 | self.stream.writeln( |
|
2796 | 2798 | '%d (%d tests)\t%s: %s (%s - %d total)' |
|
2797 | 2799 | % ( |
|
2798 | 2800 | totalcount, |
|
2799 | 2801 | testcount, |
|
2800 | 2802 | frame, |
|
2801 | 2803 | exc, |
|
2802 | 2804 | leasttest, |
|
2803 | 2805 | leastcount, |
|
2804 | 2806 | ) |
|
2805 | 2807 | ) |
|
2806 | 2808 | |
|
2807 | 2809 | self.stream.flush() |
|
2808 | 2810 | |
|
2809 | 2811 | return self._result |
|
2810 | 2812 | |
|
2811 | 2813 | def _bisecttests(self, tests): |
|
2812 | 2814 | bisectcmd = ['hg', 'bisect'] |
|
2813 | 2815 | bisectrepo = self._runner.options.bisect_repo |
|
2814 | 2816 | if bisectrepo: |
|
2815 | 2817 | bisectcmd.extend(['-R', os.path.abspath(bisectrepo)]) |
|
2816 | 2818 | |
|
2817 | 2819 | def pread(args): |
|
2818 | 2820 | env = os.environ.copy() |
|
2819 | 2821 | env['HGPLAIN'] = '1' |
|
2820 | 2822 | p = subprocess.Popen( |
|
2821 | 2823 | args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env |
|
2822 | 2824 | ) |
|
2823 | 2825 | data = p.stdout.read() |
|
2824 | 2826 | p.wait() |
|
2825 | 2827 | return data |
|
2826 | 2828 | |
|
2827 | 2829 | for test in tests: |
|
2828 | 2830 | pread(bisectcmd + ['--reset']), |
|
2829 | 2831 | pread(bisectcmd + ['--bad', '.']) |
|
2830 | 2832 | pread(bisectcmd + ['--good', self._runner.options.known_good_rev]) |
|
2831 | 2833 | # TODO: we probably need to forward more options |
|
2832 | 2834 | # that alter hg's behavior inside the tests. |
|
2833 | 2835 | opts = '' |
|
2834 | 2836 | withhg = self._runner.options.with_hg |
|
2835 | 2837 | if withhg: |
|
2836 | 2838 | opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg)) |
|
2837 | 2839 | rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test) |
|
2838 | 2840 | data = pread(bisectcmd + ['--command', rtc]) |
|
2839 | 2841 | m = re.search( |
|
2840 | 2842 | ( |
|
2841 | 2843 | br'\nThe first (?P<goodbad>bad|good) revision ' |
|
2842 | 2844 | br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n' |
|
2843 | 2845 | br'summary: +(?P<summary>[^\n]+)\n' |
|
2844 | 2846 | ), |
|
2845 | 2847 | data, |
|
2846 | 2848 | (re.MULTILINE | re.DOTALL), |
|
2847 | 2849 | ) |
|
2848 | 2850 | if m is None: |
|
2849 | 2851 | self.stream.writeln( |
|
2850 | 2852 | 'Failed to identify failure point for %s' % test |
|
2851 | 2853 | ) |
|
2852 | 2854 | continue |
|
2853 | 2855 | dat = m.groupdict() |
|
2854 | 2856 | verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed' |
|
2855 | 2857 | self.stream.writeln( |
|
2856 | 2858 | '%s %s by %s (%s)' |
|
2857 | 2859 | % ( |
|
2858 | 2860 | test, |
|
2859 | 2861 | verb, |
|
2860 | 2862 | dat['node'].decode('ascii'), |
|
2861 | 2863 | dat['summary'].decode('utf8', 'ignore'), |
|
2862 | 2864 | ) |
|
2863 | 2865 | ) |
|
2864 | 2866 | |
|
2865 | 2867 | def printtimes(self, times): |
|
2866 | 2868 | # iolock held by run |
|
2867 | 2869 | self.stream.writeln('# Producing time report') |
|
2868 | 2870 | times.sort(key=lambda t: (t[3])) |
|
2869 | 2871 | cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s' |
|
2870 | 2872 | self.stream.writeln( |
|
2871 | 2873 | '%-7s %-7s %-7s %-7s %-7s %s' |
|
2872 | 2874 | % ('start', 'end', 'cuser', 'csys', 'real', 'Test') |
|
2873 | 2875 | ) |
|
2874 | 2876 | for tdata in times: |
|
2875 | 2877 | test = tdata[0] |
|
2876 | 2878 | cuser, csys, real, start, end = tdata[1:6] |
|
2877 | 2879 | self.stream.writeln(cols % (start, end, cuser, csys, real, test)) |
|
2878 | 2880 | |
|
2879 | 2881 | @staticmethod |
|
2880 | 2882 | def _writexunit(result, outf): |
|
2881 | 2883 | # See http://llg.cubic.org/docs/junit/ for a reference. |
|
2882 | 2884 | timesd = {t[0]: t[3] for t in result.times} |
|
2883 | 2885 | doc = minidom.Document() |
|
2884 | 2886 | s = doc.createElement('testsuite') |
|
2885 | 2887 | s.setAttribute('errors', "0") # TODO |
|
2886 | 2888 | s.setAttribute('failures', str(len(result.failures))) |
|
2887 | 2889 | s.setAttribute('name', 'run-tests') |
|
2888 | 2890 | s.setAttribute( |
|
2889 | 2891 | 'skipped', str(len(result.skipped) + len(result.ignored)) |
|
2890 | 2892 | ) |
|
2891 | 2893 | s.setAttribute('tests', str(result.testsRun)) |
|
2892 | 2894 | doc.appendChild(s) |
|
2893 | 2895 | for tc in result.successes: |
|
2894 | 2896 | t = doc.createElement('testcase') |
|
2895 | 2897 | t.setAttribute('name', tc.name) |
|
2896 | 2898 | tctime = timesd.get(tc.name) |
|
2897 | 2899 | if tctime is not None: |
|
2898 | 2900 | t.setAttribute('time', '%.3f' % tctime) |
|
2899 | 2901 | s.appendChild(t) |
|
2900 | 2902 | for tc, err in sorted(result.faildata.items()): |
|
2901 | 2903 | t = doc.createElement('testcase') |
|
2902 | 2904 | t.setAttribute('name', tc) |
|
2903 | 2905 | tctime = timesd.get(tc) |
|
2904 | 2906 | if tctime is not None: |
|
2905 | 2907 | t.setAttribute('time', '%.3f' % tctime) |
|
2906 | 2908 | # createCDATASection expects a unicode or it will |
|
2907 | 2909 | # convert using default conversion rules, which will |
|
2908 | 2910 | # fail if string isn't ASCII. |
|
2909 | 2911 | err = cdatasafe(err).decode('utf-8', 'replace') |
|
2910 | 2912 | cd = doc.createCDATASection(err) |
|
2911 | 2913 | # Use 'failure' here instead of 'error' to match errors = 0, |
|
2912 | 2914 | # failures = len(result.failures) in the testsuite element. |
|
2913 | 2915 | failelem = doc.createElement('failure') |
|
2914 | 2916 | failelem.setAttribute('message', 'output changed') |
|
2915 | 2917 | failelem.setAttribute('type', 'output-mismatch') |
|
2916 | 2918 | failelem.appendChild(cd) |
|
2917 | 2919 | t.appendChild(failelem) |
|
2918 | 2920 | s.appendChild(t) |
|
2919 | 2921 | for tc, message in result.skipped: |
|
2920 | 2922 | # According to the schema, 'skipped' has no attributes. So store |
|
2921 | 2923 | # the skip message as a text node instead. |
|
2922 | 2924 | t = doc.createElement('testcase') |
|
2923 | 2925 | t.setAttribute('name', tc.name) |
|
2924 | 2926 | binmessage = message.encode('utf-8') |
|
2925 | 2927 | message = cdatasafe(binmessage).decode('utf-8', 'replace') |
|
2926 | 2928 | cd = doc.createCDATASection(message) |
|
2927 | 2929 | skipelem = doc.createElement('skipped') |
|
2928 | 2930 | skipelem.appendChild(cd) |
|
2929 | 2931 | t.appendChild(skipelem) |
|
2930 | 2932 | s.appendChild(t) |
|
2931 | 2933 | outf.write(doc.toprettyxml(indent=' ', encoding='utf-8')) |
|
2932 | 2934 | |
|
2933 | 2935 | @staticmethod |
|
2934 | 2936 | def _writejson(result, outf): |
|
2935 | 2937 | timesd = {} |
|
2936 | 2938 | for tdata in result.times: |
|
2937 | 2939 | test = tdata[0] |
|
2938 | 2940 | timesd[test] = tdata[1:] |
|
2939 | 2941 | |
|
2940 | 2942 | outcome = {} |
|
2941 | 2943 | groups = [ |
|
2942 | 2944 | ('success', ((tc, None) for tc in result.successes)), |
|
2943 | 2945 | ('failure', result.failures), |
|
2944 | 2946 | ('skip', result.skipped), |
|
2945 | 2947 | ] |
|
2946 | 2948 | for res, testcases in groups: |
|
2947 | 2949 | for tc, __ in testcases: |
|
2948 | 2950 | if tc.name in timesd: |
|
2949 | 2951 | diff = result.faildata.get(tc.name, b'') |
|
2950 | 2952 | try: |
|
2951 | 2953 | diff = diff.decode('unicode_escape') |
|
2952 | 2954 | except UnicodeDecodeError as e: |
|
2953 | 2955 | diff = '%r decoding diff, sorry' % e |
|
2954 | 2956 | tres = { |
|
2955 | 2957 | 'result': res, |
|
2956 | 2958 | 'time': ('%0.3f' % timesd[tc.name][2]), |
|
2957 | 2959 | 'cuser': ('%0.3f' % timesd[tc.name][0]), |
|
2958 | 2960 | 'csys': ('%0.3f' % timesd[tc.name][1]), |
|
2959 | 2961 | 'start': ('%0.3f' % timesd[tc.name][3]), |
|
2960 | 2962 | 'end': ('%0.3f' % timesd[tc.name][4]), |
|
2961 | 2963 | 'diff': diff, |
|
2962 | 2964 | } |
|
2963 | 2965 | else: |
|
2964 | 2966 | # blacklisted test |
|
2965 | 2967 | tres = {'result': res} |
|
2966 | 2968 | |
|
2967 | 2969 | outcome[tc.name] = tres |
|
2968 | 2970 | jsonout = json.dumps( |
|
2969 | 2971 | outcome, sort_keys=True, indent=4, separators=(',', ': ') |
|
2970 | 2972 | ) |
|
2971 | 2973 | outf.writelines(("testreport =", jsonout)) |
|
2972 | 2974 | |
|
2973 | 2975 | |
|
2974 | 2976 | def sorttests(testdescs, previoustimes, shuffle=False): |
|
2975 | 2977 | """Do an in-place sort of tests.""" |
|
2976 | 2978 | if shuffle: |
|
2977 | 2979 | random.shuffle(testdescs) |
|
2978 | 2980 | return |
|
2979 | 2981 | |
|
2980 | 2982 | if previoustimes: |
|
2981 | 2983 | |
|
2982 | 2984 | def sortkey(f): |
|
2983 | 2985 | f = f['path'] |
|
2984 | 2986 | if f in previoustimes: |
|
2985 | 2987 | # Use most recent time as estimate |
|
2986 | 2988 | return -(previoustimes[f][-1]) |
|
2987 | 2989 | else: |
|
2988 | 2990 | # Default to a rather arbitrary value of 1 second for new tests |
|
2989 | 2991 | return -1.0 |
|
2990 | 2992 | |
|
2991 | 2993 | else: |
|
2992 | 2994 | # keywords for slow tests |
|
2993 | 2995 | slow = { |
|
2994 | 2996 | b'svn': 10, |
|
2995 | 2997 | b'cvs': 10, |
|
2996 | 2998 | b'hghave': 10, |
|
2997 | 2999 | b'largefiles-update': 10, |
|
2998 | 3000 | b'run-tests': 10, |
|
2999 | 3001 | b'corruption': 10, |
|
3000 | 3002 | b'race': 10, |
|
3001 | 3003 | b'i18n': 10, |
|
3002 | 3004 | b'check': 100, |
|
3003 | 3005 | b'gendoc': 100, |
|
3004 | 3006 | b'contrib-perf': 200, |
|
3005 | 3007 | b'merge-combination': 100, |
|
3006 | 3008 | } |
|
3007 | 3009 | perf = {} |
|
3008 | 3010 | |
|
3009 | 3011 | def sortkey(f): |
|
3010 | 3012 | # run largest tests first, as they tend to take the longest |
|
3011 | 3013 | f = f['path'] |
|
3012 | 3014 | try: |
|
3013 | 3015 | return perf[f] |
|
3014 | 3016 | except KeyError: |
|
3015 | 3017 | try: |
|
3016 | 3018 | val = -os.stat(f).st_size |
|
3017 | 3019 | except OSError as e: |
|
3018 | 3020 | if e.errno != errno.ENOENT: |
|
3019 | 3021 | raise |
|
3020 | 3022 | perf[f] = -1e9 # file does not exist, tell early |
|
3021 | 3023 | return -1e9 |
|
3022 | 3024 | for kw, mul in slow.items(): |
|
3023 | 3025 | if kw in f: |
|
3024 | 3026 | val *= mul |
|
3025 | 3027 | if f.endswith(b'.py'): |
|
3026 | 3028 | val /= 10.0 |
|
3027 | 3029 | perf[f] = val / 1000.0 |
|
3028 | 3030 | return perf[f] |
|
3029 | 3031 | |
|
3030 | 3032 | testdescs.sort(key=sortkey) |
|
3031 | 3033 | |
|
3032 | 3034 | |
|
3033 | 3035 | class TestRunner(object): |
|
3034 | 3036 | """Holds context for executing tests. |
|
3035 | 3037 | |
|
3036 | 3038 | Tests rely on a lot of state. This object holds it for them. |
|
3037 | 3039 | """ |
|
3038 | 3040 | |
|
3039 | 3041 | # Programs required to run tests. |
|
3040 | 3042 | REQUIREDTOOLS = [ |
|
3041 | 3043 | b'diff', |
|
3042 | 3044 | b'grep', |
|
3043 | 3045 | b'unzip', |
|
3044 | 3046 | b'gunzip', |
|
3045 | 3047 | b'bunzip2', |
|
3046 | 3048 | b'sed', |
|
3047 | 3049 | ] |
|
3048 | 3050 | |
|
3049 | 3051 | # Maps file extensions to test class. |
|
3050 | 3052 | TESTTYPES = [ |
|
3051 | 3053 | (b'.py', PythonTest), |
|
3052 | 3054 | (b'.t', TTest), |
|
3053 | 3055 | ] |
|
3054 | 3056 | |
|
3055 | 3057 | def __init__(self): |
|
3056 | 3058 | self.options = None |
|
3057 | 3059 | self._hgroot = None |
|
3058 | 3060 | self._testdir = None |
|
3059 | 3061 | self._outputdir = None |
|
3060 | 3062 | self._hgtmp = None |
|
3061 | 3063 | self._installdir = None |
|
3062 | 3064 | self._bindir = None |
|
3063 | 3065 | # a place for run-tests.py to generate executable it needs |
|
3064 | 3066 | self._custom_bin_dir = None |
|
3065 | 3067 | self._pythondir = None |
|
3066 | 3068 | # True if we had to infer the pythondir from --with-hg |
|
3067 | 3069 | self._pythondir_inferred = False |
|
3068 | 3070 | self._coveragefile = None |
|
3069 | 3071 | self._createdfiles = [] |
|
3070 | 3072 | self._hgcommand = None |
|
3071 | 3073 | self._hgpath = None |
|
3072 | 3074 | self._portoffset = 0 |
|
3073 | 3075 | self._ports = {} |
|
3074 | 3076 | |
|
3075 | 3077 | def run(self, args, parser=None): |
|
3076 | 3078 | """Run the test suite.""" |
|
3077 | 3079 | oldmask = os.umask(0o22) |
|
3078 | 3080 | try: |
|
3079 | 3081 | parser = parser or getparser() |
|
3080 | 3082 | options = parseargs(args, parser) |
|
3081 | 3083 | tests = [_sys2bytes(a) for a in options.tests] |
|
3082 | 3084 | if options.test_list is not None: |
|
3083 | 3085 | for listfile in options.test_list: |
|
3084 | 3086 | with open(listfile, 'rb') as f: |
|
3085 | 3087 | tests.extend(t for t in f.read().splitlines() if t) |
|
3086 | 3088 | self.options = options |
|
3087 | 3089 | |
|
3088 | 3090 | self._checktools() |
|
3089 | 3091 | testdescs = self.findtests(tests) |
|
3090 | 3092 | if options.profile_runner: |
|
3091 | 3093 | import statprof |
|
3092 | 3094 | |
|
3093 | 3095 | statprof.start() |
|
3094 | 3096 | result = self._run(testdescs) |
|
3095 | 3097 | if options.profile_runner: |
|
3096 | 3098 | statprof.stop() |
|
3097 | 3099 | statprof.display() |
|
3098 | 3100 | return result |
|
3099 | 3101 | |
|
3100 | 3102 | finally: |
|
3101 | 3103 | os.umask(oldmask) |
|
3102 | 3104 | |
|
3103 | 3105 | def _run(self, testdescs): |
|
3104 | 3106 | testdir = getcwdb() |
|
3105 | 3107 | # assume all tests in same folder for now |
|
3106 | 3108 | if testdescs: |
|
3107 | 3109 | pathname = os.path.dirname(testdescs[0]['path']) |
|
3108 | 3110 | if pathname: |
|
3109 | 3111 | testdir = os.path.join(testdir, pathname) |
|
3110 | 3112 | self._testdir = osenvironb[b'TESTDIR'] = testdir |
|
3111 | 3113 | if self.options.outputdir: |
|
3112 | 3114 | self._outputdir = canonpath(_sys2bytes(self.options.outputdir)) |
|
3113 | 3115 | else: |
|
3114 | 3116 | self._outputdir = getcwdb() |
|
3115 | 3117 | if testdescs and pathname: |
|
3116 | 3118 | self._outputdir = os.path.join(self._outputdir, pathname) |
|
3117 | 3119 | previoustimes = {} |
|
3118 | 3120 | if self.options.order_by_runtime: |
|
3119 | 3121 | previoustimes = dict(loadtimes(self._outputdir)) |
|
3120 | 3122 | sorttests(testdescs, previoustimes, shuffle=self.options.random) |
|
3121 | 3123 | |
|
3122 | 3124 | if 'PYTHONHASHSEED' not in os.environ: |
|
3123 | 3125 | # use a random python hash seed all the time |
|
3124 | 3126 | # we do the randomness ourself to know what seed is used |
|
3125 | 3127 | os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32)) |
|
3126 | 3128 | |
|
3127 | 3129 | # Rayon (Rust crate for multi-threading) will use all logical CPU cores |
|
3128 | 3130 | # by default, causing thrashing on high-cpu-count systems. |
|
3129 | 3131 | # Setting its limit to 3 during tests should still let us uncover |
|
3130 | 3132 | # multi-threading bugs while keeping the thrashing reasonable. |
|
3131 | 3133 | os.environ.setdefault("RAYON_NUM_THREADS", "3") |
|
3132 | 3134 | |
|
3133 | 3135 | if self.options.tmpdir: |
|
3134 | 3136 | self.options.keep_tmpdir = True |
|
3135 | 3137 | tmpdir = _sys2bytes(self.options.tmpdir) |
|
3136 | 3138 | if os.path.exists(tmpdir): |
|
3137 | 3139 | # Meaning of tmpdir has changed since 1.3: we used to create |
|
3138 | 3140 | # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if |
|
3139 | 3141 | # tmpdir already exists. |
|
3140 | 3142 | print("error: temp dir %r already exists" % tmpdir) |
|
3141 | 3143 | return 1 |
|
3142 | 3144 | |
|
3143 | 3145 | os.makedirs(tmpdir) |
|
3144 | 3146 | else: |
|
3145 | 3147 | d = None |
|
3146 | 3148 | if WINDOWS: |
|
3147 | 3149 | # without this, we get the default temp dir location, but |
|
3148 | 3150 | # in all lowercase, which causes troubles with paths (issue3490) |
|
3149 | 3151 | d = osenvironb.get(b'TMP', None) |
|
3150 | 3152 | tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d) |
|
3151 | 3153 | |
|
3152 | 3154 | self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir) |
|
3153 | 3155 | |
|
3154 | 3156 | self._custom_bin_dir = os.path.join(self._hgtmp, b'custom-bin') |
|
3155 | 3157 | os.makedirs(self._custom_bin_dir) |
|
3156 | 3158 | |
|
3157 | 3159 | if self.options.with_hg: |
|
3158 | 3160 | self._installdir = None |
|
3159 | 3161 | whg = self.options.with_hg |
|
3160 | 3162 | self._bindir = os.path.dirname(os.path.realpath(whg)) |
|
3161 | 3163 | assert isinstance(self._bindir, bytes) |
|
3162 | 3164 | self._hgcommand = os.path.basename(whg) |
|
3163 | 3165 | |
|
3164 | 3166 | normbin = os.path.normpath(os.path.abspath(whg)) |
|
3165 | 3167 | normbin = normbin.replace(_sys2bytes(os.sep), b'/') |
|
3166 | 3168 | |
|
3167 | 3169 | # Other Python scripts in the test harness need to |
|
3168 | 3170 | # `import mercurial`. If `hg` is a Python script, we assume |
|
3169 | 3171 | # the Mercurial modules are relative to its path and tell the tests |
|
3170 | 3172 | # to load Python modules from its directory. |
|
3171 | 3173 | with open(whg, 'rb') as fh: |
|
3172 | 3174 | initial = fh.read(1024) |
|
3173 | 3175 | |
|
3174 | 3176 | if re.match(b'#!.*python', initial): |
|
3175 | 3177 | self._pythondir = self._bindir |
|
3176 | 3178 | # If it looks like our in-repo Rust binary, use the source root. |
|
3177 | 3179 | # This is a bit hacky. But rhg is still not supported outside the |
|
3178 | 3180 | # source directory. So until it is, do the simple thing. |
|
3179 | 3181 | elif re.search(b'/rust/target/[^/]+/hg', normbin): |
|
3180 | 3182 | self._pythondir = os.path.dirname(self._testdir) |
|
3181 | 3183 | # Fall back to the legacy behavior. |
|
3182 | 3184 | else: |
|
3183 | 3185 | self._pythondir = self._bindir |
|
3184 | 3186 | self._pythondir_inferred = True |
|
3185 | 3187 | |
|
3186 | 3188 | else: |
|
3187 | 3189 | self._installdir = os.path.join(self._hgtmp, b"install") |
|
3188 | 3190 | self._bindir = os.path.join(self._installdir, b"bin") |
|
3189 | 3191 | self._hgcommand = b'hg' |
|
3190 | 3192 | self._pythondir = os.path.join(self._installdir, b"lib", b"python") |
|
3191 | 3193 | |
|
3192 | 3194 | # Force the use of hg.exe instead of relying on MSYS to recognize hg is |
|
3193 | 3195 | # a python script and feed it to python.exe. Legacy stdio is force |
|
3194 | 3196 | # enabled by hg.exe, and this is a more realistic way to launch hg |
|
3195 | 3197 | # anyway. |
|
3196 | 3198 | if WINDOWS and not self._hgcommand.endswith(b'.exe'): |
|
3197 | 3199 | self._hgcommand += b'.exe' |
|
3198 | 3200 | |
|
3199 | 3201 | real_hg = os.path.join(self._bindir, self._hgcommand) |
|
3200 | 3202 | osenvironb[b'HGTEST_REAL_HG'] = real_hg |
|
3201 | 3203 | # set CHGHG, then replace "hg" command by "chg" |
|
3202 | 3204 | chgbindir = self._bindir |
|
3203 | 3205 | if self.options.chg or self.options.with_chg: |
|
3204 | 3206 | osenvironb[b'CHG_INSTALLED_AS_HG'] = b'1' |
|
3205 | 3207 | osenvironb[b'CHGHG'] = real_hg |
|
3206 | 3208 | else: |
|
3207 | 3209 | # drop flag for hghave |
|
3208 | 3210 | osenvironb.pop(b'CHG_INSTALLED_AS_HG', None) |
|
3209 | 3211 | if self.options.chg: |
|
3210 | 3212 | self._hgcommand = b'chg' |
|
3211 | 3213 | elif self.options.with_chg: |
|
3212 | 3214 | chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg)) |
|
3213 | 3215 | self._hgcommand = os.path.basename(self.options.with_chg) |
|
3214 | 3216 | |
|
3215 | 3217 | # configure fallback and replace "hg" command by "rhg" |
|
3216 | 3218 | rhgbindir = self._bindir |
|
3217 | 3219 | if self.options.rhg or self.options.with_rhg: |
|
3218 | 3220 | # Affects hghave.py |
|
3219 | 3221 | osenvironb[b'RHG_INSTALLED_AS_HG'] = b'1' |
|
3220 | 3222 | # Affects configuration. Alternatives would be setting configuration through |
|
3221 | 3223 | # `$HGRCPATH` but some tests override that, or changing `_hgcommand` to include |
|
3222 | 3224 | # `--config` but that disrupts tests that print command lines and check expected |
|
3223 | 3225 | # output. |
|
3224 | 3226 | osenvironb[b'RHG_ON_UNSUPPORTED'] = b'fallback' |
|
3225 | 3227 | osenvironb[b'RHG_FALLBACK_EXECUTABLE'] = real_hg |
|
3226 | 3228 | else: |
|
3227 | 3229 | # drop flag for hghave |
|
3228 | 3230 | osenvironb.pop(b'RHG_INSTALLED_AS_HG', None) |
|
3229 | 3231 | if self.options.rhg: |
|
3230 | 3232 | self._hgcommand = b'rhg' |
|
3231 | 3233 | elif self.options.with_rhg: |
|
3232 | 3234 | rhgbindir = os.path.dirname(os.path.realpath(self.options.with_rhg)) |
|
3233 | 3235 | self._hgcommand = os.path.basename(self.options.with_rhg) |
|
3234 | 3236 | |
|
3235 | 3237 | if self.options.pyoxidized: |
|
3236 | 3238 | testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0]))) |
|
3237 | 3239 | reporootdir = os.path.dirname(testdir) |
|
3238 | 3240 | # XXX we should ideally install stuff instead of using the local build |
|
3239 | 3241 | bin_path = ( |
|
3240 | 3242 | b'build/pyoxidizer/x86_64-pc-windows-msvc/release/app/hg.exe' |
|
3241 | 3243 | ) |
|
3242 | 3244 | full_path = os.path.join(reporootdir, bin_path) |
|
3243 | 3245 | self._hgcommand = full_path |
|
3244 | 3246 | # Affects hghave.py |
|
3245 | 3247 | osenvironb[b'PYOXIDIZED_INSTALLED_AS_HG'] = b'1' |
|
3246 | 3248 | else: |
|
3247 | 3249 | osenvironb.pop(b'PYOXIDIZED_INSTALLED_AS_HG', None) |
|
3248 | 3250 | |
|
3249 | 3251 | osenvironb[b"BINDIR"] = self._bindir |
|
3250 | 3252 | osenvironb[b"PYTHON"] = PYTHON |
|
3251 | 3253 | |
|
3252 | 3254 | fileb = _sys2bytes(__file__) |
|
3253 | 3255 | runtestdir = os.path.abspath(os.path.dirname(fileb)) |
|
3254 | 3256 | osenvironb[b'RUNTESTDIR'] = runtestdir |
|
3255 | 3257 | if PYTHON3: |
|
3256 | 3258 | sepb = _sys2bytes(os.pathsep) |
|
3257 | 3259 | else: |
|
3258 | 3260 | sepb = os.pathsep |
|
3259 | 3261 | path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb) |
|
3260 | 3262 | if os.path.islink(__file__): |
|
3261 | 3263 | # test helper will likely be at the end of the symlink |
|
3262 | 3264 | realfile = os.path.realpath(fileb) |
|
3263 | 3265 | realdir = os.path.abspath(os.path.dirname(realfile)) |
|
3264 | 3266 | path.insert(2, realdir) |
|
3265 | 3267 | if chgbindir != self._bindir: |
|
3266 | 3268 | path.insert(1, chgbindir) |
|
3267 | 3269 | if rhgbindir != self._bindir: |
|
3268 | 3270 | path.insert(1, rhgbindir) |
|
3269 | 3271 | if self._testdir != runtestdir: |
|
3270 | 3272 | path = [self._testdir] + path |
|
3271 | 3273 | path = [self._custom_bin_dir] + path |
|
3272 | 3274 | osenvironb[b"PATH"] = sepb.join(path) |
|
3273 | 3275 | |
|
3274 | 3276 | # Include TESTDIR in PYTHONPATH so that out-of-tree extensions |
|
3275 | 3277 | # can run .../tests/run-tests.py test-foo where test-foo |
|
3276 | 3278 | # adds an extension to HGRC. Also include run-test.py directory to |
|
3277 | 3279 | # import modules like heredoctest. |
|
3278 | 3280 | pypath = [self._pythondir, self._testdir, runtestdir] |
|
3279 | 3281 | # We have to augment PYTHONPATH, rather than simply replacing |
|
3280 | 3282 | # it, in case external libraries are only available via current |
|
3281 | 3283 | # PYTHONPATH. (In particular, the Subversion bindings on OS X |
|
3282 | 3284 | # are in /opt/subversion.) |
|
3283 | 3285 | oldpypath = osenvironb.get(IMPL_PATH) |
|
3284 | 3286 | if oldpypath: |
|
3285 | 3287 | pypath.append(oldpypath) |
|
3286 | 3288 | osenvironb[IMPL_PATH] = sepb.join(pypath) |
|
3287 | 3289 | |
|
3288 | 3290 | if self.options.pure: |
|
3289 | 3291 | os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure" |
|
3290 | 3292 | os.environ["HGMODULEPOLICY"] = "py" |
|
3291 | 3293 | if self.options.rust: |
|
3292 | 3294 | os.environ["HGMODULEPOLICY"] = "rust+c" |
|
3293 | 3295 | if self.options.no_rust: |
|
3294 | 3296 | current_policy = os.environ.get("HGMODULEPOLICY", "") |
|
3295 | 3297 | if current_policy.startswith("rust+"): |
|
3296 | 3298 | os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :] |
|
3297 | 3299 | os.environ.pop("HGWITHRUSTEXT", None) |
|
3298 | 3300 | |
|
3299 | 3301 | if self.options.allow_slow_tests: |
|
3300 | 3302 | os.environ["HGTEST_SLOW"] = "slow" |
|
3301 | 3303 | elif 'HGTEST_SLOW' in os.environ: |
|
3302 | 3304 | del os.environ['HGTEST_SLOW'] |
|
3303 | 3305 | |
|
3304 | 3306 | self._coveragefile = os.path.join(self._testdir, b'.coverage') |
|
3305 | 3307 | |
|
3306 | 3308 | if self.options.exceptions: |
|
3307 | 3309 | exceptionsdir = os.path.join(self._outputdir, b'exceptions') |
|
3308 | 3310 | try: |
|
3309 | 3311 | os.makedirs(exceptionsdir) |
|
3310 | 3312 | except OSError as e: |
|
3311 | 3313 | if e.errno != errno.EEXIST: |
|
3312 | 3314 | raise |
|
3313 | 3315 | |
|
3314 | 3316 | # Remove all existing exception reports. |
|
3315 | 3317 | for f in os.listdir(exceptionsdir): |
|
3316 | 3318 | os.unlink(os.path.join(exceptionsdir, f)) |
|
3317 | 3319 | |
|
3318 | 3320 | osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir |
|
3319 | 3321 | logexceptions = os.path.join(self._testdir, b'logexceptions.py') |
|
3320 | 3322 | self.options.extra_config_opt.append( |
|
3321 | 3323 | 'extensions.logexceptions=%s' % logexceptions.decode('utf-8') |
|
3322 | 3324 | ) |
|
3323 | 3325 | |
|
3324 | 3326 | vlog("# Using TESTDIR", _bytes2sys(self._testdir)) |
|
3325 | 3327 | vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR'])) |
|
3326 | 3328 | vlog("# Using HGTMP", _bytes2sys(self._hgtmp)) |
|
3327 | 3329 | vlog("# Using PATH", os.environ["PATH"]) |
|
3328 | 3330 | vlog( |
|
3329 | 3331 | "# Using", |
|
3330 | 3332 | _bytes2sys(IMPL_PATH), |
|
3331 | 3333 | _bytes2sys(osenvironb[IMPL_PATH]), |
|
3332 | 3334 | ) |
|
3333 | 3335 | vlog("# Writing to directory", _bytes2sys(self._outputdir)) |
|
3334 | 3336 | |
|
3335 | 3337 | try: |
|
3336 | 3338 | return self._runtests(testdescs) or 0 |
|
3337 | 3339 | finally: |
|
3338 | 3340 | time.sleep(0.1) |
|
3339 | 3341 | self._cleanup() |
|
3340 | 3342 | |
|
3341 | 3343 | def findtests(self, args): |
|
3342 | 3344 | """Finds possible test files from arguments. |
|
3343 | 3345 | |
|
3344 | 3346 | If you wish to inject custom tests into the test harness, this would |
|
3345 | 3347 | be a good function to monkeypatch or override in a derived class. |
|
3346 | 3348 | """ |
|
3347 | 3349 | if not args: |
|
3348 | 3350 | if self.options.changed: |
|
3349 | 3351 | proc = Popen4( |
|
3350 | 3352 | b'hg st --rev "%s" -man0 .' |
|
3351 | 3353 | % _sys2bytes(self.options.changed), |
|
3352 | 3354 | None, |
|
3353 | 3355 | 0, |
|
3354 | 3356 | ) |
|
3355 | 3357 | stdout, stderr = proc.communicate() |
|
3356 | 3358 | args = stdout.strip(b'\0').split(b'\0') |
|
3357 | 3359 | else: |
|
3358 | 3360 | args = os.listdir(b'.') |
|
3359 | 3361 | |
|
3360 | 3362 | expanded_args = [] |
|
3361 | 3363 | for arg in args: |
|
3362 | 3364 | if os.path.isdir(arg): |
|
3363 | 3365 | if not arg.endswith(b'/'): |
|
3364 | 3366 | arg += b'/' |
|
3365 | 3367 | expanded_args.extend([arg + a for a in os.listdir(arg)]) |
|
3366 | 3368 | else: |
|
3367 | 3369 | expanded_args.append(arg) |
|
3368 | 3370 | args = expanded_args |
|
3369 | 3371 | |
|
3370 | 3372 | testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))') |
|
3371 | 3373 | tests = [] |
|
3372 | 3374 | for t in args: |
|
3373 | 3375 | case = [] |
|
3374 | 3376 | |
|
3375 | 3377 | if not ( |
|
3376 | 3378 | os.path.basename(t).startswith(b'test-') |
|
3377 | 3379 | and (t.endswith(b'.py') or t.endswith(b'.t')) |
|
3378 | 3380 | ): |
|
3379 | 3381 | |
|
3380 | 3382 | m = testcasepattern.match(os.path.basename(t)) |
|
3381 | 3383 | if m is not None: |
|
3382 | 3384 | t_basename, casestr = m.groups() |
|
3383 | 3385 | t = os.path.join(os.path.dirname(t), t_basename) |
|
3384 | 3386 | if casestr: |
|
3385 | 3387 | case = casestr.split(b'#') |
|
3386 | 3388 | else: |
|
3387 | 3389 | continue |
|
3388 | 3390 | |
|
3389 | 3391 | if t.endswith(b'.t'): |
|
3390 | 3392 | # .t file may contain multiple test cases |
|
3391 | 3393 | casedimensions = parsettestcases(t) |
|
3392 | 3394 | if casedimensions: |
|
3393 | 3395 | cases = [] |
|
3394 | 3396 | |
|
3395 | 3397 | def addcases(case, casedimensions): |
|
3396 | 3398 | if not casedimensions: |
|
3397 | 3399 | cases.append(case) |
|
3398 | 3400 | else: |
|
3399 | 3401 | for c in casedimensions[0]: |
|
3400 | 3402 | addcases(case + [c], casedimensions[1:]) |
|
3401 | 3403 | |
|
3402 | 3404 | addcases([], casedimensions) |
|
3403 | 3405 | if case and case in cases: |
|
3404 | 3406 | cases = [case] |
|
3405 | 3407 | elif case: |
|
3406 | 3408 | # Ignore invalid cases |
|
3407 | 3409 | cases = [] |
|
3408 | 3410 | else: |
|
3409 | 3411 | pass |
|
3410 | 3412 | tests += [{'path': t, 'case': c} for c in sorted(cases)] |
|
3411 | 3413 | else: |
|
3412 | 3414 | tests.append({'path': t}) |
|
3413 | 3415 | else: |
|
3414 | 3416 | tests.append({'path': t}) |
|
3415 | 3417 | |
|
3416 | 3418 | if self.options.retest: |
|
3417 | 3419 | retest_args = [] |
|
3418 | 3420 | for test in tests: |
|
3419 | 3421 | errpath = self._geterrpath(test) |
|
3420 | 3422 | if os.path.exists(errpath): |
|
3421 | 3423 | retest_args.append(test) |
|
3422 | 3424 | tests = retest_args |
|
3423 | 3425 | return tests |
|
3424 | 3426 | |
|
3425 | 3427 | def _runtests(self, testdescs): |
|
3426 | 3428 | def _reloadtest(test, i): |
|
3427 | 3429 | # convert a test back to its description dict |
|
3428 | 3430 | desc = {'path': test.path} |
|
3429 | 3431 | case = getattr(test, '_case', []) |
|
3430 | 3432 | if case: |
|
3431 | 3433 | desc['case'] = case |
|
3432 | 3434 | return self._gettest(desc, i) |
|
3433 | 3435 | |
|
3434 | 3436 | try: |
|
3435 | 3437 | if self.options.restart: |
|
3436 | 3438 | orig = list(testdescs) |
|
3437 | 3439 | while testdescs: |
|
3438 | 3440 | desc = testdescs[0] |
|
3439 | 3441 | errpath = self._geterrpath(desc) |
|
3440 | 3442 | if os.path.exists(errpath): |
|
3441 | 3443 | break |
|
3442 | 3444 | testdescs.pop(0) |
|
3443 | 3445 | if not testdescs: |
|
3444 | 3446 | print("running all tests") |
|
3445 | 3447 | testdescs = orig |
|
3446 | 3448 | |
|
3447 | 3449 | tests = [self._gettest(d, i) for i, d in enumerate(testdescs)] |
|
3448 | 3450 | num_tests = len(tests) * self.options.runs_per_test |
|
3449 | 3451 | |
|
3450 | 3452 | jobs = min(num_tests, self.options.jobs) |
|
3451 | 3453 | |
|
3452 | 3454 | failed = False |
|
3453 | 3455 | kws = self.options.keywords |
|
3454 | 3456 | if kws is not None and PYTHON3: |
|
3455 | 3457 | kws = kws.encode('utf-8') |
|
3456 | 3458 | |
|
3457 | 3459 | suite = TestSuite( |
|
3458 | 3460 | self._testdir, |
|
3459 | 3461 | jobs=jobs, |
|
3460 | 3462 | whitelist=self.options.whitelisted, |
|
3461 | 3463 | blacklist=self.options.blacklist, |
|
3462 | 3464 | keywords=kws, |
|
3463 | 3465 | loop=self.options.loop, |
|
3464 | 3466 | runs_per_test=self.options.runs_per_test, |
|
3465 | 3467 | showchannels=self.options.showchannels, |
|
3466 | 3468 | tests=tests, |
|
3467 | 3469 | loadtest=_reloadtest, |
|
3468 | 3470 | ) |
|
3469 | 3471 | verbosity = 1 |
|
3470 | 3472 | if self.options.list_tests: |
|
3471 | 3473 | verbosity = 0 |
|
3472 | 3474 | elif self.options.verbose: |
|
3473 | 3475 | verbosity = 2 |
|
3474 | 3476 | runner = TextTestRunner(self, verbosity=verbosity) |
|
3475 | 3477 | |
|
3476 | 3478 | if self.options.list_tests: |
|
3477 | 3479 | result = runner.listtests(suite) |
|
3478 | 3480 | else: |
|
3479 | 3481 | self._usecorrectpython() |
|
3480 | 3482 | if self._installdir: |
|
3481 | 3483 | self._installhg() |
|
3482 | 3484 | self._checkhglib("Testing") |
|
3483 | 3485 | if self.options.chg: |
|
3484 | 3486 | assert self._installdir |
|
3485 | 3487 | self._installchg() |
|
3486 | 3488 | if self.options.rhg: |
|
3487 | 3489 | assert self._installdir |
|
3488 | 3490 | self._installrhg() |
|
3489 | 3491 | elif self.options.pyoxidized: |
|
3490 | 3492 | self._build_pyoxidized() |
|
3491 | 3493 | self._use_correct_mercurial() |
|
3492 | 3494 | |
|
3493 | 3495 | log( |
|
3494 | 3496 | 'running %d tests using %d parallel processes' |
|
3495 | 3497 | % (num_tests, jobs) |
|
3496 | 3498 | ) |
|
3497 | 3499 | |
|
3498 | 3500 | result = runner.run(suite) |
|
3499 | 3501 | |
|
3500 | 3502 | if result.failures or result.errors: |
|
3501 | 3503 | failed = True |
|
3502 | 3504 | |
|
3503 | 3505 | result.onEnd() |
|
3504 | 3506 | |
|
3505 | 3507 | if self.options.anycoverage: |
|
3506 | 3508 | self._outputcoverage() |
|
3507 | 3509 | except KeyboardInterrupt: |
|
3508 | 3510 | failed = True |
|
3509 | 3511 | print("\ninterrupted!") |
|
3510 | 3512 | |
|
3511 | 3513 | if failed: |
|
3512 | 3514 | return 1 |
|
3513 | 3515 | |
|
3514 | 3516 | def _geterrpath(self, test): |
|
3515 | 3517 | # test['path'] is a relative path |
|
3516 | 3518 | if 'case' in test: |
|
3517 | 3519 | # for multiple dimensions test cases |
|
3518 | 3520 | casestr = b'#'.join(test['case']) |
|
3519 | 3521 | errpath = b'%s#%s.err' % (test['path'], casestr) |
|
3520 | 3522 | else: |
|
3521 | 3523 | errpath = b'%s.err' % test['path'] |
|
3522 | 3524 | if self.options.outputdir: |
|
3523 | 3525 | self._outputdir = canonpath(_sys2bytes(self.options.outputdir)) |
|
3524 | 3526 | errpath = os.path.join(self._outputdir, errpath) |
|
3525 | 3527 | return errpath |
|
3526 | 3528 | |
|
3527 | 3529 | def _getport(self, count): |
|
3528 | 3530 | port = self._ports.get(count) # do we have a cached entry? |
|
3529 | 3531 | if port is None: |
|
3530 | 3532 | portneeded = 3 |
|
3531 | 3533 | # above 100 tries we just give up and let test reports failure |
|
3532 | 3534 | for tries in xrange(100): |
|
3533 | 3535 | allfree = True |
|
3534 | 3536 | port = self.options.port + self._portoffset |
|
3535 | 3537 | for idx in xrange(portneeded): |
|
3536 | 3538 | if not checkportisavailable(port + idx): |
|
3537 | 3539 | allfree = False |
|
3538 | 3540 | break |
|
3539 | 3541 | self._portoffset += portneeded |
|
3540 | 3542 | if allfree: |
|
3541 | 3543 | break |
|
3542 | 3544 | self._ports[count] = port |
|
3543 | 3545 | return port |
|
3544 | 3546 | |
|
3545 | 3547 | def _gettest(self, testdesc, count): |
|
3546 | 3548 | """Obtain a Test by looking at its filename. |
|
3547 | 3549 | |
|
3548 | 3550 | Returns a Test instance. The Test may not be runnable if it doesn't |
|
3549 | 3551 | map to a known type. |
|
3550 | 3552 | """ |
|
3551 | 3553 | path = testdesc['path'] |
|
3552 | 3554 | lctest = path.lower() |
|
3553 | 3555 | testcls = Test |
|
3554 | 3556 | |
|
3555 | 3557 | for ext, cls in self.TESTTYPES: |
|
3556 | 3558 | if lctest.endswith(ext): |
|
3557 | 3559 | testcls = cls |
|
3558 | 3560 | break |
|
3559 | 3561 | |
|
3560 | 3562 | refpath = os.path.join(getcwdb(), path) |
|
3561 | 3563 | tmpdir = os.path.join(self._hgtmp, b'child%d' % count) |
|
3562 | 3564 | |
|
3563 | 3565 | # extra keyword parameters. 'case' is used by .t tests |
|
3564 | 3566 | kwds = {k: testdesc[k] for k in ['case'] if k in testdesc} |
|
3565 | 3567 | |
|
3566 | 3568 | t = testcls( |
|
3567 | 3569 | refpath, |
|
3568 | 3570 | self._outputdir, |
|
3569 | 3571 | tmpdir, |
|
3570 | 3572 | keeptmpdir=self.options.keep_tmpdir, |
|
3571 | 3573 | debug=self.options.debug, |
|
3572 | 3574 | first=self.options.first, |
|
3573 | 3575 | timeout=self.options.timeout, |
|
3574 | 3576 | startport=self._getport(count), |
|
3575 | 3577 | extraconfigopts=self.options.extra_config_opt, |
|
3576 | 3578 | shell=self.options.shell, |
|
3577 | 3579 | hgcommand=self._hgcommand, |
|
3578 | 3580 | usechg=bool(self.options.with_chg or self.options.chg), |
|
3579 | 3581 | chgdebug=self.options.chg_debug, |
|
3580 | 3582 | useipv6=useipv6, |
|
3581 | 3583 | **kwds |
|
3582 | 3584 | ) |
|
3583 | 3585 | t.should_reload = True |
|
3584 | 3586 | return t |
|
3585 | 3587 | |
|
3586 | 3588 | def _cleanup(self): |
|
3587 | 3589 | """Clean up state from this test invocation.""" |
|
3588 | 3590 | if self.options.keep_tmpdir: |
|
3589 | 3591 | return |
|
3590 | 3592 | |
|
3591 | 3593 | vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp)) |
|
3592 | 3594 | shutil.rmtree(self._hgtmp, True) |
|
3593 | 3595 | for f in self._createdfiles: |
|
3594 | 3596 | try: |
|
3595 | 3597 | os.remove(f) |
|
3596 | 3598 | except OSError: |
|
3597 | 3599 | pass |
|
3598 | 3600 | |
|
3599 | 3601 | def _usecorrectpython(self): |
|
3600 | 3602 | """Configure the environment to use the appropriate Python in tests.""" |
|
3601 | 3603 | # Tests must use the same interpreter as us or bad things will happen. |
|
3602 | 3604 | if WINDOWS and PYTHON3: |
|
3603 | 3605 | pyexe_names = [b'python', b'python3', b'python.exe'] |
|
3604 | 3606 | elif WINDOWS: |
|
3605 | 3607 | pyexe_names = [b'python', b'python.exe'] |
|
3606 | 3608 | elif PYTHON3: |
|
3607 | 3609 | pyexe_names = [b'python', b'python3'] |
|
3608 | 3610 | else: |
|
3609 | 3611 | pyexe_names = [b'python', b'python2'] |
|
3610 | 3612 | |
|
3611 | 3613 | # os.symlink() is a thing with py3 on Windows, but it requires |
|
3612 | 3614 | # Administrator rights. |
|
3613 | 3615 | if not WINDOWS and getattr(os, 'symlink', None): |
|
3614 | 3616 | msg = "# Making python executable in test path a symlink to '%s'" |
|
3615 | 3617 | msg %= sysexecutable |
|
3616 | 3618 | vlog(msg) |
|
3617 | 3619 | for pyexename in pyexe_names: |
|
3618 | 3620 | mypython = os.path.join(self._custom_bin_dir, pyexename) |
|
3619 | 3621 | try: |
|
3620 | 3622 | if os.readlink(mypython) == sysexecutable: |
|
3621 | 3623 | continue |
|
3622 | 3624 | os.unlink(mypython) |
|
3623 | 3625 | except OSError as err: |
|
3624 | 3626 | if err.errno != errno.ENOENT: |
|
3625 | 3627 | raise |
|
3626 | 3628 | if self._findprogram(pyexename) != sysexecutable: |
|
3627 | 3629 | try: |
|
3628 | 3630 | os.symlink(sysexecutable, mypython) |
|
3629 | 3631 | self._createdfiles.append(mypython) |
|
3630 | 3632 | except OSError as err: |
|
3631 | 3633 | # child processes may race, which is harmless |
|
3632 | 3634 | if err.errno != errno.EEXIST: |
|
3633 | 3635 | raise |
|
3634 | 3636 | elif WINDOWS and not os.getenv('MSYSTEM'): |
|
3635 | 3637 | raise AssertionError('cannot run test on Windows without MSYSTEM') |
|
3636 | 3638 | else: |
|
3637 | 3639 | # Generate explicit file instead of symlink |
|
3638 | 3640 | # |
|
3639 | 3641 | # This is especially important as Windows doesn't have |
|
3640 | 3642 | # `python3.exe`, and MSYS cannot understand the reparse point with |
|
3641 | 3643 | # that name provided by Microsoft. Create a simple script on PATH |
|
3642 | 3644 | # with that name that delegates to the py3 launcher so the shebang |
|
3643 | 3645 | # lines work. |
|
3644 | 3646 | esc_executable = _sys2bytes(shellquote(sysexecutable)) |
|
3645 | 3647 | for pyexename in pyexe_names: |
|
3646 | 3648 | stub_exec_path = os.path.join(self._custom_bin_dir, pyexename) |
|
3647 | 3649 | with open(stub_exec_path, 'wb') as f: |
|
3648 | 3650 | f.write(b'#!/bin/sh\n') |
|
3649 | 3651 | f.write(b'%s "$@"\n' % esc_executable) |
|
3650 | 3652 | |
|
3651 | 3653 | if WINDOWS: |
|
3652 | 3654 | if not PYTHON3: |
|
3653 | 3655 | # lets try to build a valid python3 executable for the |
|
3654 | 3656 | # scrip that requires it. |
|
3655 | 3657 | py3exe_name = os.path.join(self._custom_bin_dir, b'python3') |
|
3656 | 3658 | with open(py3exe_name, 'wb') as f: |
|
3657 | 3659 | f.write(b'#!/bin/sh\n') |
|
3658 | 3660 | f.write(b'py -3 "$@"\n') |
|
3659 | 3661 | |
|
3660 | 3662 | # adjust the path to make sur the main python finds it own dll |
|
3661 | 3663 | path = os.environ['PATH'].split(os.pathsep) |
|
3662 | 3664 | main_exec_dir = os.path.dirname(sysexecutable) |
|
3663 | 3665 | extra_paths = [_bytes2sys(self._custom_bin_dir), main_exec_dir] |
|
3664 | 3666 | |
|
3665 | 3667 | # Binaries installed by pip into the user area like pylint.exe may |
|
3666 | 3668 | # not be in PATH by default. |
|
3667 | 3669 | appdata = os.environ.get('APPDATA') |
|
3668 | 3670 | vi = sys.version_info |
|
3669 | 3671 | if appdata is not None: |
|
3670 | 3672 | python_dir = 'Python%d%d' % (vi[0], vi[1]) |
|
3671 | 3673 | scripts_path = [appdata, 'Python', python_dir, 'Scripts'] |
|
3672 | 3674 | if not PYTHON3: |
|
3673 | 3675 | scripts_path = [appdata, 'Python', 'Scripts'] |
|
3674 | 3676 | scripts_dir = os.path.join(*scripts_path) |
|
3675 | 3677 | extra_paths.append(scripts_dir) |
|
3676 | 3678 | |
|
3677 | 3679 | os.environ['PATH'] = os.pathsep.join(extra_paths + path) |
|
3678 | 3680 | |
|
3679 | 3681 | def _use_correct_mercurial(self): |
|
3680 | 3682 | target_exec = os.path.join(self._custom_bin_dir, b'hg') |
|
3681 | 3683 | if self._hgcommand != b'hg': |
|
3682 | 3684 | # shutil.which only accept bytes from 3.8 |
|
3683 | 3685 | real_exec = which(self._hgcommand) |
|
3684 | 3686 | if real_exec is None: |
|
3685 | 3687 | raise ValueError('could not find exec path for "%s"', real_exec) |
|
3686 | 3688 | if real_exec == target_exec: |
|
3687 | 3689 | # do not overwrite something with itself |
|
3688 | 3690 | return |
|
3689 | 3691 | if WINDOWS: |
|
3690 | 3692 | with open(target_exec, 'wb') as f: |
|
3691 | 3693 | f.write(b'#!/bin/sh\n') |
|
3692 | 3694 | escaped_exec = shellquote(_bytes2sys(real_exec)) |
|
3693 | 3695 | f.write(b'%s "$@"\n' % _sys2bytes(escaped_exec)) |
|
3694 | 3696 | else: |
|
3695 | 3697 | os.symlink(real_exec, target_exec) |
|
3696 | 3698 | self._createdfiles.append(target_exec) |
|
3697 | 3699 | |
|
3698 | 3700 | def _installhg(self): |
|
3699 | 3701 | """Install hg into the test environment. |
|
3700 | 3702 | |
|
3701 | 3703 | This will also configure hg with the appropriate testing settings. |
|
3702 | 3704 | """ |
|
3703 | 3705 | vlog("# Performing temporary installation of HG") |
|
3704 | 3706 | installerrs = os.path.join(self._hgtmp, b"install.err") |
|
3705 | 3707 | compiler = '' |
|
3706 | 3708 | if self.options.compiler: |
|
3707 | 3709 | compiler = '--compiler ' + self.options.compiler |
|
3708 | 3710 | setup_opts = b"" |
|
3709 | 3711 | if self.options.pure: |
|
3710 | 3712 | setup_opts = b"--pure" |
|
3711 | 3713 | elif self.options.rust: |
|
3712 | 3714 | setup_opts = b"--rust" |
|
3713 | 3715 | elif self.options.no_rust: |
|
3714 | 3716 | setup_opts = b"--no-rust" |
|
3715 | 3717 | |
|
3716 | 3718 | # Run installer in hg root |
|
3717 | 3719 | script = os.path.realpath(sys.argv[0]) |
|
3718 | 3720 | exe = sysexecutable |
|
3719 | 3721 | if PYTHON3: |
|
3720 | 3722 | compiler = _sys2bytes(compiler) |
|
3721 | 3723 | script = _sys2bytes(script) |
|
3722 | 3724 | exe = _sys2bytes(exe) |
|
3723 | 3725 | hgroot = os.path.dirname(os.path.dirname(script)) |
|
3724 | 3726 | self._hgroot = hgroot |
|
3725 | 3727 | os.chdir(hgroot) |
|
3726 | 3728 | nohome = b'--home=""' |
|
3727 | 3729 | if WINDOWS: |
|
3728 | 3730 | # The --home="" trick works only on OS where os.sep == '/' |
|
3729 | 3731 | # because of a distutils convert_path() fast-path. Avoid it at |
|
3730 | 3732 | # least on Windows for now, deal with .pydistutils.cfg bugs |
|
3731 | 3733 | # when they happen. |
|
3732 | 3734 | nohome = b'' |
|
3733 | 3735 | cmd = ( |
|
3734 | 3736 | b'"%(exe)s" setup.py %(setup_opts)s clean --all' |
|
3735 | 3737 | b' build %(compiler)s --build-base="%(base)s"' |
|
3736 | 3738 | b' install --force --prefix="%(prefix)s"' |
|
3737 | 3739 | b' --install-lib="%(libdir)s"' |
|
3738 | 3740 | b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1' |
|
3739 | 3741 | % { |
|
3740 | 3742 | b'exe': exe, |
|
3741 | 3743 | b'setup_opts': setup_opts, |
|
3742 | 3744 | b'compiler': compiler, |
|
3743 | 3745 | b'base': os.path.join(self._hgtmp, b"build"), |
|
3744 | 3746 | b'prefix': self._installdir, |
|
3745 | 3747 | b'libdir': self._pythondir, |
|
3746 | 3748 | b'bindir': self._bindir, |
|
3747 | 3749 | b'nohome': nohome, |
|
3748 | 3750 | b'logfile': installerrs, |
|
3749 | 3751 | } |
|
3750 | 3752 | ) |
|
3751 | 3753 | |
|
3752 | 3754 | # setuptools requires install directories to exist. |
|
3753 | 3755 | def makedirs(p): |
|
3754 | 3756 | try: |
|
3755 | 3757 | os.makedirs(p) |
|
3756 | 3758 | except OSError as e: |
|
3757 | 3759 | if e.errno != errno.EEXIST: |
|
3758 | 3760 | raise |
|
3759 | 3761 | |
|
3760 | 3762 | makedirs(self._pythondir) |
|
3761 | 3763 | makedirs(self._bindir) |
|
3762 | 3764 | |
|
3763 | 3765 | vlog("# Running", cmd.decode("utf-8")) |
|
3764 | 3766 | if subprocess.call(_bytes2sys(cmd), shell=True) == 0: |
|
3765 | 3767 | if not self.options.verbose: |
|
3766 | 3768 | try: |
|
3767 | 3769 | os.remove(installerrs) |
|
3768 | 3770 | except OSError as e: |
|
3769 | 3771 | if e.errno != errno.ENOENT: |
|
3770 | 3772 | raise |
|
3771 | 3773 | else: |
|
3772 | 3774 | with open(installerrs, 'rb') as f: |
|
3773 | 3775 | for line in f: |
|
3774 | 3776 | if PYTHON3: |
|
3775 | 3777 | sys.stdout.buffer.write(line) |
|
3776 | 3778 | else: |
|
3777 | 3779 | sys.stdout.write(line) |
|
3778 | 3780 | sys.exit(1) |
|
3779 | 3781 | os.chdir(self._testdir) |
|
3780 | 3782 | |
|
3781 | 3783 | hgbat = os.path.join(self._bindir, b'hg.bat') |
|
3782 | 3784 | if os.path.isfile(hgbat): |
|
3783 | 3785 | # hg.bat expects to be put in bin/scripts while run-tests.py |
|
3784 | 3786 | # installation layout put it in bin/ directly. Fix it |
|
3785 | 3787 | with open(hgbat, 'rb') as f: |
|
3786 | 3788 | data = f.read() |
|
3787 | 3789 | if br'"%~dp0..\python" "%~dp0hg" %*' in data: |
|
3788 | 3790 | data = data.replace( |
|
3789 | 3791 | br'"%~dp0..\python" "%~dp0hg" %*', |
|
3790 | 3792 | b'"%~dp0python" "%~dp0hg" %*', |
|
3791 | 3793 | ) |
|
3792 | 3794 | with open(hgbat, 'wb') as f: |
|
3793 | 3795 | f.write(data) |
|
3794 | 3796 | else: |
|
3795 | 3797 | print('WARNING: cannot fix hg.bat reference to python.exe') |
|
3796 | 3798 | |
|
3797 | 3799 | if self.options.anycoverage: |
|
3798 | 3800 | custom = os.path.join( |
|
3799 | 3801 | osenvironb[b'RUNTESTDIR'], b'sitecustomize.py' |
|
3800 | 3802 | ) |
|
3801 | 3803 | target = os.path.join(self._pythondir, b'sitecustomize.py') |
|
3802 | 3804 | vlog('# Installing coverage trigger to %s' % target) |
|
3803 | 3805 | shutil.copyfile(custom, target) |
|
3804 | 3806 | rc = os.path.join(self._testdir, b'.coveragerc') |
|
3805 | 3807 | vlog('# Installing coverage rc to %s' % rc) |
|
3806 | 3808 | osenvironb[b'COVERAGE_PROCESS_START'] = rc |
|
3807 | 3809 | covdir = os.path.join(self._installdir, b'..', b'coverage') |
|
3808 | 3810 | try: |
|
3809 | 3811 | os.mkdir(covdir) |
|
3810 | 3812 | except OSError as e: |
|
3811 | 3813 | if e.errno != errno.EEXIST: |
|
3812 | 3814 | raise |
|
3813 | 3815 | |
|
3814 | 3816 | osenvironb[b'COVERAGE_DIR'] = covdir |
|
3815 | 3817 | |
|
3816 | 3818 | def _checkhglib(self, verb): |
|
3817 | 3819 | """Ensure that the 'mercurial' package imported by python is |
|
3818 | 3820 | the one we expect it to be. If not, print a warning to stderr.""" |
|
3819 | 3821 | if self._pythondir_inferred: |
|
3820 | 3822 | # The pythondir has been inferred from --with-hg flag. |
|
3821 | 3823 | # We cannot expect anything sensible here. |
|
3822 | 3824 | return |
|
3823 | 3825 | expecthg = os.path.join(self._pythondir, b'mercurial') |
|
3824 | 3826 | actualhg = self._gethgpath() |
|
3825 | 3827 | if os.path.abspath(actualhg) != os.path.abspath(expecthg): |
|
3826 | 3828 | sys.stderr.write( |
|
3827 | 3829 | 'warning: %s with unexpected mercurial lib: %s\n' |
|
3828 | 3830 | ' (expected %s)\n' % (verb, actualhg, expecthg) |
|
3829 | 3831 | ) |
|
3830 | 3832 | |
|
3831 | 3833 | def _gethgpath(self): |
|
3832 | 3834 | """Return the path to the mercurial package that is actually found by |
|
3833 | 3835 | the current Python interpreter.""" |
|
3834 | 3836 | if self._hgpath is not None: |
|
3835 | 3837 | return self._hgpath |
|
3836 | 3838 | |
|
3837 | 3839 | cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"' |
|
3838 | 3840 | cmd = cmd % PYTHON |
|
3839 | 3841 | if PYTHON3: |
|
3840 | 3842 | cmd = _bytes2sys(cmd) |
|
3841 | 3843 | |
|
3842 | 3844 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) |
|
3843 | 3845 | out, err = p.communicate() |
|
3844 | 3846 | |
|
3845 | 3847 | self._hgpath = out.strip() |
|
3846 | 3848 | |
|
3847 | 3849 | return self._hgpath |
|
3848 | 3850 | |
|
3849 | 3851 | def _installchg(self): |
|
3850 | 3852 | """Install chg into the test environment""" |
|
3851 | 3853 | vlog('# Performing temporary installation of CHG') |
|
3852 | 3854 | assert os.path.dirname(self._bindir) == self._installdir |
|
3853 | 3855 | assert self._hgroot, 'must be called after _installhg()' |
|
3854 | 3856 | cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % { |
|
3855 | 3857 | b'make': b'make', # TODO: switch by option or environment? |
|
3856 | 3858 | b'prefix': self._installdir, |
|
3857 | 3859 | } |
|
3858 | 3860 | cwd = os.path.join(self._hgroot, b'contrib', b'chg') |
|
3859 | 3861 | vlog("# Running", cmd) |
|
3860 | 3862 | proc = subprocess.Popen( |
|
3861 | 3863 | cmd, |
|
3862 | 3864 | shell=True, |
|
3863 | 3865 | cwd=cwd, |
|
3864 | 3866 | stdin=subprocess.PIPE, |
|
3865 | 3867 | stdout=subprocess.PIPE, |
|
3866 | 3868 | stderr=subprocess.STDOUT, |
|
3867 | 3869 | ) |
|
3868 | 3870 | out, _err = proc.communicate() |
|
3869 | 3871 | if proc.returncode != 0: |
|
3870 | 3872 | if PYTHON3: |
|
3871 | 3873 | sys.stdout.buffer.write(out) |
|
3872 | 3874 | else: |
|
3873 | 3875 | sys.stdout.write(out) |
|
3874 | 3876 | sys.exit(1) |
|
3875 | 3877 | |
|
3876 | 3878 | def _installrhg(self): |
|
3877 | 3879 | """Install rhg into the test environment""" |
|
3878 | 3880 | vlog('# Performing temporary installation of rhg') |
|
3879 | 3881 | assert os.path.dirname(self._bindir) == self._installdir |
|
3880 | 3882 | assert self._hgroot, 'must be called after _installhg()' |
|
3881 | 3883 | cmd = b'"%(make)s" install-rhg PREFIX="%(prefix)s"' % { |
|
3882 | 3884 | b'make': b'make', # TODO: switch by option or environment? |
|
3883 | 3885 | b'prefix': self._installdir, |
|
3884 | 3886 | } |
|
3885 | 3887 | cwd = self._hgroot |
|
3886 | 3888 | vlog("# Running", cmd) |
|
3887 | 3889 | proc = subprocess.Popen( |
|
3888 | 3890 | cmd, |
|
3889 | 3891 | shell=True, |
|
3890 | 3892 | cwd=cwd, |
|
3891 | 3893 | stdin=subprocess.PIPE, |
|
3892 | 3894 | stdout=subprocess.PIPE, |
|
3893 | 3895 | stderr=subprocess.STDOUT, |
|
3894 | 3896 | ) |
|
3895 | 3897 | out, _err = proc.communicate() |
|
3896 | 3898 | if proc.returncode != 0: |
|
3897 | 3899 | if PYTHON3: |
|
3898 | 3900 | sys.stdout.buffer.write(out) |
|
3899 | 3901 | else: |
|
3900 | 3902 | sys.stdout.write(out) |
|
3901 | 3903 | sys.exit(1) |
|
3902 | 3904 | |
|
3903 | 3905 | def _build_pyoxidized(self): |
|
3904 | 3906 | """build a pyoxidized version of mercurial into the test environment |
|
3905 | 3907 | |
|
3906 | 3908 | Ideally this function would be `install_pyoxidier` and would both build |
|
3907 | 3909 | and install pyoxidier. However we are starting small to get pyoxidizer |
|
3908 | 3910 | build binary to testing quickly. |
|
3909 | 3911 | """ |
|
3910 | 3912 | vlog('# build a pyoxidized version of Mercurial') |
|
3911 | 3913 | assert os.path.dirname(self._bindir) == self._installdir |
|
3912 | 3914 | assert self._hgroot, 'must be called after _installhg()' |
|
3913 | 3915 | cmd = b'"%(make)s" pyoxidizer-windows-tests' % { |
|
3914 | 3916 | b'make': b'make', |
|
3915 | 3917 | } |
|
3916 | 3918 | cwd = self._hgroot |
|
3917 | 3919 | vlog("# Running", cmd) |
|
3918 | 3920 | proc = subprocess.Popen( |
|
3919 | 3921 | _bytes2sys(cmd), |
|
3920 | 3922 | shell=True, |
|
3921 | 3923 | cwd=_bytes2sys(cwd), |
|
3922 | 3924 | stdin=subprocess.PIPE, |
|
3923 | 3925 | stdout=subprocess.PIPE, |
|
3924 | 3926 | stderr=subprocess.STDOUT, |
|
3925 | 3927 | ) |
|
3926 | 3928 | out, _err = proc.communicate() |
|
3927 | 3929 | if proc.returncode != 0: |
|
3928 | 3930 | if PYTHON3: |
|
3929 | 3931 | sys.stdout.buffer.write(out) |
|
3930 | 3932 | else: |
|
3931 | 3933 | sys.stdout.write(out) |
|
3932 | 3934 | sys.exit(1) |
|
3933 | 3935 | |
|
3934 | 3936 | def _outputcoverage(self): |
|
3935 | 3937 | """Produce code coverage output.""" |
|
3936 | 3938 | import coverage |
|
3937 | 3939 | |
|
3938 | 3940 | coverage = coverage.coverage |
|
3939 | 3941 | |
|
3940 | 3942 | vlog('# Producing coverage report') |
|
3941 | 3943 | # chdir is the easiest way to get short, relative paths in the |
|
3942 | 3944 | # output. |
|
3943 | 3945 | os.chdir(self._hgroot) |
|
3944 | 3946 | covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage') |
|
3945 | 3947 | cov = coverage(data_file=os.path.join(covdir, 'cov')) |
|
3946 | 3948 | |
|
3947 | 3949 | # Map install directory paths back to source directory. |
|
3948 | 3950 | cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)] |
|
3949 | 3951 | |
|
3950 | 3952 | cov.combine() |
|
3951 | 3953 | |
|
3952 | 3954 | omit = [ |
|
3953 | 3955 | _bytes2sys(os.path.join(x, b'*')) |
|
3954 | 3956 | for x in [self._bindir, self._testdir] |
|
3955 | 3957 | ] |
|
3956 | 3958 | cov.report(ignore_errors=True, omit=omit) |
|
3957 | 3959 | |
|
3958 | 3960 | if self.options.htmlcov: |
|
3959 | 3961 | htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov') |
|
3960 | 3962 | cov.html_report(directory=htmldir, omit=omit) |
|
3961 | 3963 | if self.options.annotate: |
|
3962 | 3964 | adir = os.path.join(_bytes2sys(self._outputdir), 'annotated') |
|
3963 | 3965 | if not os.path.isdir(adir): |
|
3964 | 3966 | os.mkdir(adir) |
|
3965 | 3967 | cov.annotate(directory=adir, omit=omit) |
|
3966 | 3968 | |
|
3967 | 3969 | def _findprogram(self, program): |
|
3968 | 3970 | """Search PATH for a executable program""" |
|
3969 | 3971 | dpb = _sys2bytes(os.defpath) |
|
3970 | 3972 | sepb = _sys2bytes(os.pathsep) |
|
3971 | 3973 | for p in osenvironb.get(b'PATH', dpb).split(sepb): |
|
3972 | 3974 | name = os.path.join(p, program) |
|
3973 | 3975 | if WINDOWS or os.access(name, os.X_OK): |
|
3974 | 3976 | return _bytes2sys(name) |
|
3975 | 3977 | return None |
|
3976 | 3978 | |
|
3977 | 3979 | def _checktools(self): |
|
3978 | 3980 | """Ensure tools required to run tests are present.""" |
|
3979 | 3981 | for p in self.REQUIREDTOOLS: |
|
3980 | 3982 | if WINDOWS and not p.endswith(b'.exe'): |
|
3981 | 3983 | p += b'.exe' |
|
3982 | 3984 | found = self._findprogram(p) |
|
3983 | 3985 | p = p.decode("utf-8") |
|
3984 | 3986 | if found: |
|
3985 | 3987 | vlog("# Found prerequisite", p, "at", found) |
|
3986 | 3988 | else: |
|
3987 | 3989 | print("WARNING: Did not find prerequisite tool: %s " % p) |
|
3988 | 3990 | |
|
3989 | 3991 | |
|
3990 | 3992 | def aggregateexceptions(path): |
|
3991 | 3993 | exceptioncounts = collections.Counter() |
|
3992 | 3994 | testsbyfailure = collections.defaultdict(set) |
|
3993 | 3995 | failuresbytest = collections.defaultdict(set) |
|
3994 | 3996 | |
|
3995 | 3997 | for f in os.listdir(path): |
|
3996 | 3998 | with open(os.path.join(path, f), 'rb') as fh: |
|
3997 | 3999 | data = fh.read().split(b'\0') |
|
3998 | 4000 | if len(data) != 5: |
|
3999 | 4001 | continue |
|
4000 | 4002 | |
|
4001 | 4003 | exc, mainframe, hgframe, hgline, testname = data |
|
4002 | 4004 | exc = exc.decode('utf-8') |
|
4003 | 4005 | mainframe = mainframe.decode('utf-8') |
|
4004 | 4006 | hgframe = hgframe.decode('utf-8') |
|
4005 | 4007 | hgline = hgline.decode('utf-8') |
|
4006 | 4008 | testname = testname.decode('utf-8') |
|
4007 | 4009 | |
|
4008 | 4010 | key = (hgframe, hgline, exc) |
|
4009 | 4011 | exceptioncounts[key] += 1 |
|
4010 | 4012 | testsbyfailure[key].add(testname) |
|
4011 | 4013 | failuresbytest[testname].add(key) |
|
4012 | 4014 | |
|
4013 | 4015 | # Find test having fewest failures for each failure. |
|
4014 | 4016 | leastfailing = {} |
|
4015 | 4017 | for key, tests in testsbyfailure.items(): |
|
4016 | 4018 | fewesttest = None |
|
4017 | 4019 | fewestcount = 99999999 |
|
4018 | 4020 | for test in sorted(tests): |
|
4019 | 4021 | if len(failuresbytest[test]) < fewestcount: |
|
4020 | 4022 | fewesttest = test |
|
4021 | 4023 | fewestcount = len(failuresbytest[test]) |
|
4022 | 4024 | |
|
4023 | 4025 | leastfailing[key] = (fewestcount, fewesttest) |
|
4024 | 4026 | |
|
4025 | 4027 | # Create a combined counter so we can sort by total occurrences and |
|
4026 | 4028 | # impacted tests. |
|
4027 | 4029 | combined = {} |
|
4028 | 4030 | for key in exceptioncounts: |
|
4029 | 4031 | combined[key] = ( |
|
4030 | 4032 | exceptioncounts[key], |
|
4031 | 4033 | len(testsbyfailure[key]), |
|
4032 | 4034 | leastfailing[key][0], |
|
4033 | 4035 | leastfailing[key][1], |
|
4034 | 4036 | ) |
|
4035 | 4037 | |
|
4036 | 4038 | return { |
|
4037 | 4039 | 'exceptioncounts': exceptioncounts, |
|
4038 | 4040 | 'total': sum(exceptioncounts.values()), |
|
4039 | 4041 | 'combined': combined, |
|
4040 | 4042 | 'leastfailing': leastfailing, |
|
4041 | 4043 | 'byfailure': testsbyfailure, |
|
4042 | 4044 | 'bytest': failuresbytest, |
|
4043 | 4045 | } |
|
4044 | 4046 | |
|
4045 | 4047 | |
|
4046 | 4048 | if __name__ == '__main__': |
|
4047 | 4049 | if WINDOWS and not os.getenv('MSYSTEM'): |
|
4048 | 4050 | print('cannot run test on Windows without MSYSTEM', file=sys.stderr) |
|
4049 | 4051 | print( |
|
4050 | 4052 | '(if you need to do so contact the mercurial devs: ' |
|
4051 | 4053 | 'mercurial@mercurial-scm.org)', |
|
4052 | 4054 | file=sys.stderr, |
|
4053 | 4055 | ) |
|
4054 | 4056 | sys.exit(255) |
|
4055 | 4057 | |
|
4056 | 4058 | runner = TestRunner() |
|
4057 | 4059 | |
|
4058 | 4060 | try: |
|
4059 | 4061 | import msvcrt |
|
4060 | 4062 | |
|
4061 | 4063 | msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) |
|
4062 | 4064 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) |
|
4063 | 4065 | msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) |
|
4064 | 4066 | except ImportError: |
|
4065 | 4067 | pass |
|
4066 | 4068 | |
|
4067 | 4069 | sys.exit(runner.run(sys.argv[1:])) |
@@ -1,256 +1,257 b'' | |||
|
1 | 1 | Create a repository: |
|
2 | 2 | |
|
3 | 3 | #if no-extraextensions |
|
4 | 4 | $ hg config |
|
5 | 5 | chgserver.idletimeout=60 |
|
6 | 6 | devel.all-warnings=true |
|
7 | 7 | devel.default-date=0 0 |
|
8 | 8 | extensions.fsmonitor= (fsmonitor !) |
|
9 | 9 | format.exp-dirstate-v2=1 (dirstate-v2 !) |
|
10 | 10 | largefiles.usercache=$TESTTMP/.cache/largefiles |
|
11 | 11 | lfs.usercache=$TESTTMP/.cache/lfs |
|
12 | 12 | ui.slash=True |
|
13 | 13 | ui.interactive=False |
|
14 | 14 | ui.detailed-exit-code=True |
|
15 | 15 | ui.merge=internal:merge |
|
16 | 16 | ui.mergemarkers=detailed |
|
17 | 17 | ui.promptecho=True |
|
18 | ui.ssh=* (glob) | |
|
18 | 19 | ui.timeout.warn=15 |
|
19 | 20 | web.address=localhost |
|
20 | 21 | web\.ipv6=(?:True|False) (re) |
|
21 | 22 | web.server-header=testing stub value |
|
22 | 23 | #endif |
|
23 | 24 | |
|
24 | 25 | $ hg init t |
|
25 | 26 | $ cd t |
|
26 | 27 | |
|
27 | 28 | Prepare a changeset: |
|
28 | 29 | |
|
29 | 30 | $ echo a > a |
|
30 | 31 | $ hg add a |
|
31 | 32 | |
|
32 | 33 | $ hg status |
|
33 | 34 | A a |
|
34 | 35 | |
|
35 | 36 | Writes to stdio succeed and fail appropriately |
|
36 | 37 | |
|
37 | 38 | #if devfull |
|
38 | 39 | $ hg status 2>/dev/full |
|
39 | 40 | A a |
|
40 | 41 | |
|
41 | 42 | $ hg status >/dev/full |
|
42 | 43 | abort: No space left on device |
|
43 | 44 | [255] |
|
44 | 45 | #endif |
|
45 | 46 | |
|
46 | 47 | #if devfull |
|
47 | 48 | $ hg status >/dev/full 2>&1 |
|
48 | 49 | [255] |
|
49 | 50 | |
|
50 | 51 | $ hg status ENOENT 2>/dev/full |
|
51 | 52 | [255] |
|
52 | 53 | #endif |
|
53 | 54 | |
|
54 | 55 | On Python 3, stdio may be None: |
|
55 | 56 | |
|
56 | 57 | $ hg debuguiprompt --config ui.interactive=true 0<&- |
|
57 | 58 | abort: Bad file descriptor |
|
58 | 59 | [255] |
|
59 | 60 | $ hg version -q 0<&- |
|
60 | 61 | Mercurial Distributed SCM * (glob) |
|
61 | 62 | |
|
62 | 63 | #if py3 |
|
63 | 64 | $ hg version -q 1>&- |
|
64 | 65 | abort: Bad file descriptor |
|
65 | 66 | [255] |
|
66 | 67 | #else |
|
67 | 68 | $ hg version -q 1>&- |
|
68 | 69 | #endif |
|
69 | 70 | $ hg unknown -q 1>&- |
|
70 | 71 | hg: unknown command 'unknown' |
|
71 | 72 | (did you mean debugknown?) |
|
72 | 73 | [10] |
|
73 | 74 | |
|
74 | 75 | $ hg version -q 2>&- |
|
75 | 76 | Mercurial Distributed SCM * (glob) |
|
76 | 77 | $ hg unknown -q 2>&- |
|
77 | 78 | [10] |
|
78 | 79 | |
|
79 | 80 | $ hg commit -m test |
|
80 | 81 | |
|
81 | 82 | This command is ancient: |
|
82 | 83 | |
|
83 | 84 | $ hg history |
|
84 | 85 | changeset: 0:acb14030fe0a |
|
85 | 86 | tag: tip |
|
86 | 87 | user: test |
|
87 | 88 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
88 | 89 | summary: test |
|
89 | 90 | |
|
90 | 91 | |
|
91 | 92 | Verify that updating to revision 0 via commands.update() works properly |
|
92 | 93 | |
|
93 | 94 | $ cat <<EOF > update_to_rev0.py |
|
94 | 95 | > from mercurial import commands, hg, ui as uimod |
|
95 | 96 | > myui = uimod.ui.load() |
|
96 | 97 | > repo = hg.repository(myui, path=b'.') |
|
97 | 98 | > commands.update(myui, repo, rev=b"0") |
|
98 | 99 | > EOF |
|
99 | 100 | $ hg up null |
|
100 | 101 | 0 files updated, 0 files merged, 1 files removed, 0 files unresolved |
|
101 | 102 | $ "$PYTHON" ./update_to_rev0.py |
|
102 | 103 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
103 | 104 | $ hg identify -n |
|
104 | 105 | 0 |
|
105 | 106 | |
|
106 | 107 | |
|
107 | 108 | Poke around at hashes: |
|
108 | 109 | |
|
109 | 110 | $ hg manifest --debug |
|
110 | 111 | b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 a |
|
111 | 112 | |
|
112 | 113 | $ hg cat a |
|
113 | 114 | a |
|
114 | 115 | |
|
115 | 116 | Verify should succeed: |
|
116 | 117 | |
|
117 | 118 | $ hg verify |
|
118 | 119 | checking changesets |
|
119 | 120 | checking manifests |
|
120 | 121 | crosschecking files in changesets and manifests |
|
121 | 122 | checking files |
|
122 | 123 | checked 1 changesets with 1 changes to 1 files |
|
123 | 124 | |
|
124 | 125 | Repository root: |
|
125 | 126 | |
|
126 | 127 | $ hg root |
|
127 | 128 | $TESTTMP/t |
|
128 | 129 | $ hg log -l1 -T '{reporoot}\n' |
|
129 | 130 | $TESTTMP/t |
|
130 | 131 | $ hg root -Tjson | sed 's|\\\\|\\|g' |
|
131 | 132 | [ |
|
132 | 133 | { |
|
133 | 134 | "hgpath": "$TESTTMP/t/.hg", |
|
134 | 135 | "reporoot": "$TESTTMP/t", |
|
135 | 136 | "storepath": "$TESTTMP/t/.hg/store" |
|
136 | 137 | } |
|
137 | 138 | ] |
|
138 | 139 | |
|
139 | 140 | At the end... |
|
140 | 141 | |
|
141 | 142 | $ cd .. |
|
142 | 143 | |
|
143 | 144 | Status message redirection: |
|
144 | 145 | |
|
145 | 146 | $ hg init empty |
|
146 | 147 | |
|
147 | 148 | status messages are sent to stdout by default: |
|
148 | 149 | |
|
149 | 150 | $ hg outgoing -R t empty -Tjson 2>/dev/null |
|
150 | 151 | comparing with empty |
|
151 | 152 | searching for changes |
|
152 | 153 | [ |
|
153 | 154 | { |
|
154 | 155 | "bookmarks": [], |
|
155 | 156 | "branch": "default", |
|
156 | 157 | "date": [0, 0], |
|
157 | 158 | "desc": "test", |
|
158 | 159 | "node": "acb14030fe0a21b60322c440ad2d20cf7685a376", |
|
159 | 160 | "parents": ["0000000000000000000000000000000000000000"], |
|
160 | 161 | "phase": "draft", |
|
161 | 162 | "rev": 0, |
|
162 | 163 | "tags": ["tip"], |
|
163 | 164 | "user": "test" |
|
164 | 165 | } |
|
165 | 166 | ] |
|
166 | 167 | |
|
167 | 168 | which can be configured to send to stderr, so the output wouldn't be |
|
168 | 169 | interleaved: |
|
169 | 170 | |
|
170 | 171 | $ cat <<'EOF' >> "$HGRCPATH" |
|
171 | 172 | > [ui] |
|
172 | 173 | > message-output = stderr |
|
173 | 174 | > EOF |
|
174 | 175 | $ hg outgoing -R t empty -Tjson 2>/dev/null |
|
175 | 176 | [ |
|
176 | 177 | { |
|
177 | 178 | "bookmarks": [], |
|
178 | 179 | "branch": "default", |
|
179 | 180 | "date": [0, 0], |
|
180 | 181 | "desc": "test", |
|
181 | 182 | "node": "acb14030fe0a21b60322c440ad2d20cf7685a376", |
|
182 | 183 | "parents": ["0000000000000000000000000000000000000000"], |
|
183 | 184 | "phase": "draft", |
|
184 | 185 | "rev": 0, |
|
185 | 186 | "tags": ["tip"], |
|
186 | 187 | "user": "test" |
|
187 | 188 | } |
|
188 | 189 | ] |
|
189 | 190 | $ hg outgoing -R t empty -Tjson >/dev/null |
|
190 | 191 | comparing with empty |
|
191 | 192 | searching for changes |
|
192 | 193 | |
|
193 | 194 | this option should be turned off by HGPLAIN= since it may break scripting use: |
|
194 | 195 | |
|
195 | 196 | $ HGPLAIN= hg outgoing -R t empty -Tjson 2>/dev/null |
|
196 | 197 | comparing with empty |
|
197 | 198 | searching for changes |
|
198 | 199 | [ |
|
199 | 200 | { |
|
200 | 201 | "bookmarks": [], |
|
201 | 202 | "branch": "default", |
|
202 | 203 | "date": [0, 0], |
|
203 | 204 | "desc": "test", |
|
204 | 205 | "node": "acb14030fe0a21b60322c440ad2d20cf7685a376", |
|
205 | 206 | "parents": ["0000000000000000000000000000000000000000"], |
|
206 | 207 | "phase": "draft", |
|
207 | 208 | "rev": 0, |
|
208 | 209 | "tags": ["tip"], |
|
209 | 210 | "user": "test" |
|
210 | 211 | } |
|
211 | 212 | ] |
|
212 | 213 | |
|
213 | 214 | but still overridden by --config: |
|
214 | 215 | |
|
215 | 216 | $ HGPLAIN= hg outgoing -R t empty -Tjson --config ui.message-output=stderr \ |
|
216 | 217 | > 2>/dev/null |
|
217 | 218 | [ |
|
218 | 219 | { |
|
219 | 220 | "bookmarks": [], |
|
220 | 221 | "branch": "default", |
|
221 | 222 | "date": [0, 0], |
|
222 | 223 | "desc": "test", |
|
223 | 224 | "node": "acb14030fe0a21b60322c440ad2d20cf7685a376", |
|
224 | 225 | "parents": ["0000000000000000000000000000000000000000"], |
|
225 | 226 | "phase": "draft", |
|
226 | 227 | "rev": 0, |
|
227 | 228 | "tags": ["tip"], |
|
228 | 229 | "user": "test" |
|
229 | 230 | } |
|
230 | 231 | ] |
|
231 | 232 | |
|
232 | 233 | Invalid ui.message-output option: |
|
233 | 234 | |
|
234 | 235 | $ hg log -R t --config ui.message-output=bad |
|
235 | 236 | abort: invalid ui.message-output destination: bad |
|
236 | 237 | [255] |
|
237 | 238 | |
|
238 | 239 | Underlying message streams should be updated when ui.fout/ferr are set: |
|
239 | 240 | |
|
240 | 241 | $ cat <<'EOF' > capui.py |
|
241 | 242 | > from mercurial import pycompat, registrar |
|
242 | 243 | > cmdtable = {} |
|
243 | 244 | > command = registrar.command(cmdtable) |
|
244 | 245 | > @command(b'capui', norepo=True) |
|
245 | 246 | > def capui(ui): |
|
246 | 247 | > out = ui.fout |
|
247 | 248 | > ui.fout = pycompat.bytesio() |
|
248 | 249 | > ui.status(b'status\n') |
|
249 | 250 | > ui.ferr = pycompat.bytesio() |
|
250 | 251 | > ui.warn(b'warn\n') |
|
251 | 252 | > out.write(b'stdout: %s' % ui.fout.getvalue()) |
|
252 | 253 | > out.write(b'stderr: %s' % ui.ferr.getvalue()) |
|
253 | 254 | > EOF |
|
254 | 255 | $ hg --config extensions.capui=capui.py --config ui.message-output=stdio capui |
|
255 | 256 | stdout: status |
|
256 | 257 | stderr: warn |
@@ -1,1172 +1,1174 b'' | |||
|
1 | 1 | #require no-rhg no-chg |
|
2 | 2 | |
|
3 | 3 | XXX-RHG this test hangs if `hg` is really `rhg`. This was hidden by the use of |
|
4 | 4 | `alias hg=rhg` by run-tests.py. With such alias removed, this test is revealed |
|
5 | 5 | buggy. This need to be resolved sooner than later. |
|
6 | 6 | |
|
7 | 7 | XXX-CHG this test hangs if `hg` is really `chg`. This was hidden by the use of |
|
8 | 8 | `alias hg=chg` by run-tests.py. With such alias removed, this test is revealed |
|
9 | 9 | buggy. This need to be resolved sooner than later. |
|
10 | 10 | |
|
11 | 11 | #if windows |
|
12 | 12 | $ PYTHONPATH="$TESTDIR/../contrib;$PYTHONPATH" |
|
13 | 13 | #else |
|
14 | 14 | $ PYTHONPATH="$TESTDIR/../contrib:$PYTHONPATH" |
|
15 | 15 | #endif |
|
16 | 16 | $ export PYTHONPATH |
|
17 | 17 | |
|
18 | 18 | typical client does not want echo-back messages, so test without it: |
|
19 | 19 | |
|
20 | 20 | $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new |
|
21 | 21 | $ mv $HGRCPATH.new $HGRCPATH |
|
22 | 22 | |
|
23 | 23 | $ hg init repo |
|
24 | 24 | $ cd repo |
|
25 | 25 | |
|
26 | 26 | >>> from __future__ import absolute_import |
|
27 | 27 | >>> import os |
|
28 | 28 | >>> import sys |
|
29 | 29 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
30 | 30 | >>> @check |
|
31 | 31 | ... def hellomessage(server): |
|
32 | 32 | ... ch, data = readchannel(server) |
|
33 | 33 | ... bprint(b'%c, %r' % (ch, data)) |
|
34 | 34 | ... # run an arbitrary command to make sure the next thing the server |
|
35 | 35 | ... # sends isn't part of the hello message |
|
36 | 36 | ... runcommand(server, [b'id']) |
|
37 | 37 | o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob) |
|
38 | 38 | *** runcommand id |
|
39 | 39 | 000000000000 tip |
|
40 | 40 | |
|
41 | 41 | >>> from hgclient import check |
|
42 | 42 | >>> @check |
|
43 | 43 | ... def unknowncommand(server): |
|
44 | 44 | ... server.stdin.write(b'unknowncommand\n') |
|
45 | 45 | abort: unknown command unknowncommand |
|
46 | 46 | |
|
47 | 47 | >>> from hgclient import check, readchannel, runcommand |
|
48 | 48 | >>> @check |
|
49 | 49 | ... def checkruncommand(server): |
|
50 | 50 | ... # hello block |
|
51 | 51 | ... readchannel(server) |
|
52 | 52 | ... |
|
53 | 53 | ... # no args |
|
54 | 54 | ... runcommand(server, []) |
|
55 | 55 | ... |
|
56 | 56 | ... # global options |
|
57 | 57 | ... runcommand(server, [b'id', b'--quiet']) |
|
58 | 58 | ... |
|
59 | 59 | ... # make sure global options don't stick through requests |
|
60 | 60 | ... runcommand(server, [b'id']) |
|
61 | 61 | ... |
|
62 | 62 | ... # --config |
|
63 | 63 | ... runcommand(server, [b'id', b'--config', b'ui.quiet=True']) |
|
64 | 64 | ... |
|
65 | 65 | ... # make sure --config doesn't stick |
|
66 | 66 | ... runcommand(server, [b'id']) |
|
67 | 67 | ... |
|
68 | 68 | ... # negative return code should be masked |
|
69 | 69 | ... runcommand(server, [b'id', b'-runknown']) |
|
70 | 70 | *** runcommand |
|
71 | 71 | Mercurial Distributed SCM |
|
72 | 72 | |
|
73 | 73 | basic commands: |
|
74 | 74 | |
|
75 | 75 | add add the specified files on the next commit |
|
76 | 76 | annotate show changeset information by line for each file |
|
77 | 77 | clone make a copy of an existing repository |
|
78 | 78 | commit commit the specified files or all outstanding changes |
|
79 | 79 | diff diff repository (or selected files) |
|
80 | 80 | export dump the header and diffs for one or more changesets |
|
81 | 81 | forget forget the specified files on the next commit |
|
82 | 82 | init create a new repository in the given directory |
|
83 | 83 | log show revision history of entire repository or files |
|
84 | 84 | merge merge another revision into working directory |
|
85 | 85 | pull pull changes from the specified source |
|
86 | 86 | push push changes to the specified destination |
|
87 | 87 | remove remove the specified files on the next commit |
|
88 | 88 | serve start stand-alone webserver |
|
89 | 89 | status show changed files in the working directory |
|
90 | 90 | summary summarize working directory state |
|
91 | 91 | update update working directory (or switch revisions) |
|
92 | 92 | |
|
93 | 93 | (use 'hg help' for the full list of commands or 'hg -v' for details) |
|
94 | 94 | *** runcommand id --quiet |
|
95 | 95 | 000000000000 |
|
96 | 96 | *** runcommand id |
|
97 | 97 | 000000000000 tip |
|
98 | 98 | *** runcommand id --config ui.quiet=True |
|
99 | 99 | 000000000000 |
|
100 | 100 | *** runcommand id |
|
101 | 101 | 000000000000 tip |
|
102 | 102 | *** runcommand id -runknown |
|
103 | 103 | abort: unknown revision 'unknown' |
|
104 | 104 | [255] |
|
105 | 105 | |
|
106 | 106 | >>> from hgclient import bprint, check, readchannel |
|
107 | 107 | >>> @check |
|
108 | 108 | ... def inputeof(server): |
|
109 | 109 | ... readchannel(server) |
|
110 | 110 | ... server.stdin.write(b'runcommand\n') |
|
111 | 111 | ... # close stdin while server is waiting for input |
|
112 | 112 | ... server.stdin.close() |
|
113 | 113 | ... |
|
114 | 114 | ... # server exits with 1 if the pipe closed while reading the command |
|
115 | 115 | ... bprint(b'server exit code =', b'%d' % server.wait()) |
|
116 | 116 | server exit code = 1 |
|
117 | 117 | |
|
118 | 118 | >>> from hgclient import check, readchannel, runcommand, stringio |
|
119 | 119 | >>> @check |
|
120 | 120 | ... def serverinput(server): |
|
121 | 121 | ... readchannel(server) |
|
122 | 122 | ... |
|
123 | 123 | ... patch = b""" |
|
124 | 124 | ... # HG changeset patch |
|
125 | 125 | ... # User test |
|
126 | 126 | ... # Date 0 0 |
|
127 | 127 | ... # Node ID c103a3dec114d882c98382d684d8af798d09d857 |
|
128 | 128 | ... # Parent 0000000000000000000000000000000000000000 |
|
129 | 129 | ... 1 |
|
130 | 130 | ... |
|
131 | 131 | ... diff -r 000000000000 -r c103a3dec114 a |
|
132 | 132 | ... --- /dev/null Thu Jan 01 00:00:00 1970 +0000 |
|
133 | 133 | ... +++ b/a Thu Jan 01 00:00:00 1970 +0000 |
|
134 | 134 | ... @@ -0,0 +1,1 @@ |
|
135 | 135 | ... +1 |
|
136 | 136 | ... """ |
|
137 | 137 | ... |
|
138 | 138 | ... runcommand(server, [b'import', b'-'], input=stringio(patch)) |
|
139 | 139 | ... runcommand(server, [b'log']) |
|
140 | 140 | *** runcommand import - |
|
141 | 141 | applying patch from stdin |
|
142 | 142 | *** runcommand log |
|
143 | 143 | changeset: 0:eff892de26ec |
|
144 | 144 | tag: tip |
|
145 | 145 | user: test |
|
146 | 146 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
147 | 147 | summary: 1 |
|
148 | 148 | |
|
149 | 149 | |
|
150 | 150 | check strict parsing of early options: |
|
151 | 151 | |
|
152 | 152 | >>> import os |
|
153 | 153 | >>> from hgclient import check, readchannel, runcommand |
|
154 | 154 | >>> os.environ['HGPLAIN'] = '+strictflags' |
|
155 | 155 | >>> @check |
|
156 | 156 | ... def cwd(server): |
|
157 | 157 | ... readchannel(server) |
|
158 | 158 | ... runcommand(server, [b'log', b'-b', b'--config=alias.log=!echo pwned', |
|
159 | 159 | ... b'default']) |
|
160 | 160 | *** runcommand log -b --config=alias.log=!echo pwned default |
|
161 | 161 | abort: unknown revision '--config=alias.log=!echo pwned' |
|
162 | 162 | [255] |
|
163 | 163 | |
|
164 | 164 | check that "histedit --commands=-" can read rules from the input channel: |
|
165 | 165 | |
|
166 | 166 | >>> from hgclient import check, readchannel, runcommand, stringio |
|
167 | 167 | >>> @check |
|
168 | 168 | ... def serverinput(server): |
|
169 | 169 | ... readchannel(server) |
|
170 | 170 | ... rules = b'pick eff892de26ec\n' |
|
171 | 171 | ... runcommand(server, [b'histedit', b'0', b'--commands=-', |
|
172 | 172 | ... b'--config', b'extensions.histedit='], |
|
173 | 173 | ... input=stringio(rules)) |
|
174 | 174 | *** runcommand histedit 0 --commands=- --config extensions.histedit= |
|
175 | 175 | |
|
176 | 176 | check that --cwd doesn't persist between requests: |
|
177 | 177 | |
|
178 | 178 | $ mkdir foo |
|
179 | 179 | $ touch foo/bar |
|
180 | 180 | >>> from hgclient import check, readchannel, runcommand |
|
181 | 181 | >>> @check |
|
182 | 182 | ... def cwd(server): |
|
183 | 183 | ... readchannel(server) |
|
184 | 184 | ... runcommand(server, [b'--cwd', b'foo', b'st', b'bar']) |
|
185 | 185 | ... runcommand(server, [b'st', b'foo/bar']) |
|
186 | 186 | *** runcommand --cwd foo st bar |
|
187 | 187 | ? bar |
|
188 | 188 | *** runcommand st foo/bar |
|
189 | 189 | ? foo/bar |
|
190 | 190 | |
|
191 | 191 | $ rm foo/bar |
|
192 | 192 | |
|
193 | 193 | |
|
194 | 194 | check that local configs for the cached repo aren't inherited when -R is used: |
|
195 | 195 | |
|
196 | 196 | $ cat <<EOF >> .hg/hgrc |
|
197 | 197 | > [ui] |
|
198 | 198 | > foo = bar |
|
199 | 199 | > EOF |
|
200 | 200 | |
|
201 | 201 | #if no-extraextensions |
|
202 | 202 | |
|
203 | 203 | >>> from hgclient import check, readchannel, runcommand, sep |
|
204 | 204 | >>> @check |
|
205 | 205 | ... def localhgrc(server): |
|
206 | 206 | ... readchannel(server) |
|
207 | 207 | ... |
|
208 | 208 | ... # the cached repo local hgrc contains ui.foo=bar, so showconfig should |
|
209 | 209 | ... # show it |
|
210 | 210 | ... runcommand(server, [b'showconfig'], outfilter=sep) |
|
211 | 211 | ... |
|
212 | 212 | ... # but not for this repo |
|
213 | 213 | ... runcommand(server, [b'init', b'foo']) |
|
214 | 214 | ... runcommand(server, [b'-R', b'foo', b'showconfig', b'ui', b'defaults']) |
|
215 | 215 | *** runcommand showconfig |
|
216 | 216 | bundle.mainreporoot=$TESTTMP/repo |
|
217 | 217 | chgserver.idletimeout=60 |
|
218 | 218 | devel.all-warnings=true |
|
219 | 219 | devel.default-date=0 0 |
|
220 | 220 | extensions.fsmonitor= (fsmonitor !) |
|
221 | 221 | format.exp-dirstate-v2=1 (dirstate-v2 !) |
|
222 | 222 | largefiles.usercache=$TESTTMP/.cache/largefiles |
|
223 | 223 | lfs.usercache=$TESTTMP/.cache/lfs |
|
224 | 224 | ui.slash=True |
|
225 | 225 | ui.interactive=False |
|
226 | 226 | ui.detailed-exit-code=True |
|
227 | 227 | ui.merge=internal:merge |
|
228 | 228 | ui.mergemarkers=detailed |
|
229 | ui.ssh=* (glob) | |
|
229 | 230 | ui.timeout.warn=15 |
|
230 | 231 | ui.foo=bar |
|
231 | 232 | ui.nontty=true |
|
232 | 233 | web.address=localhost |
|
233 | 234 | web\.ipv6=(?:True|False) (re) |
|
234 | 235 | web.server-header=testing stub value |
|
235 | 236 | *** runcommand init foo |
|
236 | 237 | *** runcommand -R foo showconfig ui defaults |
|
237 | 238 | ui.slash=True |
|
238 | 239 | ui.interactive=False |
|
239 | 240 | ui.detailed-exit-code=True |
|
240 | 241 | ui.merge=internal:merge |
|
241 | 242 | ui.mergemarkers=detailed |
|
243 | ui.ssh=* (glob) | |
|
242 | 244 | ui.timeout.warn=15 |
|
243 | 245 | ui.nontty=true |
|
244 | 246 | #endif |
|
245 | 247 | |
|
246 | 248 | $ rm -R foo |
|
247 | 249 | |
|
248 | 250 | #if windows |
|
249 | 251 | $ PYTHONPATH="$TESTTMP/repo;$PYTHONPATH" |
|
250 | 252 | #else |
|
251 | 253 | $ PYTHONPATH="$TESTTMP/repo:$PYTHONPATH" |
|
252 | 254 | #endif |
|
253 | 255 | |
|
254 | 256 | $ cat <<EOF > hook.py |
|
255 | 257 | > import sys |
|
256 | 258 | > from hgclient import bprint |
|
257 | 259 | > def hook(**args): |
|
258 | 260 | > bprint(b'hook talking') |
|
259 | 261 | > bprint(b'now try to read something: %r' % sys.stdin.read()) |
|
260 | 262 | > EOF |
|
261 | 263 | |
|
262 | 264 | >>> from hgclient import check, readchannel, runcommand, stringio |
|
263 | 265 | >>> @check |
|
264 | 266 | ... def hookoutput(server): |
|
265 | 267 | ... readchannel(server) |
|
266 | 268 | ... runcommand(server, [b'--config', |
|
267 | 269 | ... b'hooks.pre-identify=python:hook.hook', |
|
268 | 270 | ... b'id'], |
|
269 | 271 | ... input=stringio(b'some input')) |
|
270 | 272 | *** runcommand --config hooks.pre-identify=python:hook.hook id |
|
271 | 273 | eff892de26ec tip |
|
272 | 274 | hook talking |
|
273 | 275 | now try to read something: '' |
|
274 | 276 | |
|
275 | 277 | Clean hook cached version |
|
276 | 278 | $ rm hook.py* |
|
277 | 279 | $ rm -Rf __pycache__ |
|
278 | 280 | |
|
279 | 281 | $ echo a >> a |
|
280 | 282 | >>> import os |
|
281 | 283 | >>> from hgclient import check, readchannel, runcommand |
|
282 | 284 | >>> @check |
|
283 | 285 | ... def outsidechanges(server): |
|
284 | 286 | ... readchannel(server) |
|
285 | 287 | ... runcommand(server, [b'status']) |
|
286 | 288 | ... os.system('hg ci -Am2') |
|
287 | 289 | ... runcommand(server, [b'tip']) |
|
288 | 290 | ... runcommand(server, [b'status']) |
|
289 | 291 | *** runcommand status |
|
290 | 292 | M a |
|
291 | 293 | *** runcommand tip |
|
292 | 294 | changeset: 1:d3a0a68be6de |
|
293 | 295 | tag: tip |
|
294 | 296 | user: test |
|
295 | 297 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
296 | 298 | summary: 2 |
|
297 | 299 | |
|
298 | 300 | *** runcommand status |
|
299 | 301 | |
|
300 | 302 | >>> import os |
|
301 | 303 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
302 | 304 | >>> @check |
|
303 | 305 | ... def bookmarks(server): |
|
304 | 306 | ... readchannel(server) |
|
305 | 307 | ... runcommand(server, [b'bookmarks']) |
|
306 | 308 | ... |
|
307 | 309 | ... # changes .hg/bookmarks |
|
308 | 310 | ... os.system('hg bookmark -i bm1') |
|
309 | 311 | ... os.system('hg bookmark -i bm2') |
|
310 | 312 | ... runcommand(server, [b'bookmarks']) |
|
311 | 313 | ... |
|
312 | 314 | ... # changes .hg/bookmarks.current |
|
313 | 315 | ... os.system('hg upd bm1 -q') |
|
314 | 316 | ... runcommand(server, [b'bookmarks']) |
|
315 | 317 | ... |
|
316 | 318 | ... runcommand(server, [b'bookmarks', b'bm3']) |
|
317 | 319 | ... f = open('a', 'ab') |
|
318 | 320 | ... f.write(b'a\n') and None |
|
319 | 321 | ... f.close() |
|
320 | 322 | ... runcommand(server, [b'commit', b'-Amm']) |
|
321 | 323 | ... runcommand(server, [b'bookmarks']) |
|
322 | 324 | ... bprint(b'') |
|
323 | 325 | *** runcommand bookmarks |
|
324 | 326 | no bookmarks set |
|
325 | 327 | *** runcommand bookmarks |
|
326 | 328 | bm1 1:d3a0a68be6de |
|
327 | 329 | bm2 1:d3a0a68be6de |
|
328 | 330 | *** runcommand bookmarks |
|
329 | 331 | * bm1 1:d3a0a68be6de |
|
330 | 332 | bm2 1:d3a0a68be6de |
|
331 | 333 | *** runcommand bookmarks bm3 |
|
332 | 334 | *** runcommand commit -Amm |
|
333 | 335 | *** runcommand bookmarks |
|
334 | 336 | bm1 1:d3a0a68be6de |
|
335 | 337 | bm2 1:d3a0a68be6de |
|
336 | 338 | * bm3 2:aef17e88f5f0 |
|
337 | 339 | |
|
338 | 340 | |
|
339 | 341 | >>> import os |
|
340 | 342 | >>> from hgclient import check, readchannel, runcommand |
|
341 | 343 | >>> @check |
|
342 | 344 | ... def tagscache(server): |
|
343 | 345 | ... readchannel(server) |
|
344 | 346 | ... runcommand(server, [b'id', b'-t', b'-r', b'0']) |
|
345 | 347 | ... os.system('hg tag -r 0 foo') |
|
346 | 348 | ... runcommand(server, [b'id', b'-t', b'-r', b'0']) |
|
347 | 349 | *** runcommand id -t -r 0 |
|
348 | 350 | |
|
349 | 351 | *** runcommand id -t -r 0 |
|
350 | 352 | foo |
|
351 | 353 | |
|
352 | 354 | >>> import os |
|
353 | 355 | >>> from hgclient import check, readchannel, runcommand |
|
354 | 356 | >>> @check |
|
355 | 357 | ... def setphase(server): |
|
356 | 358 | ... readchannel(server) |
|
357 | 359 | ... runcommand(server, [b'phase', b'-r', b'.']) |
|
358 | 360 | ... os.system('hg phase -r . -p') |
|
359 | 361 | ... runcommand(server, [b'phase', b'-r', b'.']) |
|
360 | 362 | *** runcommand phase -r . |
|
361 | 363 | 3: draft |
|
362 | 364 | *** runcommand phase -r . |
|
363 | 365 | 3: public |
|
364 | 366 | |
|
365 | 367 | $ echo a >> a |
|
366 | 368 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
367 | 369 | >>> @check |
|
368 | 370 | ... def rollback(server): |
|
369 | 371 | ... readchannel(server) |
|
370 | 372 | ... runcommand(server, [b'phase', b'-r', b'.', b'-p']) |
|
371 | 373 | ... runcommand(server, [b'commit', b'-Am.']) |
|
372 | 374 | ... runcommand(server, [b'rollback']) |
|
373 | 375 | ... runcommand(server, [b'phase', b'-r', b'.']) |
|
374 | 376 | ... bprint(b'') |
|
375 | 377 | *** runcommand phase -r . -p |
|
376 | 378 | no phases changed |
|
377 | 379 | *** runcommand commit -Am. |
|
378 | 380 | *** runcommand rollback |
|
379 | 381 | repository tip rolled back to revision 3 (undo commit) |
|
380 | 382 | working directory now based on revision 3 |
|
381 | 383 | *** runcommand phase -r . |
|
382 | 384 | 3: public |
|
383 | 385 | |
|
384 | 386 | |
|
385 | 387 | >>> import os |
|
386 | 388 | >>> from hgclient import check, readchannel, runcommand |
|
387 | 389 | >>> @check |
|
388 | 390 | ... def branch(server): |
|
389 | 391 | ... readchannel(server) |
|
390 | 392 | ... runcommand(server, [b'branch']) |
|
391 | 393 | ... os.system('hg branch foo') |
|
392 | 394 | ... runcommand(server, [b'branch']) |
|
393 | 395 | ... os.system('hg branch default') |
|
394 | 396 | *** runcommand branch |
|
395 | 397 | default |
|
396 | 398 | marked working directory as branch foo |
|
397 | 399 | (branches are permanent and global, did you want a bookmark?) |
|
398 | 400 | *** runcommand branch |
|
399 | 401 | foo |
|
400 | 402 | marked working directory as branch default |
|
401 | 403 | (branches are permanent and global, did you want a bookmark?) |
|
402 | 404 | |
|
403 | 405 | $ touch .hgignore |
|
404 | 406 | >>> import os |
|
405 | 407 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
406 | 408 | >>> @check |
|
407 | 409 | ... def hgignore(server): |
|
408 | 410 | ... readchannel(server) |
|
409 | 411 | ... runcommand(server, [b'commit', b'-Am.']) |
|
410 | 412 | ... f = open('ignored-file', 'ab') |
|
411 | 413 | ... f.write(b'') and None |
|
412 | 414 | ... f.close() |
|
413 | 415 | ... f = open('.hgignore', 'ab') |
|
414 | 416 | ... f.write(b'ignored-file') |
|
415 | 417 | ... f.close() |
|
416 | 418 | ... runcommand(server, [b'status', b'-i', b'-u']) |
|
417 | 419 | ... bprint(b'') |
|
418 | 420 | *** runcommand commit -Am. |
|
419 | 421 | adding .hgignore |
|
420 | 422 | *** runcommand status -i -u |
|
421 | 423 | I ignored-file |
|
422 | 424 | |
|
423 | 425 | |
|
424 | 426 | cache of non-public revisions should be invalidated on repository change |
|
425 | 427 | (issue4855): |
|
426 | 428 | |
|
427 | 429 | >>> import os |
|
428 | 430 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
429 | 431 | >>> @check |
|
430 | 432 | ... def phasesetscacheaftercommit(server): |
|
431 | 433 | ... readchannel(server) |
|
432 | 434 | ... # load _phasecache._phaserevs and _phasesets |
|
433 | 435 | ... runcommand(server, [b'log', b'-qr', b'draft()']) |
|
434 | 436 | ... # create draft commits by another process |
|
435 | 437 | ... for i in range(5, 7): |
|
436 | 438 | ... f = open('a', 'ab') |
|
437 | 439 | ... f.seek(0, os.SEEK_END) |
|
438 | 440 | ... f.write(b'a\n') and None |
|
439 | 441 | ... f.close() |
|
440 | 442 | ... os.system('hg commit -Aqm%d' % i) |
|
441 | 443 | ... # new commits should be listed as draft revisions |
|
442 | 444 | ... runcommand(server, [b'log', b'-qr', b'draft()']) |
|
443 | 445 | ... bprint(b'') |
|
444 | 446 | *** runcommand log -qr draft() |
|
445 | 447 | 4:7966c8e3734d |
|
446 | 448 | *** runcommand log -qr draft() |
|
447 | 449 | 4:7966c8e3734d |
|
448 | 450 | 5:41f6602d1c4f |
|
449 | 451 | 6:10501e202c35 |
|
450 | 452 | |
|
451 | 453 | |
|
452 | 454 | >>> import os |
|
453 | 455 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
454 | 456 | >>> @check |
|
455 | 457 | ... def phasesetscacheafterstrip(server): |
|
456 | 458 | ... readchannel(server) |
|
457 | 459 | ... # load _phasecache._phaserevs and _phasesets |
|
458 | 460 | ... runcommand(server, [b'log', b'-qr', b'draft()']) |
|
459 | 461 | ... # strip cached revisions by another process |
|
460 | 462 | ... os.system('hg --config extensions.strip= strip -q 5') |
|
461 | 463 | ... # shouldn't abort by "unknown revision '6'" |
|
462 | 464 | ... runcommand(server, [b'log', b'-qr', b'draft()']) |
|
463 | 465 | ... bprint(b'') |
|
464 | 466 | *** runcommand log -qr draft() |
|
465 | 467 | 4:7966c8e3734d |
|
466 | 468 | 5:41f6602d1c4f |
|
467 | 469 | 6:10501e202c35 |
|
468 | 470 | *** runcommand log -qr draft() |
|
469 | 471 | 4:7966c8e3734d |
|
470 | 472 | |
|
471 | 473 | |
|
472 | 474 | cache of phase roots should be invalidated on strip (issue3827): |
|
473 | 475 | |
|
474 | 476 | >>> import os |
|
475 | 477 | >>> from hgclient import check, readchannel, runcommand, sep |
|
476 | 478 | >>> @check |
|
477 | 479 | ... def phasecacheafterstrip(server): |
|
478 | 480 | ... readchannel(server) |
|
479 | 481 | ... |
|
480 | 482 | ... # create new head, 5:731265503d86 |
|
481 | 483 | ... runcommand(server, [b'update', b'-C', b'0']) |
|
482 | 484 | ... f = open('a', 'ab') |
|
483 | 485 | ... f.write(b'a\n') and None |
|
484 | 486 | ... f.close() |
|
485 | 487 | ... runcommand(server, [b'commit', b'-Am.', b'a']) |
|
486 | 488 | ... runcommand(server, [b'log', b'-Gq']) |
|
487 | 489 | ... |
|
488 | 490 | ... # make it public; draft marker moves to 4:7966c8e3734d |
|
489 | 491 | ... runcommand(server, [b'phase', b'-p', b'.']) |
|
490 | 492 | ... # load _phasecache.phaseroots |
|
491 | 493 | ... runcommand(server, [b'phase', b'.'], outfilter=sep) |
|
492 | 494 | ... |
|
493 | 495 | ... # strip 1::4 outside server |
|
494 | 496 | ... os.system('hg -q --config extensions.mq= strip 1') |
|
495 | 497 | ... |
|
496 | 498 | ... # shouldn't raise "7966c8e3734d: no node!" |
|
497 | 499 | ... runcommand(server, [b'branches']) |
|
498 | 500 | *** runcommand update -C 0 |
|
499 | 501 | 1 files updated, 0 files merged, 2 files removed, 0 files unresolved |
|
500 | 502 | (leaving bookmark bm3) |
|
501 | 503 | *** runcommand commit -Am. a |
|
502 | 504 | created new head |
|
503 | 505 | *** runcommand log -Gq |
|
504 | 506 | @ 5:731265503d86 |
|
505 | 507 | | |
|
506 | 508 | | o 4:7966c8e3734d |
|
507 | 509 | | | |
|
508 | 510 | | o 3:b9b85890c400 |
|
509 | 511 | | | |
|
510 | 512 | | o 2:aef17e88f5f0 |
|
511 | 513 | | | |
|
512 | 514 | | o 1:d3a0a68be6de |
|
513 | 515 | |/ |
|
514 | 516 | o 0:eff892de26ec |
|
515 | 517 | |
|
516 | 518 | *** runcommand phase -p . |
|
517 | 519 | *** runcommand phase . |
|
518 | 520 | 5: public |
|
519 | 521 | *** runcommand branches |
|
520 | 522 | default 1:731265503d86 |
|
521 | 523 | |
|
522 | 524 | in-memory cache must be reloaded if transaction is aborted. otherwise |
|
523 | 525 | changelog and manifest would have invalid node: |
|
524 | 526 | |
|
525 | 527 | $ echo a >> a |
|
526 | 528 | >>> from hgclient import check, readchannel, runcommand |
|
527 | 529 | >>> @check |
|
528 | 530 | ... def txabort(server): |
|
529 | 531 | ... readchannel(server) |
|
530 | 532 | ... runcommand(server, [b'commit', b'--config', b'hooks.pretxncommit=false', |
|
531 | 533 | ... b'-mfoo']) |
|
532 | 534 | ... runcommand(server, [b'verify']) |
|
533 | 535 | *** runcommand commit --config hooks.pretxncommit=false -mfoo |
|
534 | 536 | transaction abort! |
|
535 | 537 | rollback completed |
|
536 | 538 | abort: pretxncommit hook exited with status 1 |
|
537 | 539 | [40] |
|
538 | 540 | *** runcommand verify |
|
539 | 541 | checking changesets |
|
540 | 542 | checking manifests |
|
541 | 543 | crosschecking files in changesets and manifests |
|
542 | 544 | checking files |
|
543 | 545 | checked 2 changesets with 2 changes to 1 files |
|
544 | 546 | $ hg revert --no-backup -aq |
|
545 | 547 | |
|
546 | 548 | $ cat >> .hg/hgrc << EOF |
|
547 | 549 | > [experimental] |
|
548 | 550 | > evolution.createmarkers=True |
|
549 | 551 | > EOF |
|
550 | 552 | |
|
551 | 553 | >>> import os |
|
552 | 554 | >>> from hgclient import check, readchannel, runcommand |
|
553 | 555 | >>> @check |
|
554 | 556 | ... def obsolete(server): |
|
555 | 557 | ... readchannel(server) |
|
556 | 558 | ... |
|
557 | 559 | ... runcommand(server, [b'up', b'null']) |
|
558 | 560 | ... runcommand(server, [b'phase', b'-df', b'tip']) |
|
559 | 561 | ... cmd = 'hg debugobsolete `hg log -r tip --template {node}`' |
|
560 | 562 | ... if os.name == 'nt': |
|
561 | 563 | ... cmd = 'sh -c "%s"' % cmd # run in sh, not cmd.exe |
|
562 | 564 | ... os.system(cmd) |
|
563 | 565 | ... runcommand(server, [b'log', b'--hidden']) |
|
564 | 566 | ... runcommand(server, [b'log']) |
|
565 | 567 | *** runcommand up null |
|
566 | 568 | 0 files updated, 0 files merged, 1 files removed, 0 files unresolved |
|
567 | 569 | *** runcommand phase -df tip |
|
568 | 570 | 1 new obsolescence markers |
|
569 | 571 | obsoleted 1 changesets |
|
570 | 572 | *** runcommand log --hidden |
|
571 | 573 | changeset: 1:731265503d86 |
|
572 | 574 | tag: tip |
|
573 | 575 | user: test |
|
574 | 576 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
575 | 577 | obsolete: pruned |
|
576 | 578 | summary: . |
|
577 | 579 | |
|
578 | 580 | changeset: 0:eff892de26ec |
|
579 | 581 | bookmark: bm1 |
|
580 | 582 | bookmark: bm2 |
|
581 | 583 | bookmark: bm3 |
|
582 | 584 | user: test |
|
583 | 585 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
584 | 586 | summary: 1 |
|
585 | 587 | |
|
586 | 588 | *** runcommand log |
|
587 | 589 | changeset: 0:eff892de26ec |
|
588 | 590 | bookmark: bm1 |
|
589 | 591 | bookmark: bm2 |
|
590 | 592 | bookmark: bm3 |
|
591 | 593 | tag: tip |
|
592 | 594 | user: test |
|
593 | 595 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
594 | 596 | summary: 1 |
|
595 | 597 | |
|
596 | 598 | |
|
597 | 599 | $ cat <<EOF >> .hg/hgrc |
|
598 | 600 | > [extensions] |
|
599 | 601 | > mq = |
|
600 | 602 | > EOF |
|
601 | 603 | |
|
602 | 604 | >>> import os |
|
603 | 605 | >>> from hgclient import check, readchannel, runcommand |
|
604 | 606 | >>> @check |
|
605 | 607 | ... def mqoutsidechanges(server): |
|
606 | 608 | ... readchannel(server) |
|
607 | 609 | ... |
|
608 | 610 | ... # load repo.mq |
|
609 | 611 | ... runcommand(server, [b'qapplied']) |
|
610 | 612 | ... os.system('hg qnew 0.diff') |
|
611 | 613 | ... # repo.mq should be invalidated |
|
612 | 614 | ... runcommand(server, [b'qapplied']) |
|
613 | 615 | ... |
|
614 | 616 | ... runcommand(server, [b'qpop', b'--all']) |
|
615 | 617 | ... os.system('hg qqueue --create foo') |
|
616 | 618 | ... # repo.mq should be recreated to point to new queue |
|
617 | 619 | ... runcommand(server, [b'qqueue', b'--active']) |
|
618 | 620 | *** runcommand qapplied |
|
619 | 621 | *** runcommand qapplied |
|
620 | 622 | 0.diff |
|
621 | 623 | *** runcommand qpop --all |
|
622 | 624 | popping 0.diff |
|
623 | 625 | patch queue now empty |
|
624 | 626 | *** runcommand qqueue --active |
|
625 | 627 | foo |
|
626 | 628 | |
|
627 | 629 | $ cat <<'EOF' > ../dbgui.py |
|
628 | 630 | > import os |
|
629 | 631 | > import sys |
|
630 | 632 | > from mercurial import commands, registrar |
|
631 | 633 | > cmdtable = {} |
|
632 | 634 | > command = registrar.command(cmdtable) |
|
633 | 635 | > @command(b"debuggetpass", norepo=True) |
|
634 | 636 | > def debuggetpass(ui): |
|
635 | 637 | > ui.write(b"%s\n" % ui.getpass()) |
|
636 | 638 | > @command(b"debugprompt", norepo=True) |
|
637 | 639 | > def debugprompt(ui): |
|
638 | 640 | > ui.write(b"%s\n" % ui.prompt(b"prompt:")) |
|
639 | 641 | > @command(b"debugpromptchoice", norepo=True) |
|
640 | 642 | > def debugpromptchoice(ui): |
|
641 | 643 | > msg = b"promptchoice (y/n)? $$ &Yes $$ &No" |
|
642 | 644 | > ui.write(b"%d\n" % ui.promptchoice(msg)) |
|
643 | 645 | > @command(b"debugreadstdin", norepo=True) |
|
644 | 646 | > def debugreadstdin(ui): |
|
645 | 647 | > ui.write(b"read: %r\n" % sys.stdin.read(1)) |
|
646 | 648 | > @command(b"debugwritestdout", norepo=True) |
|
647 | 649 | > def debugwritestdout(ui): |
|
648 | 650 | > os.write(1, b"low-level stdout fd and\n") |
|
649 | 651 | > sys.stdout.write("stdout should be redirected to stderr\n") |
|
650 | 652 | > sys.stdout.flush() |
|
651 | 653 | > EOF |
|
652 | 654 | $ cat <<EOF >> .hg/hgrc |
|
653 | 655 | > [extensions] |
|
654 | 656 | > dbgui = ../dbgui.py |
|
655 | 657 | > EOF |
|
656 | 658 | |
|
657 | 659 | >>> from hgclient import check, readchannel, runcommand, stringio |
|
658 | 660 | >>> @check |
|
659 | 661 | ... def getpass(server): |
|
660 | 662 | ... readchannel(server) |
|
661 | 663 | ... runcommand(server, [b'debuggetpass', b'--config', |
|
662 | 664 | ... b'ui.interactive=True'], |
|
663 | 665 | ... input=stringio(b'1234\n')) |
|
664 | 666 | ... runcommand(server, [b'debuggetpass', b'--config', |
|
665 | 667 | ... b'ui.interactive=True'], |
|
666 | 668 | ... input=stringio(b'\n')) |
|
667 | 669 | ... runcommand(server, [b'debuggetpass', b'--config', |
|
668 | 670 | ... b'ui.interactive=True'], |
|
669 | 671 | ... input=stringio(b'')) |
|
670 | 672 | ... runcommand(server, [b'debugprompt', b'--config', |
|
671 | 673 | ... b'ui.interactive=True'], |
|
672 | 674 | ... input=stringio(b'5678\n')) |
|
673 | 675 | ... runcommand(server, [b'debugprompt', b'--config', |
|
674 | 676 | ... b'ui.interactive=True'], |
|
675 | 677 | ... input=stringio(b'\nremainder\nshould\nnot\nbe\nread\n')) |
|
676 | 678 | ... runcommand(server, [b'debugreadstdin']) |
|
677 | 679 | ... runcommand(server, [b'debugwritestdout']) |
|
678 | 680 | *** runcommand debuggetpass --config ui.interactive=True |
|
679 | 681 | password: 1234 |
|
680 | 682 | *** runcommand debuggetpass --config ui.interactive=True |
|
681 | 683 | password: |
|
682 | 684 | *** runcommand debuggetpass --config ui.interactive=True |
|
683 | 685 | password: abort: response expected |
|
684 | 686 | [255] |
|
685 | 687 | *** runcommand debugprompt --config ui.interactive=True |
|
686 | 688 | prompt: 5678 |
|
687 | 689 | *** runcommand debugprompt --config ui.interactive=True |
|
688 | 690 | prompt: y |
|
689 | 691 | *** runcommand debugreadstdin |
|
690 | 692 | read: '' |
|
691 | 693 | *** runcommand debugwritestdout |
|
692 | 694 | low-level stdout fd and |
|
693 | 695 | stdout should be redirected to stderr |
|
694 | 696 | |
|
695 | 697 | |
|
696 | 698 | run commandserver in commandserver, which is silly but should work: |
|
697 | 699 | |
|
698 | 700 | >>> from hgclient import bprint, check, readchannel, runcommand, stringio |
|
699 | 701 | >>> @check |
|
700 | 702 | ... def nested(server): |
|
701 | 703 | ... bprint(b'%c, %r' % readchannel(server)) |
|
702 | 704 | ... class nestedserver(object): |
|
703 | 705 | ... stdin = stringio(b'getencoding\n') |
|
704 | 706 | ... stdout = stringio() |
|
705 | 707 | ... runcommand(server, [b'serve', b'--cmdserver', b'pipe'], |
|
706 | 708 | ... output=nestedserver.stdout, input=nestedserver.stdin) |
|
707 | 709 | ... nestedserver.stdout.seek(0) |
|
708 | 710 | ... bprint(b'%c, %r' % readchannel(nestedserver)) # hello |
|
709 | 711 | ... bprint(b'%c, %r' % readchannel(nestedserver)) # getencoding |
|
710 | 712 | o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob) |
|
711 | 713 | *** runcommand serve --cmdserver pipe |
|
712 | 714 | o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob) |
|
713 | 715 | r, '*' (glob) |
|
714 | 716 | |
|
715 | 717 | |
|
716 | 718 | start without repository: |
|
717 | 719 | |
|
718 | 720 | $ cd .. |
|
719 | 721 | |
|
720 | 722 | >>> from hgclient import bprint, check, readchannel, runcommand |
|
721 | 723 | >>> @check |
|
722 | 724 | ... def hellomessage(server): |
|
723 | 725 | ... ch, data = readchannel(server) |
|
724 | 726 | ... bprint(b'%c, %r' % (ch, data)) |
|
725 | 727 | ... # run an arbitrary command to make sure the next thing the server |
|
726 | 728 | ... # sends isn't part of the hello message |
|
727 | 729 | ... runcommand(server, [b'id']) |
|
728 | 730 | o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob) |
|
729 | 731 | *** runcommand id |
|
730 | 732 | abort: there is no Mercurial repository here (.hg not found) |
|
731 | 733 | [10] |
|
732 | 734 | |
|
733 | 735 | >>> from hgclient import check, readchannel, runcommand |
|
734 | 736 | >>> @check |
|
735 | 737 | ... def startwithoutrepo(server): |
|
736 | 738 | ... readchannel(server) |
|
737 | 739 | ... runcommand(server, [b'init', b'repo2']) |
|
738 | 740 | ... runcommand(server, [b'id', b'-R', b'repo2']) |
|
739 | 741 | *** runcommand init repo2 |
|
740 | 742 | *** runcommand id -R repo2 |
|
741 | 743 | 000000000000 tip |
|
742 | 744 | |
|
743 | 745 | |
|
744 | 746 | don't fall back to cwd if invalid -R path is specified (issue4805): |
|
745 | 747 | |
|
746 | 748 | $ cd repo |
|
747 | 749 | $ hg serve --cmdserver pipe -R ../nonexistent |
|
748 | 750 | abort: repository ../nonexistent not found |
|
749 | 751 | [255] |
|
750 | 752 | $ cd .. |
|
751 | 753 | |
|
752 | 754 | |
|
753 | 755 | #if no-windows |
|
754 | 756 | |
|
755 | 757 | option to not shutdown on SIGINT: |
|
756 | 758 | |
|
757 | 759 | $ cat <<'EOF' > dbgint.py |
|
758 | 760 | > import os |
|
759 | 761 | > import signal |
|
760 | 762 | > import time |
|
761 | 763 | > from mercurial import commands, registrar |
|
762 | 764 | > cmdtable = {} |
|
763 | 765 | > command = registrar.command(cmdtable) |
|
764 | 766 | > @command(b"debugsleep", norepo=True) |
|
765 | 767 | > def debugsleep(ui): |
|
766 | 768 | > time.sleep(1) |
|
767 | 769 | > @command(b"debugsuicide", norepo=True) |
|
768 | 770 | > def debugsuicide(ui): |
|
769 | 771 | > os.kill(os.getpid(), signal.SIGINT) |
|
770 | 772 | > time.sleep(1) |
|
771 | 773 | > EOF |
|
772 | 774 | |
|
773 | 775 | >>> import signal |
|
774 | 776 | >>> import time |
|
775 | 777 | >>> from hgclient import checkwith, readchannel, runcommand |
|
776 | 778 | >>> @checkwith(extraargs=[b'--config', b'cmdserver.shutdown-on-interrupt=False', |
|
777 | 779 | ... b'--config', b'extensions.dbgint=dbgint.py']) |
|
778 | 780 | ... def nointr(server): |
|
779 | 781 | ... readchannel(server) |
|
780 | 782 | ... server.send_signal(signal.SIGINT) # server won't be terminated |
|
781 | 783 | ... time.sleep(1) |
|
782 | 784 | ... runcommand(server, [b'debugsleep']) |
|
783 | 785 | ... server.send_signal(signal.SIGINT) # server won't be terminated |
|
784 | 786 | ... runcommand(server, [b'debugsleep']) |
|
785 | 787 | ... runcommand(server, [b'debugsuicide']) # command can be interrupted |
|
786 | 788 | ... server.send_signal(signal.SIGTERM) # server will be terminated |
|
787 | 789 | ... time.sleep(1) |
|
788 | 790 | *** runcommand debugsleep |
|
789 | 791 | *** runcommand debugsleep |
|
790 | 792 | *** runcommand debugsuicide |
|
791 | 793 | interrupted! |
|
792 | 794 | killed! |
|
793 | 795 | [255] |
|
794 | 796 | |
|
795 | 797 | #endif |
|
796 | 798 | |
|
797 | 799 | |
|
798 | 800 | structured message channel: |
|
799 | 801 | |
|
800 | 802 | $ cat <<'EOF' >> repo2/.hg/hgrc |
|
801 | 803 | > [ui] |
|
802 | 804 | > # server --config should precede repository option |
|
803 | 805 | > message-output = stdio |
|
804 | 806 | > EOF |
|
805 | 807 | |
|
806 | 808 | >>> from hgclient import bprint, checkwith, readchannel, runcommand |
|
807 | 809 | >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel', |
|
808 | 810 | ... b'--config', b'cmdserver.message-encodings=foo cbor']) |
|
809 | 811 | ... def verify(server): |
|
810 | 812 | ... _ch, data = readchannel(server) |
|
811 | 813 | ... bprint(data) |
|
812 | 814 | ... runcommand(server, [b'-R', b'repo2', b'verify']) |
|
813 | 815 | capabilities: getencoding runcommand |
|
814 | 816 | encoding: ascii |
|
815 | 817 | message-encoding: cbor |
|
816 | 818 | pid: * (glob) |
|
817 | 819 | pgid: * (glob) (no-windows !) |
|
818 | 820 | *** runcommand -R repo2 verify |
|
819 | 821 | message: '\xa2DdataTchecking changesets\nDtypeFstatus' |
|
820 | 822 | message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@' |
|
821 | 823 | message: '\xa2DdataSchecking manifests\nDtypeFstatus' |
|
822 | 824 | message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@' |
|
823 | 825 | message: '\xa2DdataX0crosschecking files in changesets and manifests\nDtypeFstatus' |
|
824 | 826 | message: '\xa6Ditem@Cpos\xf6EtopicMcrosscheckingEtotal\xf6DtypeHprogressDunit@' |
|
825 | 827 | message: '\xa2DdataOchecking files\nDtypeFstatus' |
|
826 | 828 | message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@' |
|
827 | 829 | message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nDtypeFstatus' |
|
828 | 830 | |
|
829 | 831 | >>> from hgclient import checkwith, readchannel, runcommand, stringio |
|
830 | 832 | >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel', |
|
831 | 833 | ... b'--config', b'cmdserver.message-encodings=cbor', |
|
832 | 834 | ... b'--config', b'extensions.dbgui=dbgui.py']) |
|
833 | 835 | ... def prompt(server): |
|
834 | 836 | ... readchannel(server) |
|
835 | 837 | ... interactive = [b'--config', b'ui.interactive=True'] |
|
836 | 838 | ... runcommand(server, [b'debuggetpass'] + interactive, |
|
837 | 839 | ... input=stringio(b'1234\n')) |
|
838 | 840 | ... runcommand(server, [b'debugprompt'] + interactive, |
|
839 | 841 | ... input=stringio(b'5678\n')) |
|
840 | 842 | ... runcommand(server, [b'debugpromptchoice'] + interactive, |
|
841 | 843 | ... input=stringio(b'n\n')) |
|
842 | 844 | *** runcommand debuggetpass --config ui.interactive=True |
|
843 | 845 | message: '\xa3DdataJpassword: Hpassword\xf5DtypeFprompt' |
|
844 | 846 | 1234 |
|
845 | 847 | *** runcommand debugprompt --config ui.interactive=True |
|
846 | 848 | message: '\xa3DdataGprompt:GdefaultAyDtypeFprompt' |
|
847 | 849 | 5678 |
|
848 | 850 | *** runcommand debugpromptchoice --config ui.interactive=True |
|
849 | 851 | message: '\xa4Gchoices\x82\x82AyCYes\x82AnBNoDdataTpromptchoice (y/n)? GdefaultAyDtypeFprompt' |
|
850 | 852 | 1 |
|
851 | 853 | |
|
852 | 854 | bad message encoding: |
|
853 | 855 | |
|
854 | 856 | $ hg serve --cmdserver pipe --config ui.message-output=channel |
|
855 | 857 | abort: no supported message encodings: |
|
856 | 858 | [255] |
|
857 | 859 | $ hg serve --cmdserver pipe --config ui.message-output=channel \ |
|
858 | 860 | > --config cmdserver.message-encodings='foo bar' |
|
859 | 861 | abort: no supported message encodings: foo bar |
|
860 | 862 | [255] |
|
861 | 863 | |
|
862 | 864 | unix domain socket: |
|
863 | 865 | |
|
864 | 866 | $ cd repo |
|
865 | 867 | $ hg update -q |
|
866 | 868 | |
|
867 | 869 | #if unix-socket unix-permissions |
|
868 | 870 | |
|
869 | 871 | >>> from hgclient import bprint, check, readchannel, runcommand, stringio, unixserver |
|
870 | 872 | >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log') |
|
871 | 873 | >>> def hellomessage(conn): |
|
872 | 874 | ... ch, data = readchannel(conn) |
|
873 | 875 | ... bprint(b'%c, %r' % (ch, data)) |
|
874 | 876 | ... runcommand(conn, [b'id']) |
|
875 | 877 | >>> check(hellomessage, server.connect) |
|
876 | 878 | o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob) |
|
877 | 879 | *** runcommand id |
|
878 | 880 | eff892de26ec tip bm1/bm2/bm3 |
|
879 | 881 | >>> def unknowncommand(conn): |
|
880 | 882 | ... readchannel(conn) |
|
881 | 883 | ... conn.stdin.write(b'unknowncommand\n') |
|
882 | 884 | >>> check(unknowncommand, server.connect) # error sent to server.log |
|
883 | 885 | >>> def serverinput(conn): |
|
884 | 886 | ... readchannel(conn) |
|
885 | 887 | ... patch = b""" |
|
886 | 888 | ... # HG changeset patch |
|
887 | 889 | ... # User test |
|
888 | 890 | ... # Date 0 0 |
|
889 | 891 | ... 2 |
|
890 | 892 | ... |
|
891 | 893 | ... diff -r eff892de26ec -r 1ed24be7e7a0 a |
|
892 | 894 | ... --- a/a |
|
893 | 895 | ... +++ b/a |
|
894 | 896 | ... @@ -1,1 +1,2 @@ |
|
895 | 897 | ... 1 |
|
896 | 898 | ... +2 |
|
897 | 899 | ... """ |
|
898 | 900 | ... runcommand(conn, [b'import', b'-'], input=stringio(patch)) |
|
899 | 901 | ... runcommand(conn, [b'log', b'-rtip', b'-q']) |
|
900 | 902 | >>> check(serverinput, server.connect) |
|
901 | 903 | *** runcommand import - |
|
902 | 904 | applying patch from stdin |
|
903 | 905 | *** runcommand log -rtip -q |
|
904 | 906 | 2:1ed24be7e7a0 |
|
905 | 907 | >>> server.shutdown() |
|
906 | 908 | |
|
907 | 909 | $ cat .hg/server.log |
|
908 | 910 | listening at .hg/server.sock |
|
909 | 911 | abort: unknown command unknowncommand |
|
910 | 912 | killed! |
|
911 | 913 | $ rm .hg/server.log |
|
912 | 914 | |
|
913 | 915 | if server crashed before hello, traceback will be sent to 'e' channel as |
|
914 | 916 | last ditch: |
|
915 | 917 | |
|
916 | 918 | $ cat <<'EOF' > ../earlycrasher.py |
|
917 | 919 | > from mercurial import commandserver, extensions |
|
918 | 920 | > def _serverequest(orig, ui, repo, conn, createcmdserver, prereposetups): |
|
919 | 921 | > def createcmdserver(*args, **kwargs): |
|
920 | 922 | > raise Exception('crash') |
|
921 | 923 | > return orig(ui, repo, conn, createcmdserver, prereposetups) |
|
922 | 924 | > def extsetup(ui): |
|
923 | 925 | > extensions.wrapfunction(commandserver, b'_serverequest', _serverequest) |
|
924 | 926 | > EOF |
|
925 | 927 | $ cat <<EOF >> .hg/hgrc |
|
926 | 928 | > [extensions] |
|
927 | 929 | > earlycrasher = ../earlycrasher.py |
|
928 | 930 | > EOF |
|
929 | 931 | >>> from hgclient import bprint, check, readchannel, unixserver |
|
930 | 932 | >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log') |
|
931 | 933 | >>> def earlycrash(conn): |
|
932 | 934 | ... while True: |
|
933 | 935 | ... try: |
|
934 | 936 | ... ch, data = readchannel(conn) |
|
935 | 937 | ... for l in data.splitlines(True): |
|
936 | 938 | ... if not l.startswith(b' '): |
|
937 | 939 | ... bprint(b'%c, %r' % (ch, l)) |
|
938 | 940 | ... except EOFError: |
|
939 | 941 | ... break |
|
940 | 942 | >>> check(earlycrash, server.connect) |
|
941 | 943 | e, 'Traceback (most recent call last):\n' |
|
942 | 944 | e, 'Exception: crash\n' |
|
943 | 945 | >>> server.shutdown() |
|
944 | 946 | |
|
945 | 947 | $ cat .hg/server.log | grep -v '^ ' |
|
946 | 948 | listening at .hg/server.sock |
|
947 | 949 | Traceback (most recent call last): |
|
948 | 950 | Exception: crash |
|
949 | 951 | killed! |
|
950 | 952 | #endif |
|
951 | 953 | #if no-unix-socket |
|
952 | 954 | |
|
953 | 955 | $ hg serve --cmdserver unix -a .hg/server.sock |
|
954 | 956 | abort: unsupported platform |
|
955 | 957 | [255] |
|
956 | 958 | |
|
957 | 959 | #endif |
|
958 | 960 | |
|
959 | 961 | $ cd .. |
|
960 | 962 | |
|
961 | 963 | Test that accessing to invalid changelog cache is avoided at |
|
962 | 964 | subsequent operations even if repo object is reused even after failure |
|
963 | 965 | of transaction (see 0a7610758c42 also) |
|
964 | 966 | |
|
965 | 967 | "hg log" after failure of transaction is needed to detect invalid |
|
966 | 968 | cache in repoview: this can't detect by "hg verify" only. |
|
967 | 969 | |
|
968 | 970 | Combination of "finalization" and "empty-ness of changelog" (2 x 2 = |
|
969 | 971 | 4) are tested, because '00changelog.i' are differently changed in each |
|
970 | 972 | cases. |
|
971 | 973 | |
|
972 | 974 | $ cat > $TESTTMP/failafterfinalize.py <<EOF |
|
973 | 975 | > # extension to abort transaction after finalization forcibly |
|
974 | 976 | > from mercurial import commands, error, extensions, lock as lockmod |
|
975 | 977 | > from mercurial import registrar |
|
976 | 978 | > cmdtable = {} |
|
977 | 979 | > command = registrar.command(cmdtable) |
|
978 | 980 | > configtable = {} |
|
979 | 981 | > configitem = registrar.configitem(configtable) |
|
980 | 982 | > configitem(b'failafterfinalize', b'fail', |
|
981 | 983 | > default=None, |
|
982 | 984 | > ) |
|
983 | 985 | > def fail(tr): |
|
984 | 986 | > raise error.Abort(b'fail after finalization') |
|
985 | 987 | > def reposetup(ui, repo): |
|
986 | 988 | > class failrepo(repo.__class__): |
|
987 | 989 | > def commitctx(self, ctx, error=False, origctx=None): |
|
988 | 990 | > if self.ui.configbool(b'failafterfinalize', b'fail'): |
|
989 | 991 | > # 'sorted()' by ASCII code on category names causes |
|
990 | 992 | > # invoking 'fail' after finalization of changelog |
|
991 | 993 | > # using "'cl-%i' % id(self)" as category name |
|
992 | 994 | > self.currenttransaction().addfinalize(b'zzzzzzzz', fail) |
|
993 | 995 | > return super(failrepo, self).commitctx(ctx, error, origctx) |
|
994 | 996 | > repo.__class__ = failrepo |
|
995 | 997 | > EOF |
|
996 | 998 | |
|
997 | 999 | $ hg init repo3 |
|
998 | 1000 | $ cd repo3 |
|
999 | 1001 | |
|
1000 | 1002 | $ cat <<EOF >> $HGRCPATH |
|
1001 | 1003 | > [command-templates] |
|
1002 | 1004 | > log = {rev} {desc|firstline} ({files})\n |
|
1003 | 1005 | > |
|
1004 | 1006 | > [extensions] |
|
1005 | 1007 | > failafterfinalize = $TESTTMP/failafterfinalize.py |
|
1006 | 1008 | > EOF |
|
1007 | 1009 | |
|
1008 | 1010 | - test failure with "empty changelog" |
|
1009 | 1011 | |
|
1010 | 1012 | $ echo foo > foo |
|
1011 | 1013 | $ hg add foo |
|
1012 | 1014 | |
|
1013 | 1015 | (failure before finalization) |
|
1014 | 1016 | |
|
1015 | 1017 | >>> from hgclient import check, readchannel, runcommand |
|
1016 | 1018 | >>> @check |
|
1017 | 1019 | ... def abort(server): |
|
1018 | 1020 | ... readchannel(server) |
|
1019 | 1021 | ... runcommand(server, [b'commit', |
|
1020 | 1022 | ... b'--config', b'hooks.pretxncommit=false', |
|
1021 | 1023 | ... b'-mfoo']) |
|
1022 | 1024 | ... runcommand(server, [b'log']) |
|
1023 | 1025 | ... runcommand(server, [b'verify', b'-q']) |
|
1024 | 1026 | *** runcommand commit --config hooks.pretxncommit=false -mfoo |
|
1025 | 1027 | transaction abort! |
|
1026 | 1028 | rollback completed |
|
1027 | 1029 | abort: pretxncommit hook exited with status 1 |
|
1028 | 1030 | [40] |
|
1029 | 1031 | *** runcommand log |
|
1030 | 1032 | *** runcommand verify -q |
|
1031 | 1033 | |
|
1032 | 1034 | (failure after finalization) |
|
1033 | 1035 | |
|
1034 | 1036 | >>> from hgclient import check, readchannel, runcommand |
|
1035 | 1037 | >>> @check |
|
1036 | 1038 | ... def abort(server): |
|
1037 | 1039 | ... readchannel(server) |
|
1038 | 1040 | ... runcommand(server, [b'commit', |
|
1039 | 1041 | ... b'--config', b'failafterfinalize.fail=true', |
|
1040 | 1042 | ... b'-mfoo']) |
|
1041 | 1043 | ... runcommand(server, [b'log']) |
|
1042 | 1044 | ... runcommand(server, [b'verify', b'-q']) |
|
1043 | 1045 | *** runcommand commit --config failafterfinalize.fail=true -mfoo |
|
1044 | 1046 | transaction abort! |
|
1045 | 1047 | rollback completed |
|
1046 | 1048 | abort: fail after finalization |
|
1047 | 1049 | [255] |
|
1048 | 1050 | *** runcommand log |
|
1049 | 1051 | *** runcommand verify -q |
|
1050 | 1052 | |
|
1051 | 1053 | - test failure with "not-empty changelog" |
|
1052 | 1054 | |
|
1053 | 1055 | $ echo bar > bar |
|
1054 | 1056 | $ hg add bar |
|
1055 | 1057 | $ hg commit -mbar bar |
|
1056 | 1058 | |
|
1057 | 1059 | (failure before finalization) |
|
1058 | 1060 | |
|
1059 | 1061 | >>> from hgclient import check, readchannel, runcommand |
|
1060 | 1062 | >>> @check |
|
1061 | 1063 | ... def abort(server): |
|
1062 | 1064 | ... readchannel(server) |
|
1063 | 1065 | ... runcommand(server, [b'commit', |
|
1064 | 1066 | ... b'--config', b'hooks.pretxncommit=false', |
|
1065 | 1067 | ... b'-mfoo', b'foo']) |
|
1066 | 1068 | ... runcommand(server, [b'log']) |
|
1067 | 1069 | ... runcommand(server, [b'verify', b'-q']) |
|
1068 | 1070 | *** runcommand commit --config hooks.pretxncommit=false -mfoo foo |
|
1069 | 1071 | transaction abort! |
|
1070 | 1072 | rollback completed |
|
1071 | 1073 | abort: pretxncommit hook exited with status 1 |
|
1072 | 1074 | [40] |
|
1073 | 1075 | *** runcommand log |
|
1074 | 1076 | 0 bar (bar) |
|
1075 | 1077 | *** runcommand verify -q |
|
1076 | 1078 | |
|
1077 | 1079 | (failure after finalization) |
|
1078 | 1080 | |
|
1079 | 1081 | >>> from hgclient import check, readchannel, runcommand |
|
1080 | 1082 | >>> @check |
|
1081 | 1083 | ... def abort(server): |
|
1082 | 1084 | ... readchannel(server) |
|
1083 | 1085 | ... runcommand(server, [b'commit', |
|
1084 | 1086 | ... b'--config', b'failafterfinalize.fail=true', |
|
1085 | 1087 | ... b'-mfoo', b'foo']) |
|
1086 | 1088 | ... runcommand(server, [b'log']) |
|
1087 | 1089 | ... runcommand(server, [b'verify', b'-q']) |
|
1088 | 1090 | *** runcommand commit --config failafterfinalize.fail=true -mfoo foo |
|
1089 | 1091 | transaction abort! |
|
1090 | 1092 | rollback completed |
|
1091 | 1093 | abort: fail after finalization |
|
1092 | 1094 | [255] |
|
1093 | 1095 | *** runcommand log |
|
1094 | 1096 | 0 bar (bar) |
|
1095 | 1097 | *** runcommand verify -q |
|
1096 | 1098 | |
|
1097 | 1099 | $ cd .. |
|
1098 | 1100 | |
|
1099 | 1101 | Test symlink traversal over cached audited paths: |
|
1100 | 1102 | ------------------------------------------------- |
|
1101 | 1103 | |
|
1102 | 1104 | #if symlink |
|
1103 | 1105 | |
|
1104 | 1106 | set up symlink hell |
|
1105 | 1107 | |
|
1106 | 1108 | $ mkdir merge-symlink-out |
|
1107 | 1109 | $ hg init merge-symlink |
|
1108 | 1110 | $ cd merge-symlink |
|
1109 | 1111 | $ touch base |
|
1110 | 1112 | $ hg commit -qAm base |
|
1111 | 1113 | $ ln -s ../merge-symlink-out a |
|
1112 | 1114 | $ hg commit -qAm 'symlink a -> ../merge-symlink-out' |
|
1113 | 1115 | $ hg up -q 0 |
|
1114 | 1116 | $ mkdir a |
|
1115 | 1117 | $ touch a/poisoned |
|
1116 | 1118 | $ hg commit -qAm 'file a/poisoned' |
|
1117 | 1119 | $ hg log -G -T '{rev}: {desc}\n' |
|
1118 | 1120 | @ 2: file a/poisoned |
|
1119 | 1121 | | |
|
1120 | 1122 | | o 1: symlink a -> ../merge-symlink-out |
|
1121 | 1123 | |/ |
|
1122 | 1124 | o 0: base |
|
1123 | 1125 | |
|
1124 | 1126 | |
|
1125 | 1127 | try trivial merge after update: cache of audited paths should be discarded, |
|
1126 | 1128 | and the merge should fail (issue5628) |
|
1127 | 1129 | |
|
1128 | 1130 | $ hg up -q null |
|
1129 | 1131 | >>> from hgclient import check, readchannel, runcommand |
|
1130 | 1132 | >>> @check |
|
1131 | 1133 | ... def merge(server): |
|
1132 | 1134 | ... readchannel(server) |
|
1133 | 1135 | ... # audit a/poisoned as a good path |
|
1134 | 1136 | ... runcommand(server, [b'up', b'-qC', b'2']) |
|
1135 | 1137 | ... runcommand(server, [b'up', b'-qC', b'1']) |
|
1136 | 1138 | ... # here a is a symlink, so a/poisoned is bad |
|
1137 | 1139 | ... runcommand(server, [b'merge', b'2']) |
|
1138 | 1140 | *** runcommand up -qC 2 |
|
1139 | 1141 | *** runcommand up -qC 1 |
|
1140 | 1142 | *** runcommand merge 2 |
|
1141 | 1143 | abort: path 'a/poisoned' traverses symbolic link 'a' |
|
1142 | 1144 | [255] |
|
1143 | 1145 | $ ls ../merge-symlink-out |
|
1144 | 1146 | |
|
1145 | 1147 | cache of repo.auditor should be discarded, so matcher would never traverse |
|
1146 | 1148 | symlinks: |
|
1147 | 1149 | |
|
1148 | 1150 | $ hg up -qC 0 |
|
1149 | 1151 | $ touch ../merge-symlink-out/poisoned |
|
1150 | 1152 | >>> from hgclient import check, readchannel, runcommand |
|
1151 | 1153 | >>> @check |
|
1152 | 1154 | ... def files(server): |
|
1153 | 1155 | ... readchannel(server) |
|
1154 | 1156 | ... runcommand(server, [b'up', b'-qC', b'2']) |
|
1155 | 1157 | ... # audit a/poisoned as a good path |
|
1156 | 1158 | ... runcommand(server, [b'files', b'a/poisoned']) |
|
1157 | 1159 | ... runcommand(server, [b'up', b'-qC', b'0']) |
|
1158 | 1160 | ... runcommand(server, [b'up', b'-qC', b'1']) |
|
1159 | 1161 | ... # here 'a' is a symlink, so a/poisoned should be warned |
|
1160 | 1162 | ... runcommand(server, [b'files', b'a/poisoned']) |
|
1161 | 1163 | *** runcommand up -qC 2 |
|
1162 | 1164 | *** runcommand files a/poisoned |
|
1163 | 1165 | a/poisoned |
|
1164 | 1166 | *** runcommand up -qC 0 |
|
1165 | 1167 | *** runcommand up -qC 1 |
|
1166 | 1168 | *** runcommand files a/poisoned |
|
1167 | 1169 | abort: path 'a/poisoned' traverses symbolic link 'a' |
|
1168 | 1170 | [255] |
|
1169 | 1171 | |
|
1170 | 1172 | $ cd .. |
|
1171 | 1173 | |
|
1172 | 1174 | #endif |
@@ -1,546 +1,546 b'' | |||
|
1 | 1 | Windows needs ';' as a file separator in an environment variable, and MSYS |
|
2 | 2 | doesn't automatically convert it in every case. |
|
3 | 3 | |
|
4 | 4 | #if windows |
|
5 | 5 | $ path_list_var() { |
|
6 | 6 | > echo $1 | sed 's/:/;/' |
|
7 | 7 | > } |
|
8 | 8 | #else |
|
9 | 9 | $ path_list_var() { |
|
10 | 10 | > echo $1 |
|
11 | 11 | > } |
|
12 | 12 | #endif |
|
13 | 13 | |
|
14 | 14 | |
|
15 | 15 | hide outer repo |
|
16 | 16 | $ hg init |
|
17 | 17 | |
|
18 | 18 | Invalid syntax: no value |
|
19 | 19 | |
|
20 | 20 | $ cat > .hg/hgrc << EOF |
|
21 | 21 | > novaluekey |
|
22 | 22 | > EOF |
|
23 | 23 | $ hg showconfig |
|
24 | 24 | config error at $TESTTMP/.hg/hgrc:1: novaluekey |
|
25 | 25 | [30] |
|
26 | 26 | |
|
27 | 27 | Invalid syntax: no key |
|
28 | 28 | |
|
29 | 29 | $ cat > .hg/hgrc << EOF |
|
30 | 30 | > =nokeyvalue |
|
31 | 31 | > EOF |
|
32 | 32 | $ hg showconfig |
|
33 | 33 | config error at $TESTTMP/.hg/hgrc:1: =nokeyvalue |
|
34 | 34 | [30] |
|
35 | 35 | |
|
36 | 36 | Test hint about invalid syntax from leading white space |
|
37 | 37 | |
|
38 | 38 | $ cat > .hg/hgrc << EOF |
|
39 | 39 | > key=value |
|
40 | 40 | > EOF |
|
41 | 41 | $ hg showconfig |
|
42 | 42 | config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: key=value |
|
43 | 43 | [30] |
|
44 | 44 | |
|
45 | 45 | $ cat > .hg/hgrc << EOF |
|
46 | 46 | > [section] |
|
47 | 47 | > key=value |
|
48 | 48 | > EOF |
|
49 | 49 | $ hg showconfig |
|
50 | 50 | config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: [section] |
|
51 | 51 | [30] |
|
52 | 52 | |
|
53 | 53 | Reset hgrc |
|
54 | 54 | |
|
55 | 55 | $ echo > .hg/hgrc |
|
56 | 56 | |
|
57 | 57 | Test case sensitive configuration |
|
58 | 58 | |
|
59 | 59 | $ cat <<EOF >> $HGRCPATH |
|
60 | 60 | > [Section] |
|
61 | 61 | > KeY = Case Sensitive |
|
62 | 62 | > key = lower case |
|
63 | 63 | > EOF |
|
64 | 64 | |
|
65 | 65 | $ hg showconfig Section |
|
66 | 66 | Section.KeY=Case Sensitive |
|
67 | 67 | Section.key=lower case |
|
68 | 68 | |
|
69 | 69 | $ hg showconfig Section -Tjson |
|
70 | 70 | [ |
|
71 | 71 | { |
|
72 | 72 | "defaultvalue": null, |
|
73 | 73 | "name": "Section.KeY", |
|
74 | 74 | "source": "*.hgrc:*", (glob) |
|
75 | 75 | "value": "Case Sensitive" |
|
76 | 76 | }, |
|
77 | 77 | { |
|
78 | 78 | "defaultvalue": null, |
|
79 | 79 | "name": "Section.key", |
|
80 | 80 | "source": "*.hgrc:*", (glob) |
|
81 | 81 | "value": "lower case" |
|
82 | 82 | } |
|
83 | 83 | ] |
|
84 | 84 | $ hg showconfig Section.KeY -Tjson |
|
85 | 85 | [ |
|
86 | 86 | { |
|
87 | 87 | "defaultvalue": null, |
|
88 | 88 | "name": "Section.KeY", |
|
89 | 89 | "source": "*.hgrc:*", (glob) |
|
90 | 90 | "value": "Case Sensitive" |
|
91 | 91 | } |
|
92 | 92 | ] |
|
93 | 93 | $ hg showconfig -Tjson | tail -7 |
|
94 | 94 | { |
|
95 | 95 | "defaultvalue": null, |
|
96 | 96 | "name": "*", (glob) |
|
97 | 97 | "source": "*", (glob) |
|
98 | 98 | "value": "*" (glob) |
|
99 | 99 | } |
|
100 | 100 | ] |
|
101 | 101 | |
|
102 | 102 | Test config default of various types: |
|
103 | 103 | |
|
104 | 104 | {"defaultvalue": ""} for -T'json(defaultvalue)' looks weird, but that's |
|
105 | 105 | how the templater works. Unknown keywords are evaluated to "". |
|
106 | 106 | |
|
107 | 107 | dynamicdefault |
|
108 | 108 | |
|
109 | 109 | $ hg config --config alias.foo= alias -Tjson |
|
110 | 110 | [ |
|
111 | 111 | { |
|
112 | 112 | "name": "alias.foo", |
|
113 | 113 | "source": "--config", |
|
114 | 114 | "value": "" |
|
115 | 115 | } |
|
116 | 116 | ] |
|
117 | 117 | $ hg config --config alias.foo= alias -T'json(defaultvalue)' |
|
118 | 118 | [ |
|
119 | 119 | {"defaultvalue": ""} |
|
120 | 120 | ] |
|
121 | 121 | $ hg config --config alias.foo= alias -T'{defaultvalue}\n' |
|
122 | 122 | |
|
123 | 123 | |
|
124 | 124 | null |
|
125 | 125 | |
|
126 | 126 | $ hg config --config auth.cookiefile= auth -Tjson |
|
127 | 127 | [ |
|
128 | 128 | { |
|
129 | 129 | "defaultvalue": null, |
|
130 | 130 | "name": "auth.cookiefile", |
|
131 | 131 | "source": "--config", |
|
132 | 132 | "value": "" |
|
133 | 133 | } |
|
134 | 134 | ] |
|
135 | 135 | $ hg config --config auth.cookiefile= auth -T'json(defaultvalue)' |
|
136 | 136 | [ |
|
137 | 137 | {"defaultvalue": null} |
|
138 | 138 | ] |
|
139 | 139 | $ hg config --config auth.cookiefile= auth -T'{defaultvalue}\n' |
|
140 | 140 | |
|
141 | 141 | |
|
142 | 142 | false |
|
143 | 143 | |
|
144 | 144 | $ hg config --config commands.commit.post-status= commands -Tjson |
|
145 | 145 | [ |
|
146 | 146 | { |
|
147 | 147 | "defaultvalue": false, |
|
148 | 148 | "name": "commands.commit.post-status", |
|
149 | 149 | "source": "--config", |
|
150 | 150 | "value": "" |
|
151 | 151 | } |
|
152 | 152 | ] |
|
153 | 153 | $ hg config --config commands.commit.post-status= commands -T'json(defaultvalue)' |
|
154 | 154 | [ |
|
155 | 155 | {"defaultvalue": false} |
|
156 | 156 | ] |
|
157 | 157 | $ hg config --config commands.commit.post-status= commands -T'{defaultvalue}\n' |
|
158 | 158 | False |
|
159 | 159 | |
|
160 | 160 | true |
|
161 | 161 | |
|
162 | 162 | $ hg config --config format.dotencode= format.dotencode -Tjson |
|
163 | 163 | [ |
|
164 | 164 | { |
|
165 | 165 | "defaultvalue": true, |
|
166 | 166 | "name": "format.dotencode", |
|
167 | 167 | "source": "--config", |
|
168 | 168 | "value": "" |
|
169 | 169 | } |
|
170 | 170 | ] |
|
171 | 171 | $ hg config --config format.dotencode= format.dotencode -T'json(defaultvalue)' |
|
172 | 172 | [ |
|
173 | 173 | {"defaultvalue": true} |
|
174 | 174 | ] |
|
175 | 175 | $ hg config --config format.dotencode= format.dotencode -T'{defaultvalue}\n' |
|
176 | 176 | True |
|
177 | 177 | |
|
178 | 178 | bytes |
|
179 | 179 | |
|
180 | 180 | $ hg config --config commands.resolve.mark-check= commands -Tjson |
|
181 | 181 | [ |
|
182 | 182 | { |
|
183 | 183 | "defaultvalue": "none", |
|
184 | 184 | "name": "commands.resolve.mark-check", |
|
185 | 185 | "source": "--config", |
|
186 | 186 | "value": "" |
|
187 | 187 | } |
|
188 | 188 | ] |
|
189 | 189 | $ hg config --config commands.resolve.mark-check= commands -T'json(defaultvalue)' |
|
190 | 190 | [ |
|
191 | 191 | {"defaultvalue": "none"} |
|
192 | 192 | ] |
|
193 | 193 | $ hg config --config commands.resolve.mark-check= commands -T'{defaultvalue}\n' |
|
194 | 194 | none |
|
195 | 195 | |
|
196 | 196 | empty list |
|
197 | 197 | |
|
198 | 198 | $ hg config --config commands.show.aliasprefix= commands -Tjson |
|
199 | 199 | [ |
|
200 | 200 | { |
|
201 | 201 | "defaultvalue": [], |
|
202 | 202 | "name": "commands.show.aliasprefix", |
|
203 | 203 | "source": "--config", |
|
204 | 204 | "value": "" |
|
205 | 205 | } |
|
206 | 206 | ] |
|
207 | 207 | $ hg config --config commands.show.aliasprefix= commands -T'json(defaultvalue)' |
|
208 | 208 | [ |
|
209 | 209 | {"defaultvalue": []} |
|
210 | 210 | ] |
|
211 | 211 | $ hg config --config commands.show.aliasprefix= commands -T'{defaultvalue}\n' |
|
212 | 212 | |
|
213 | 213 | |
|
214 | 214 | nonempty list |
|
215 | 215 | |
|
216 | 216 | $ hg config --config progress.format= progress -Tjson |
|
217 | 217 | [ |
|
218 | 218 | { |
|
219 | 219 | "defaultvalue": ["topic", "bar", "number", "estimate"], |
|
220 | 220 | "name": "progress.format", |
|
221 | 221 | "source": "--config", |
|
222 | 222 | "value": "" |
|
223 | 223 | } |
|
224 | 224 | ] |
|
225 | 225 | $ hg config --config progress.format= progress -T'json(defaultvalue)' |
|
226 | 226 | [ |
|
227 | 227 | {"defaultvalue": ["topic", "bar", "number", "estimate"]} |
|
228 | 228 | ] |
|
229 | 229 | $ hg config --config progress.format= progress -T'{defaultvalue}\n' |
|
230 | 230 | topic bar number estimate |
|
231 | 231 | |
|
232 | 232 | int |
|
233 | 233 | |
|
234 | 234 | $ hg config --config profiling.freq= profiling -Tjson |
|
235 | 235 | [ |
|
236 | 236 | { |
|
237 | 237 | "defaultvalue": 1000, |
|
238 | 238 | "name": "profiling.freq", |
|
239 | 239 | "source": "--config", |
|
240 | 240 | "value": "" |
|
241 | 241 | } |
|
242 | 242 | ] |
|
243 | 243 | $ hg config --config profiling.freq= profiling -T'json(defaultvalue)' |
|
244 | 244 | [ |
|
245 | 245 | {"defaultvalue": 1000} |
|
246 | 246 | ] |
|
247 | 247 | $ hg config --config profiling.freq= profiling -T'{defaultvalue}\n' |
|
248 | 248 | 1000 |
|
249 | 249 | |
|
250 | 250 | float |
|
251 | 251 | |
|
252 | 252 | $ hg config --config profiling.showmax= profiling -Tjson |
|
253 | 253 | [ |
|
254 | 254 | { |
|
255 | 255 | "defaultvalue": 0.999, |
|
256 | 256 | "name": "profiling.showmax", |
|
257 | 257 | "source": "--config", |
|
258 | 258 | "value": "" |
|
259 | 259 | } |
|
260 | 260 | ] |
|
261 | 261 | $ hg config --config profiling.showmax= profiling -T'json(defaultvalue)' |
|
262 | 262 | [ |
|
263 | 263 | {"defaultvalue": 0.999} |
|
264 | 264 | ] |
|
265 | 265 | $ hg config --config profiling.showmax= profiling -T'{defaultvalue}\n' |
|
266 | 266 | 0.999 |
|
267 | 267 | |
|
268 | 268 | Test empty config source: |
|
269 | 269 | |
|
270 | 270 | $ cat <<EOF > emptysource.py |
|
271 | 271 | > def reposetup(ui, repo): |
|
272 | 272 | > ui.setconfig(b'empty', b'source', b'value') |
|
273 | 273 | > EOF |
|
274 | 274 | $ cp .hg/hgrc .hg/hgrc.orig |
|
275 | 275 | $ cat <<EOF >> .hg/hgrc |
|
276 | 276 | > [extensions] |
|
277 | 277 | > emptysource = `pwd`/emptysource.py |
|
278 | 278 | > EOF |
|
279 | 279 | |
|
280 | 280 | $ hg config --source empty.source |
|
281 | 281 | none: value |
|
282 | 282 | $ hg config empty.source -Tjson |
|
283 | 283 | [ |
|
284 | 284 | { |
|
285 | 285 | "defaultvalue": null, |
|
286 | 286 | "name": "empty.source", |
|
287 | 287 | "source": "", |
|
288 | 288 | "value": "value" |
|
289 | 289 | } |
|
290 | 290 | ] |
|
291 | 291 | |
|
292 | 292 | $ cp .hg/hgrc.orig .hg/hgrc |
|
293 | 293 | |
|
294 | 294 | Test "%unset" |
|
295 | 295 | |
|
296 | 296 | $ cat >> $HGRCPATH <<EOF |
|
297 | 297 | > [unsettest] |
|
298 | 298 | > local-hgrcpath = should be unset (HGRCPATH) |
|
299 | 299 | > %unset local-hgrcpath |
|
300 | 300 | > |
|
301 | 301 | > global = should be unset (HGRCPATH) |
|
302 | 302 | > |
|
303 | 303 | > both = should be unset (HGRCPATH) |
|
304 | 304 | > |
|
305 | 305 | > set-after-unset = should be unset (HGRCPATH) |
|
306 | 306 | > EOF |
|
307 | 307 | |
|
308 | 308 | $ cat >> .hg/hgrc <<EOF |
|
309 | 309 | > [unsettest] |
|
310 | 310 | > local-hgrc = should be unset (.hg/hgrc) |
|
311 | 311 | > %unset local-hgrc |
|
312 | 312 | > |
|
313 | 313 | > %unset global |
|
314 | 314 | > |
|
315 | 315 | > both = should be unset (.hg/hgrc) |
|
316 | 316 | > %unset both |
|
317 | 317 | > |
|
318 | 318 | > set-after-unset = should be unset (.hg/hgrc) |
|
319 | 319 | > %unset set-after-unset |
|
320 | 320 | > set-after-unset = should be set (.hg/hgrc) |
|
321 | 321 | > EOF |
|
322 | 322 | |
|
323 | 323 | $ hg showconfig unsettest |
|
324 | 324 | unsettest.set-after-unset=should be set (.hg/hgrc) |
|
325 | 325 | |
|
326 | 326 | Test exit code when no config matches |
|
327 | 327 | |
|
328 | 328 | $ hg config Section.idontexist |
|
329 | 329 | [1] |
|
330 | 330 | |
|
331 | 331 | sub-options in [paths] aren't expanded |
|
332 | 332 | |
|
333 | 333 | $ cat > .hg/hgrc << EOF |
|
334 | 334 | > [paths] |
|
335 | 335 | > foo = ~/foo |
|
336 | 336 | > foo:suboption = ~/foo |
|
337 | 337 | > EOF |
|
338 | 338 | |
|
339 | 339 | $ hg showconfig paths |
|
340 | 340 | paths.foo=~/foo |
|
341 | 341 | paths.foo:suboption=~/foo |
|
342 | 342 | |
|
343 | 343 | note: The path expansion no longer happens at the config level, but the path is |
|
344 | 344 | still expanded: |
|
345 | 345 | |
|
346 | 346 | $ hg path | grep foo |
|
347 | 347 | foo = $TESTTMP/foo |
|
348 | 348 | |
|
349 | 349 | edit failure |
|
350 | 350 | |
|
351 | 351 | $ HGEDITOR=false hg config --edit |
|
352 | 352 | abort: edit failed: false exited with status 1 |
|
353 | 353 | [10] |
|
354 | 354 | |
|
355 | 355 | config affected by environment variables |
|
356 | 356 | |
|
357 | 357 | $ EDITOR=e1 VISUAL=e2 hg config --source | grep 'ui\.editor' |
|
358 | 358 | $VISUAL: ui.editor=e2 |
|
359 | 359 | |
|
360 | 360 | $ VISUAL=e2 hg config --source --config ui.editor=e3 | grep 'ui\.editor' |
|
361 | 361 | --config: ui.editor=e3 |
|
362 | 362 | |
|
363 | 363 | $ PAGER=p1 hg config --source | grep 'pager\.pager' |
|
364 | 364 | $PAGER: pager.pager=p1 |
|
365 | 365 | |
|
366 | 366 | $ PAGER=p1 hg config --source --config pager.pager=p2 | grep 'pager\.pager' |
|
367 | 367 | --config: pager.pager=p2 |
|
368 | 368 | |
|
369 | 369 | verify that aliases are evaluated as well |
|
370 | 370 | |
|
371 | 371 | $ hg init aliastest |
|
372 | 372 | $ cd aliastest |
|
373 | 373 | $ cat > .hg/hgrc << EOF |
|
374 | 374 | > [ui] |
|
375 | 375 | > user = repo user |
|
376 | 376 | > EOF |
|
377 | 377 | $ touch index |
|
378 | 378 | $ unset HGUSER |
|
379 | 379 | $ hg ci -Am test |
|
380 | 380 | adding index |
|
381 | 381 | $ hg log --template '{author}\n' |
|
382 | 382 | repo user |
|
383 | 383 | $ cd .. |
|
384 | 384 | |
|
385 | 385 | alias has lower priority |
|
386 | 386 | |
|
387 | 387 | $ hg init aliaspriority |
|
388 | 388 | $ cd aliaspriority |
|
389 | 389 | $ cat > .hg/hgrc << EOF |
|
390 | 390 | > [ui] |
|
391 | 391 | > user = alias user |
|
392 | 392 | > username = repo user |
|
393 | 393 | > EOF |
|
394 | 394 | $ touch index |
|
395 | 395 | $ unset HGUSER |
|
396 | 396 | $ hg ci -Am test |
|
397 | 397 | adding index |
|
398 | 398 | $ hg log --template '{author}\n' |
|
399 | 399 | repo user |
|
400 | 400 | $ cd .. |
|
401 | 401 | |
|
402 | 402 | configs should be read in lexicographical order |
|
403 | 403 | |
|
404 | 404 | $ mkdir configs |
|
405 | 405 | $ for i in `$TESTDIR/seq.py 10 99`; do |
|
406 | 406 | > printf "[section]\nkey=$i" > configs/$i.rc |
|
407 | 407 | > done |
|
408 | 408 | $ HGRCPATH=configs hg config section.key |
|
409 | 409 | 99 |
|
410 | 410 | |
|
411 | 411 | Listing all config options |
|
412 | 412 | ========================== |
|
413 | 413 | |
|
414 | 414 | The feature is experimental and behavior may varies. This test exists to make sure the code is run. We grep it to avoid too much variability in its current experimental state. |
|
415 | 415 | |
|
416 | $ hg config --exp-all-known | grep commit | |
|
416 | $ hg config --exp-all-known | grep commit | grep -v ssh | |
|
417 | 417 | commands.commit.interactive.git=False |
|
418 | 418 | commands.commit.interactive.ignoreblanklines=False |
|
419 | 419 | commands.commit.interactive.ignorews=False |
|
420 | 420 | commands.commit.interactive.ignorewsamount=False |
|
421 | 421 | commands.commit.interactive.ignorewseol=False |
|
422 | 422 | commands.commit.interactive.nobinary=False |
|
423 | 423 | commands.commit.interactive.nodates=False |
|
424 | 424 | commands.commit.interactive.noprefix=False |
|
425 | 425 | commands.commit.interactive.showfunc=False |
|
426 | 426 | commands.commit.interactive.unified=None |
|
427 | 427 | commands.commit.interactive.word-diff=False |
|
428 | 428 | commands.commit.post-status=False |
|
429 | 429 | convert.git.committeractions=[*'messagedifferent'] (glob) |
|
430 | 430 | convert.svn.dangerous-set-commit-dates=False |
|
431 | 431 | experimental.copytrace.sourcecommitlimit=100 |
|
432 | 432 | phases.new-commit=draft |
|
433 | 433 | ui.allowemptycommit=False |
|
434 | 434 | ui.commitsubrepos=False |
|
435 | 435 | |
|
436 | 436 | |
|
437 | 437 | Configuration priority |
|
438 | 438 | ====================== |
|
439 | 439 | |
|
440 | 440 | setup necessary file |
|
441 | 441 | |
|
442 | 442 | $ cat > file-A.rc << EOF |
|
443 | 443 | > [config-test] |
|
444 | 444 | > basic = value-A |
|
445 | 445 | > pre-include= value-A |
|
446 | 446 | > %include ./included.rc |
|
447 | 447 | > post-include= value-A |
|
448 | 448 | > [command-templates] |
|
449 | 449 | > log = "value-A\n" |
|
450 | 450 | > EOF |
|
451 | 451 | |
|
452 | 452 | $ cat > file-B.rc << EOF |
|
453 | 453 | > [config-test] |
|
454 | 454 | > basic = value-B |
|
455 | 455 | > [ui] |
|
456 | 456 | > logtemplate = "value-B\n" |
|
457 | 457 | > EOF |
|
458 | 458 | |
|
459 | 459 | |
|
460 | 460 | $ cat > included.rc << EOF |
|
461 | 461 | > [config-test] |
|
462 | 462 | > pre-include= value-included |
|
463 | 463 | > post-include= value-included |
|
464 | 464 | > EOF |
|
465 | 465 | |
|
466 | 466 | $ cat > file-C.rc << EOF |
|
467 | 467 | > %include ./included-alias-C.rc |
|
468 | 468 | > [ui] |
|
469 | 469 | > logtemplate = "value-C\n" |
|
470 | 470 | > EOF |
|
471 | 471 | |
|
472 | 472 | $ cat > included-alias-C.rc << EOF |
|
473 | 473 | > [command-templates] |
|
474 | 474 | > log = "value-included\n" |
|
475 | 475 | > EOF |
|
476 | 476 | |
|
477 | 477 | |
|
478 | 478 | $ cat > file-D.rc << EOF |
|
479 | 479 | > [command-templates] |
|
480 | 480 | > log = "value-D\n" |
|
481 | 481 | > %include ./included-alias-D.rc |
|
482 | 482 | > EOF |
|
483 | 483 | |
|
484 | 484 | $ cat > included-alias-D.rc << EOF |
|
485 | 485 | > [ui] |
|
486 | 486 | > logtemplate = "value-included\n" |
|
487 | 487 | > EOF |
|
488 | 488 | |
|
489 | 489 | Simple order checking |
|
490 | 490 | --------------------- |
|
491 | 491 | |
|
492 | 492 | If file B is read after file A, value from B overwrite value from A. |
|
493 | 493 | |
|
494 | 494 | $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg config config-test.basic |
|
495 | 495 | value-B |
|
496 | 496 | |
|
497 | 497 | Ordering from include |
|
498 | 498 | --------------------- |
|
499 | 499 | |
|
500 | 500 | value from an include overwrite value defined before the include, but not the one defined after the include |
|
501 | 501 | |
|
502 | 502 | $ HGRCPATH="file-A.rc" hg config config-test.pre-include |
|
503 | 503 | value-included |
|
504 | 504 | $ HGRCPATH="file-A.rc" hg config config-test.post-include |
|
505 | 505 | value-A |
|
506 | 506 | |
|
507 | 507 | command line override |
|
508 | 508 | --------------------- |
|
509 | 509 | |
|
510 | 510 | $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg config config-test.basic --config config-test.basic=value-CLI |
|
511 | 511 | value-CLI |
|
512 | 512 | |
|
513 | 513 | Alias ordering |
|
514 | 514 | -------------- |
|
515 | 515 | |
|
516 | 516 | The official config is now `command-templates.log`, the historical |
|
517 | 517 | `ui.logtemplate` is a valid alternative for it. |
|
518 | 518 | |
|
519 | 519 | When both are defined, The config value read the last "win", this should keep |
|
520 | 520 | being true if the config have other alias. In other word, the config value read |
|
521 | 521 | earlier will be considered "lower level" and the config read later would be |
|
522 | 522 | considered "higher level". And higher level values wins. |
|
523 | 523 | |
|
524 | 524 | $ HGRCPATH="file-A.rc" hg log -r . |
|
525 | 525 | value-A |
|
526 | 526 | $ HGRCPATH="file-B.rc" hg log -r . |
|
527 | 527 | value-B |
|
528 | 528 | $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg log -r . |
|
529 | 529 | value-B |
|
530 | 530 | |
|
531 | 531 | Alias and include |
|
532 | 532 | ----------------- |
|
533 | 533 | |
|
534 | 534 | The pre/post include priority should also apply when tie-breaking alternatives. |
|
535 | 535 | See the case above for details about the two config options used. |
|
536 | 536 | |
|
537 | 537 | $ HGRCPATH="file-C.rc" hg log -r . |
|
538 | 538 | value-C |
|
539 | 539 | $ HGRCPATH="file-D.rc" hg log -r . |
|
540 | 540 | value-included |
|
541 | 541 | |
|
542 | 542 | command line override |
|
543 | 543 | --------------------- |
|
544 | 544 | |
|
545 | 545 | $ HGRCPATH=`path_list_var "file-A.rc:file-B.rc"` hg log -r . --config ui.logtemplate="value-CLI\n" |
|
546 | 546 | value-CLI |
@@ -1,618 +1,618 b'' | |||
|
1 | 1 | This test is a duplicate of 'test-http.t' feel free to factor out |
|
2 | 2 | parts that are not bundle1/bundle2 specific. |
|
3 | 3 | |
|
4 | 4 | #testcases sshv1 sshv2 |
|
5 | 5 | |
|
6 | 6 | #if sshv2 |
|
7 | 7 | $ cat >> $HGRCPATH << EOF |
|
8 | 8 | > [experimental] |
|
9 | 9 | > sshpeer.advertise-v2 = true |
|
10 | 10 | > sshserver.support-v2 = true |
|
11 | 11 | > EOF |
|
12 | 12 | #endif |
|
13 | 13 | |
|
14 | 14 | $ cat << EOF >> $HGRCPATH |
|
15 | 15 | > [devel] |
|
16 | 16 | > # This test is dedicated to interaction through old bundle |
|
17 | 17 | > legacy.exchange = bundle1 |
|
18 | 18 | > EOF |
|
19 | 19 | |
|
20 | 20 | |
|
21 | 21 | This test tries to exercise the ssh functionality with a dummy script |
|
22 | 22 | |
|
23 | 23 | creating 'remote' repo |
|
24 | 24 | |
|
25 | 25 | $ hg init remote |
|
26 | 26 | $ cd remote |
|
27 | 27 | $ echo this > foo |
|
28 | 28 | $ echo this > fooO |
|
29 | 29 | $ hg ci -A -m "init" foo fooO |
|
30 | 30 | |
|
31 | 31 | insert a closed branch (issue4428) |
|
32 | 32 | |
|
33 | 33 | $ hg up null |
|
34 | 34 | 0 files updated, 0 files merged, 2 files removed, 0 files unresolved |
|
35 | 35 | $ hg branch closed |
|
36 | 36 | marked working directory as branch closed |
|
37 | 37 | (branches are permanent and global, did you want a bookmark?) |
|
38 | 38 | $ hg ci -mc0 |
|
39 | 39 | $ hg ci --close-branch -mc1 |
|
40 | 40 | $ hg up -q default |
|
41 | 41 | |
|
42 | 42 | configure for serving |
|
43 | 43 | |
|
44 | 44 | $ cat <<EOF > .hg/hgrc |
|
45 | 45 | > [server] |
|
46 | 46 | > uncompressed = True |
|
47 | 47 | > |
|
48 | 48 | > [hooks] |
|
49 | 49 | > changegroup = sh -c "printenv.py --line changegroup-in-remote 0 ../dummylog" |
|
50 | 50 | > EOF |
|
51 | 51 | $ cd $TESTTMP |
|
52 | 52 | |
|
53 | 53 | repo not found error |
|
54 | 54 | |
|
55 | 55 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local |
|
56 | 56 | remote: abort: repository nonexistent not found |
|
57 | 57 | abort: no suitable response from remote hg |
|
58 | 58 | [255] |
|
59 | 59 | |
|
60 | 60 | non-existent absolute path |
|
61 | 61 | |
|
62 | 62 | #if no-msys |
|
63 | 63 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy//`pwd`/nonexistent local |
|
64 | 64 | remote: abort: repository /$TESTTMP/nonexistent not found |
|
65 | 65 | abort: no suitable response from remote hg |
|
66 | 66 | [255] |
|
67 | 67 | #endif |
|
68 | 68 | |
|
69 | 69 | clone remote via stream |
|
70 | 70 | |
|
71 | 71 | #if no-reposimplestore |
|
72 | 72 | |
|
73 | 73 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/remote local-stream |
|
74 | 74 | streaming all changes |
|
75 | 75 | 4 files to transfer, 602 bytes of data (no-zstd !) |
|
76 | 76 | transferred 602 bytes in * seconds (*) (glob) (no-zstd !) |
|
77 | 77 | 4 files to transfer, 621 bytes of data (zstd !) |
|
78 | 78 | transferred 621 bytes in * seconds (* */sec) (glob) (zstd !) |
|
79 | 79 | searching for changes |
|
80 | 80 | no changes found |
|
81 | 81 | updating to branch default |
|
82 | 82 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
83 | 83 | $ cd local-stream |
|
84 | 84 | $ hg verify |
|
85 | 85 | checking changesets |
|
86 | 86 | checking manifests |
|
87 | 87 | crosschecking files in changesets and manifests |
|
88 | 88 | checking files |
|
89 | 89 | checked 3 changesets with 2 changes to 2 files |
|
90 | 90 | $ hg branches |
|
91 | 91 | default 0:1160648e36ce |
|
92 | 92 | $ cd $TESTTMP |
|
93 | 93 | |
|
94 | 94 | clone bookmarks via stream |
|
95 | 95 | |
|
96 | 96 | $ hg -R local-stream book mybook |
|
97 | 97 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/local-stream stream2 |
|
98 | 98 | streaming all changes |
|
99 | 99 | 4 files to transfer, 602 bytes of data (no-zstd !) |
|
100 | 100 | transferred 602 bytes in * seconds (*) (glob) (no-zstd !) |
|
101 | 101 | 4 files to transfer, 621 bytes of data (zstd !) |
|
102 | 102 | transferred 621 bytes in * seconds (* */sec) (glob) (zstd !) |
|
103 | 103 | searching for changes |
|
104 | 104 | no changes found |
|
105 | 105 | updating to branch default |
|
106 | 106 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
107 | 107 | $ cd stream2 |
|
108 | 108 | $ hg book |
|
109 | 109 | mybook 0:1160648e36ce |
|
110 | 110 | $ cd $TESTTMP |
|
111 | 111 | $ rm -rf local-stream stream2 |
|
112 | 112 | |
|
113 | 113 | #endif |
|
114 | 114 | |
|
115 | 115 | clone remote via pull |
|
116 | 116 | |
|
117 | 117 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local |
|
118 | 118 | requesting all changes |
|
119 | 119 | adding changesets |
|
120 | 120 | adding manifests |
|
121 | 121 | adding file changes |
|
122 | 122 | added 3 changesets with 2 changes to 2 files |
|
123 | 123 | new changesets 1160648e36ce:ad076bfb429d |
|
124 | 124 | updating to branch default |
|
125 | 125 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
126 | 126 | |
|
127 | 127 | verify |
|
128 | 128 | |
|
129 | 129 | $ cd local |
|
130 | 130 | $ hg verify |
|
131 | 131 | checking changesets |
|
132 | 132 | checking manifests |
|
133 | 133 | crosschecking files in changesets and manifests |
|
134 | 134 | checking files |
|
135 | 135 | checked 3 changesets with 2 changes to 2 files |
|
136 | 136 | $ cat >> .hg/hgrc <<EOF |
|
137 | 137 | > [hooks] |
|
138 | 138 | > changegroup = sh -c "printenv.py --line changegroup-in-local 0 ../dummylog" |
|
139 | 139 | > EOF |
|
140 | 140 | |
|
141 | 141 | empty default pull |
|
142 | 142 | |
|
143 | 143 | $ hg paths |
|
144 | 144 | default = ssh://user@dummy/remote |
|
145 | 145 | $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" |
|
146 | 146 | pulling from ssh://user@dummy/remote |
|
147 | 147 | searching for changes |
|
148 | 148 | no changes found |
|
149 | 149 | |
|
150 | 150 | pull from wrong ssh URL |
|
151 | 151 | |
|
152 | 152 | $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist |
|
153 | 153 | pulling from ssh://user@dummy/doesnotexist |
|
154 | 154 | remote: abort: repository doesnotexist not found |
|
155 | 155 | abort: no suitable response from remote hg |
|
156 | 156 | [255] |
|
157 | 157 | |
|
158 | 158 | local change |
|
159 | 159 | |
|
160 | 160 | $ echo bleah > foo |
|
161 | 161 | $ hg ci -m "add" |
|
162 | 162 | |
|
163 | 163 | updating rc |
|
164 | 164 | |
|
165 | 165 | $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc |
|
166 | 166 | $ echo "[ui]" >> .hg/hgrc |
|
167 | 167 | $ echo "ssh = \"$PYTHON\" \"$TESTDIR/dummyssh\"" >> .hg/hgrc |
|
168 | 168 | |
|
169 | 169 | find outgoing |
|
170 | 170 | |
|
171 | 171 | $ hg out ssh://user@dummy/remote |
|
172 | 172 | comparing with ssh://user@dummy/remote |
|
173 | 173 | searching for changes |
|
174 | 174 | changeset: 3:a28a9d1a809c |
|
175 | 175 | tag: tip |
|
176 | 176 | parent: 0:1160648e36ce |
|
177 | 177 | user: test |
|
178 | 178 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
179 | 179 | summary: add |
|
180 | 180 | |
|
181 | 181 | |
|
182 | 182 | find incoming on the remote side |
|
183 | 183 | |
|
184 | 184 | $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/local |
|
185 | 185 | comparing with ssh://user@dummy/local |
|
186 | 186 | searching for changes |
|
187 | 187 | changeset: 3:a28a9d1a809c |
|
188 | 188 | tag: tip |
|
189 | 189 | parent: 0:1160648e36ce |
|
190 | 190 | user: test |
|
191 | 191 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
192 | 192 | summary: add |
|
193 | 193 | |
|
194 | 194 | |
|
195 | 195 | find incoming on the remote side (using absolute path) |
|
196 | 196 | |
|
197 | 197 | $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`" |
|
198 | 198 | comparing with ssh://user@dummy/$TESTTMP/local |
|
199 | 199 | searching for changes |
|
200 | 200 | changeset: 3:a28a9d1a809c |
|
201 | 201 | tag: tip |
|
202 | 202 | parent: 0:1160648e36ce |
|
203 | 203 | user: test |
|
204 | 204 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
205 | 205 | summary: add |
|
206 | 206 | |
|
207 | 207 | |
|
208 | 208 | push |
|
209 | 209 | |
|
210 | 210 | $ hg push |
|
211 | 211 | pushing to ssh://user@dummy/remote |
|
212 | 212 | searching for changes |
|
213 | 213 | remote: adding changesets |
|
214 | 214 | remote: adding manifests |
|
215 | 215 | remote: adding file changes |
|
216 | 216 | remote: added 1 changesets with 1 changes to 1 files |
|
217 | 217 | $ cd $TESTTMP/remote |
|
218 | 218 | |
|
219 | 219 | check remote tip |
|
220 | 220 | |
|
221 | 221 | $ hg tip |
|
222 | 222 | changeset: 3:a28a9d1a809c |
|
223 | 223 | tag: tip |
|
224 | 224 | parent: 0:1160648e36ce |
|
225 | 225 | user: test |
|
226 | 226 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
227 | 227 | summary: add |
|
228 | 228 | |
|
229 | 229 | $ hg verify |
|
230 | 230 | checking changesets |
|
231 | 231 | checking manifests |
|
232 | 232 | crosschecking files in changesets and manifests |
|
233 | 233 | checking files |
|
234 | 234 | checked 4 changesets with 3 changes to 2 files |
|
235 | 235 | $ hg cat -r tip foo |
|
236 | 236 | bleah |
|
237 | 237 | $ echo z > z |
|
238 | 238 | $ hg ci -A -m z z |
|
239 | 239 | created new head |
|
240 | 240 | |
|
241 | 241 | test pushkeys and bookmarks |
|
242 | 242 | |
|
243 | 243 | $ cd $TESTTMP/local |
|
244 | 244 | $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces |
|
245 | 245 | bookmarks |
|
246 | 246 | namespaces |
|
247 | 247 | phases |
|
248 | 248 | $ hg book foo -r 0 |
|
249 | 249 | $ hg out -B |
|
250 | 250 | comparing with ssh://user@dummy/remote |
|
251 | 251 | searching for changed bookmarks |
|
252 | 252 | foo 1160648e36ce |
|
253 | 253 | $ hg push -B foo |
|
254 | 254 | pushing to ssh://user@dummy/remote |
|
255 | 255 | searching for changes |
|
256 | 256 | no changes found |
|
257 | 257 | exporting bookmark foo |
|
258 | 258 | [1] |
|
259 | 259 | $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks |
|
260 | 260 | foo 1160648e36cec0054048a7edc4110c6f84fde594 |
|
261 | 261 | $ hg book -f foo |
|
262 | 262 | $ hg push --traceback |
|
263 | 263 | pushing to ssh://user@dummy/remote |
|
264 | 264 | searching for changes |
|
265 | 265 | no changes found |
|
266 | 266 | updating bookmark foo |
|
267 | 267 | [1] |
|
268 | 268 | $ hg book -d foo |
|
269 | 269 | $ hg in -B |
|
270 | 270 | comparing with ssh://user@dummy/remote |
|
271 | 271 | searching for changed bookmarks |
|
272 | 272 | foo a28a9d1a809c |
|
273 | 273 | $ hg book -f -r 0 foo |
|
274 | 274 | $ hg pull -B foo |
|
275 | 275 | pulling from ssh://user@dummy/remote |
|
276 | 276 | no changes found |
|
277 | 277 | updating bookmark foo |
|
278 | 278 | $ hg book -d foo |
|
279 | 279 | $ hg push -B foo |
|
280 | 280 | pushing to ssh://user@dummy/remote |
|
281 | 281 | searching for changes |
|
282 | 282 | no changes found |
|
283 | 283 | deleting remote bookmark foo |
|
284 | 284 | [1] |
|
285 | 285 | |
|
286 | 286 | a bad, evil hook that prints to stdout |
|
287 | 287 | |
|
288 | 288 | $ cat <<EOF > $TESTTMP/badhook |
|
289 | 289 | > import sys |
|
290 | 290 | > sys.stdout.write("KABOOM\n") |
|
291 | 291 | > EOF |
|
292 | 292 | |
|
293 | 293 | $ echo '[hooks]' >> ../remote/.hg/hgrc |
|
294 | 294 | $ echo "changegroup.stdout = \"$PYTHON\" $TESTTMP/badhook" >> ../remote/.hg/hgrc |
|
295 | 295 | $ echo r > r |
|
296 | 296 | $ hg ci -A -m z r |
|
297 | 297 | |
|
298 | 298 | push should succeed even though it has an unexpected response |
|
299 | 299 | |
|
300 | 300 | $ hg push |
|
301 | 301 | pushing to ssh://user@dummy/remote |
|
302 | 302 | searching for changes |
|
303 | 303 | remote has heads on branch 'default' that are not known locally: 6c0482d977a3 |
|
304 | 304 | remote: adding changesets |
|
305 | 305 | remote: adding manifests |
|
306 | 306 | remote: adding file changes |
|
307 | 307 | remote: added 1 changesets with 1 changes to 1 files (py3 !) |
|
308 | 308 | remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !) |
|
309 | 309 | remote: KABOOM |
|
310 | 310 | remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !) |
|
311 | 311 | $ hg -R ../remote heads |
|
312 | 312 | changeset: 5:1383141674ec |
|
313 | 313 | tag: tip |
|
314 | 314 | parent: 3:a28a9d1a809c |
|
315 | 315 | user: test |
|
316 | 316 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
317 | 317 | summary: z |
|
318 | 318 | |
|
319 | 319 | changeset: 4:6c0482d977a3 |
|
320 | 320 | parent: 0:1160648e36ce |
|
321 | 321 | user: test |
|
322 | 322 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
323 | 323 | summary: z |
|
324 | 324 | |
|
325 | 325 | |
|
326 | 326 | clone bookmarks |
|
327 | 327 | |
|
328 | 328 | $ hg -R ../remote bookmark test |
|
329 | 329 | $ hg -R ../remote bookmarks |
|
330 | 330 | * test 4:6c0482d977a3 |
|
331 | 331 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks |
|
332 | 332 | requesting all changes |
|
333 | 333 | adding changesets |
|
334 | 334 | adding manifests |
|
335 | 335 | adding file changes |
|
336 | 336 | added 6 changesets with 5 changes to 4 files (+1 heads) |
|
337 | 337 | new changesets 1160648e36ce:1383141674ec |
|
338 | 338 | updating to branch default |
|
339 | 339 | 3 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
340 | 340 | $ hg -R local-bookmarks bookmarks |
|
341 | 341 | test 4:6c0482d977a3 |
|
342 | 342 | |
|
343 | 343 | passwords in ssh urls are not supported |
|
344 | 344 | (we use a glob here because different Python versions give different |
|
345 | 345 | results here) |
|
346 | 346 | |
|
347 | 347 | $ hg push ssh://user:erroneouspwd@dummy/remote |
|
348 | 348 | pushing to ssh://user:*@dummy/remote (glob) |
|
349 | 349 | abort: password in URL not supported |
|
350 | 350 | [255] |
|
351 | 351 | |
|
352 | 352 | $ cd $TESTTMP |
|
353 | 353 | |
|
354 | 354 | hide outer repo |
|
355 | 355 | $ hg init |
|
356 | 356 | |
|
357 | 357 | Test remote paths with spaces (issue2983): |
|
358 | 358 | |
|
359 | 359 | $ hg init --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo" |
|
360 | 360 | $ touch "$TESTTMP/a repo/test" |
|
361 | 361 | $ hg -R 'a repo' commit -A -m "test" |
|
362 | 362 | adding test |
|
363 | 363 | $ hg -R 'a repo' tag tag |
|
364 | 364 | $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo" |
|
365 | 365 | 73649e48688a |
|
366 | 366 | |
|
367 | 367 | $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO" |
|
368 | 368 | abort: unknown revision 'noNoNO' |
|
369 | 369 | [255] |
|
370 | 370 | |
|
371 | 371 | Test (non-)escaping of remote paths with spaces when cloning (issue3145): |
|
372 | 372 | |
|
373 | 373 | $ hg clone --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo" |
|
374 | 374 | destination directory: a repo |
|
375 | 375 | abort: destination 'a repo' is not empty |
|
376 | 376 | [10] |
|
377 | 377 | |
|
378 | 378 | Test hg-ssh using a helper script that will restore PYTHONPATH (which might |
|
379 | 379 | have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right |
|
380 | 380 | parameters: |
|
381 | 381 | |
|
382 | 382 | $ cat > ssh.sh << EOF |
|
383 | 383 | > userhost="\$1" |
|
384 | 384 | > SSH_ORIGINAL_COMMAND="\$2" |
|
385 | 385 | > export SSH_ORIGINAL_COMMAND |
|
386 | 386 | > PYTHONPATH="$PYTHONPATH" |
|
387 | 387 | > export PYTHONPATH |
|
388 | 388 | > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo" |
|
389 | 389 | > EOF |
|
390 | 390 | |
|
391 | 391 | $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo" |
|
392 | 392 | 73649e48688a |
|
393 | 393 | |
|
394 | 394 | $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo" |
|
395 | 395 | remote: Illegal repository "$TESTTMP/a'repo" |
|
396 | 396 | abort: no suitable response from remote hg |
|
397 | 397 | [255] |
|
398 | 398 | |
|
399 | 399 | $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo" |
|
400 | 400 | remote: Illegal command "hacking -R 'a'\''repo' serve --stdio" |
|
401 | 401 | abort: no suitable response from remote hg |
|
402 | 402 | [255] |
|
403 | 403 | |
|
404 | 404 | $ SSH_ORIGINAL_COMMAND="'hg' serve -R 'a'repo' --stdio" "$PYTHON" "$TESTDIR/../contrib/hg-ssh" |
|
405 | 405 | Illegal command "'hg' serve -R 'a'repo' --stdio": No closing quotation |
|
406 | 406 | [255] |
|
407 | 407 | |
|
408 | 408 | Test hg-ssh in read-only mode: |
|
409 | 409 | |
|
410 | 410 | $ cat > ssh.sh << EOF |
|
411 | 411 | > userhost="\$1" |
|
412 | 412 | > SSH_ORIGINAL_COMMAND="\$2" |
|
413 | 413 | > export SSH_ORIGINAL_COMMAND |
|
414 | 414 | > PYTHONPATH="$PYTHONPATH" |
|
415 | 415 | > export PYTHONPATH |
|
416 | 416 | > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote" |
|
417 | 417 | > EOF |
|
418 | 418 | |
|
419 | 419 | $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local |
|
420 | 420 | requesting all changes |
|
421 | 421 | adding changesets |
|
422 | 422 | adding manifests |
|
423 | 423 | adding file changes |
|
424 | 424 | added 6 changesets with 5 changes to 4 files (+1 heads) |
|
425 | 425 | new changesets 1160648e36ce:1383141674ec |
|
426 | 426 | updating to branch default |
|
427 | 427 | 3 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
428 | 428 | |
|
429 | 429 | $ cd read-only-local |
|
430 | 430 | $ echo "baz" > bar |
|
431 | 431 | $ hg ci -A -m "unpushable commit" bar |
|
432 | 432 | $ hg push --ssh "sh ../ssh.sh" |
|
433 | 433 | pushing to ssh://user@dummy/*/remote (glob) |
|
434 | 434 | searching for changes |
|
435 | 435 | remote: Permission denied |
|
436 | 436 | remote: abort: pretxnopen.hg-ssh hook failed |
|
437 | 437 | remote: Permission denied |
|
438 | 438 | remote: pushkey-abort: prepushkey.hg-ssh hook failed |
|
439 | 439 | updating 6c0482d977a3 to public failed! |
|
440 | 440 | [1] |
|
441 | 441 | |
|
442 | 442 | $ cd $TESTTMP |
|
443 | 443 | |
|
444 | 444 | stderr from remote commands should be printed before stdout from local code (issue4336) |
|
445 | 445 | |
|
446 | 446 | $ hg clone remote stderr-ordering |
|
447 | 447 | updating to branch default |
|
448 | 448 | 3 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
449 | 449 | $ cd stderr-ordering |
|
450 | 450 | $ cat >> localwrite.py << EOF |
|
451 | 451 | > from mercurial import exchange, extensions |
|
452 | 452 | > |
|
453 | 453 | > def wrappedpush(orig, repo, *args, **kwargs): |
|
454 | 454 | > res = orig(repo, *args, **kwargs) |
|
455 | 455 | > repo.ui.write(b'local stdout\n') |
|
456 | 456 | > return res |
|
457 | 457 | > |
|
458 | 458 | > def extsetup(ui): |
|
459 | 459 | > extensions.wrapfunction(exchange, b'push', wrappedpush) |
|
460 | 460 | > EOF |
|
461 | 461 | |
|
462 | 462 | $ cat >> .hg/hgrc << EOF |
|
463 | 463 | > [paths] |
|
464 | 464 | > default-push = ssh://user@dummy/remote |
|
465 | 465 | > [ui] |
|
466 | 466 | > ssh = "$PYTHON" "$TESTDIR/dummyssh" |
|
467 | 467 | > [extensions] |
|
468 | 468 | > localwrite = localwrite.py |
|
469 | 469 | > EOF |
|
470 | 470 | |
|
471 | 471 | $ echo localwrite > foo |
|
472 | 472 | $ hg commit -m 'testing localwrite' |
|
473 | 473 | $ hg push |
|
474 | 474 | pushing to ssh://user@dummy/remote |
|
475 | 475 | searching for changes |
|
476 | 476 | remote: adding changesets |
|
477 | 477 | remote: adding manifests |
|
478 | 478 | remote: adding file changes |
|
479 | 479 | remote: added 1 changesets with 1 changes to 1 files (py3 !) |
|
480 | 480 | remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !) |
|
481 | 481 | remote: KABOOM |
|
482 | 482 | remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !) |
|
483 | 483 | local stdout |
|
484 | 484 | |
|
485 | 485 | debug output |
|
486 | 486 | |
|
487 | 487 | $ hg pull --debug ssh://user@dummy/remote |
|
488 | 488 | pulling from ssh://user@dummy/remote |
|
489 |
running .* ".*/dummyssh" ['"]user@dummy['"] |
|
|
489 | running .* ".*[/\\]dummyssh" ['"]user@dummy['"] ['"]hg -R remote serve --stdio['"] (re) | |
|
490 | 490 | sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !) |
|
491 | 491 | sending hello command |
|
492 | 492 | sending between command |
|
493 | 493 | remote: 444 (sshv1 no-rust !) |
|
494 | 494 | remote: 463 (sshv1 rust !) |
|
495 | 495 | protocol upgraded to exp-ssh-v2-0003 (sshv2 !) |
|
496 | 496 | remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (no-rust !) |
|
497 | 497 | remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,persistent-nodemap,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (rust !) |
|
498 | 498 | remote: 1 (sshv1 !) |
|
499 | 499 | sending protocaps command |
|
500 | 500 | preparing listkeys for "bookmarks" |
|
501 | 501 | sending listkeys command |
|
502 | 502 | received listkey for "bookmarks": 45 bytes |
|
503 | 503 | query 1; heads |
|
504 | 504 | sending batch command |
|
505 | 505 | searching for changes |
|
506 | 506 | all remote heads known locally |
|
507 | 507 | no changes found |
|
508 | 508 | preparing listkeys for "phases" |
|
509 | 509 | sending listkeys command |
|
510 | 510 | received listkey for "phases": 15 bytes |
|
511 | 511 | checking for updated bookmarks |
|
512 | 512 | |
|
513 | 513 | $ cd $TESTTMP |
|
514 | 514 | |
|
515 | 515 | $ cat dummylog |
|
516 | 516 | Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio |
|
517 | 517 | Got arguments 1:user@dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio (no-msys !) |
|
518 | 518 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
519 | 519 | Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio (no-reposimplestore !) |
|
520 | 520 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !) |
|
521 | 521 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !) |
|
522 | 522 | Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio |
|
523 | 523 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
524 | 524 | Got arguments 1:user@dummy 2:hg -R local serve --stdio |
|
525 | 525 | Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio |
|
526 | 526 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
527 | 527 | changegroup-in-remote hook: HG_HOOKNAME=changegroup |
|
528 | 528 | HG_HOOKTYPE=changegroup |
|
529 | 529 | HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 |
|
530 | 530 | HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 |
|
531 | 531 | HG_SOURCE=serve |
|
532 | 532 | HG_TXNID=TXN:$ID$ |
|
533 | 533 | HG_TXNNAME=serve |
|
534 | 534 | remote:ssh:$LOCALIP |
|
535 | 535 | HG_URL=remote:ssh:$LOCALIP |
|
536 | 536 | |
|
537 | 537 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
538 | 538 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
539 | 539 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
540 | 540 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
541 | 541 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
542 | 542 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
543 | 543 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
544 | 544 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
545 | 545 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
546 | 546 | changegroup-in-remote hook: HG_HOOKNAME=changegroup |
|
547 | 547 | HG_HOOKTYPE=changegroup |
|
548 | 548 | HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 |
|
549 | 549 | HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 |
|
550 | 550 | HG_SOURCE=serve |
|
551 | 551 | HG_TXNID=TXN:$ID$ |
|
552 | 552 | HG_TXNNAME=serve |
|
553 | 553 | remote:ssh:$LOCALIP |
|
554 | 554 | HG_URL=remote:ssh:$LOCALIP |
|
555 | 555 | |
|
556 | 556 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
557 | 557 | Got arguments 1:user@dummy 2:hg init 'a repo' |
|
558 | 558 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
559 | 559 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
560 | 560 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
561 | 561 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
562 | 562 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
563 | 563 | changegroup-in-remote hook: HG_HOOKNAME=changegroup |
|
564 | 564 | HG_HOOKTYPE=changegroup |
|
565 | 565 | HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8 |
|
566 | 566 | HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8 |
|
567 | 567 | HG_SOURCE=serve |
|
568 | 568 | HG_TXNID=TXN:$ID$ |
|
569 | 569 | HG_TXNNAME=serve |
|
570 | 570 | remote:ssh:$LOCALIP |
|
571 | 571 | HG_URL=remote:ssh:$LOCALIP |
|
572 | 572 | |
|
573 | 573 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
574 | 574 | |
|
575 | 575 | remote hook failure is attributed to remote |
|
576 | 576 | |
|
577 | 577 | $ cat > $TESTTMP/failhook << EOF |
|
578 | 578 | > def hook(ui, repo, **kwargs): |
|
579 | 579 | > ui.write(b'hook failure!\n') |
|
580 | 580 | > ui.flush() |
|
581 | 581 | > return 1 |
|
582 | 582 | > EOF |
|
583 | 583 | |
|
584 | 584 | $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc |
|
585 | 585 | |
|
586 | 586 | $ hg -q --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout |
|
587 | 587 | $ cd hookout |
|
588 | 588 | $ touch hookfailure |
|
589 | 589 | $ hg -q commit -A -m 'remote hook failure' |
|
590 | 590 | $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" push |
|
591 | 591 | pushing to ssh://user@dummy/remote |
|
592 | 592 | searching for changes |
|
593 | 593 | remote: adding changesets |
|
594 | 594 | remote: adding manifests |
|
595 | 595 | remote: adding file changes |
|
596 | 596 | remote: hook failure! |
|
597 | 597 | remote: transaction abort! |
|
598 | 598 | remote: rollback completed |
|
599 | 599 | remote: abort: pretxnchangegroup.fail hook failed |
|
600 | 600 | [1] |
|
601 | 601 | |
|
602 | 602 | abort during pull is properly reported as such |
|
603 | 603 | |
|
604 | 604 | $ echo morefoo >> ../remote/foo |
|
605 | 605 | $ hg -R ../remote commit --message "more foo to be pulled" |
|
606 | 606 | $ cat >> ../remote/.hg/hgrc << EOF |
|
607 | 607 | > [extensions] |
|
608 | 608 | > crash = ${TESTDIR}/crashgetbundler.py |
|
609 | 609 | > EOF |
|
610 | 610 | $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" pull |
|
611 | 611 | pulling from ssh://user@dummy/remote |
|
612 | 612 | searching for changes |
|
613 | 613 | adding changesets |
|
614 | 614 | remote: abort: this is an exercise |
|
615 | 615 | transaction abort! |
|
616 | 616 | rollback completed |
|
617 | 617 | abort: stream ended unexpectedly (got 0 bytes, expected 4) |
|
618 | 618 | [255] |
@@ -1,220 +1,220 b'' | |||
|
1 | 1 | This test tries to exercise the ssh functionality with a dummy script |
|
2 | 2 | |
|
3 | 3 | #testcases sshv1 sshv2 |
|
4 | 4 | |
|
5 | 5 | #if sshv2 |
|
6 | 6 | $ cat >> $HGRCPATH << EOF |
|
7 | 7 | > [experimental] |
|
8 | 8 | > sshpeer.advertise-v2 = true |
|
9 | 9 | > sshserver.support-v2 = true |
|
10 | 10 | > EOF |
|
11 | 11 | #endif |
|
12 | 12 | |
|
13 | 13 | creating 'remote' repo |
|
14 | 14 | |
|
15 | 15 | $ hg init remote |
|
16 | 16 | $ cd remote |
|
17 | 17 | $ hg unbundle "$TESTDIR/bundles/remote.hg" |
|
18 | 18 | adding changesets |
|
19 | 19 | adding manifests |
|
20 | 20 | adding file changes |
|
21 | 21 | added 9 changesets with 7 changes to 4 files (+1 heads) |
|
22 | 22 | new changesets bfaf4b5cbf01:916f1afdef90 (9 drafts) |
|
23 | 23 | (run 'hg heads' to see heads, 'hg merge' to merge) |
|
24 | 24 | $ hg up tip |
|
25 | 25 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
26 | 26 | $ cd .. |
|
27 | 27 | |
|
28 | 28 | clone remote via stream |
|
29 | 29 | |
|
30 | 30 | $ for i in 0 1 2 3 4 5 6 7 8; do |
|
31 |
> hg clone |
|
|
31 | > hg clone --stream -r "$i" ssh://user@dummy/remote test-"$i" | |
|
32 | 32 | > if cd test-"$i"; then |
|
33 | 33 | > hg verify |
|
34 | 34 | > cd .. |
|
35 | 35 | > fi |
|
36 | 36 | > done |
|
37 | 37 | adding changesets |
|
38 | 38 | adding manifests |
|
39 | 39 | adding file changes |
|
40 | 40 | added 1 changesets with 1 changes to 1 files |
|
41 | 41 | new changesets bfaf4b5cbf01 |
|
42 | 42 | updating to branch default |
|
43 | 43 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
44 | 44 | checking changesets |
|
45 | 45 | checking manifests |
|
46 | 46 | crosschecking files in changesets and manifests |
|
47 | 47 | checking files |
|
48 | 48 | checked 1 changesets with 1 changes to 1 files |
|
49 | 49 | adding changesets |
|
50 | 50 | adding manifests |
|
51 | 51 | adding file changes |
|
52 | 52 | added 2 changesets with 2 changes to 1 files |
|
53 | 53 | new changesets bfaf4b5cbf01:21f32785131f |
|
54 | 54 | updating to branch default |
|
55 | 55 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
56 | 56 | checking changesets |
|
57 | 57 | checking manifests |
|
58 | 58 | crosschecking files in changesets and manifests |
|
59 | 59 | checking files |
|
60 | 60 | checked 2 changesets with 2 changes to 1 files |
|
61 | 61 | adding changesets |
|
62 | 62 | adding manifests |
|
63 | 63 | adding file changes |
|
64 | 64 | added 3 changesets with 3 changes to 1 files |
|
65 | 65 | new changesets bfaf4b5cbf01:4ce51a113780 |
|
66 | 66 | updating to branch default |
|
67 | 67 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
68 | 68 | checking changesets |
|
69 | 69 | checking manifests |
|
70 | 70 | crosschecking files in changesets and manifests |
|
71 | 71 | checking files |
|
72 | 72 | checked 3 changesets with 3 changes to 1 files |
|
73 | 73 | adding changesets |
|
74 | 74 | adding manifests |
|
75 | 75 | adding file changes |
|
76 | 76 | added 4 changesets with 4 changes to 1 files |
|
77 | 77 | new changesets bfaf4b5cbf01:93ee6ab32777 |
|
78 | 78 | updating to branch default |
|
79 | 79 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
80 | 80 | checking changesets |
|
81 | 81 | checking manifests |
|
82 | 82 | crosschecking files in changesets and manifests |
|
83 | 83 | checking files |
|
84 | 84 | checked 4 changesets with 4 changes to 1 files |
|
85 | 85 | adding changesets |
|
86 | 86 | adding manifests |
|
87 | 87 | adding file changes |
|
88 | 88 | added 2 changesets with 2 changes to 1 files |
|
89 | 89 | new changesets bfaf4b5cbf01:c70afb1ee985 |
|
90 | 90 | updating to branch default |
|
91 | 91 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
92 | 92 | checking changesets |
|
93 | 93 | checking manifests |
|
94 | 94 | crosschecking files in changesets and manifests |
|
95 | 95 | checking files |
|
96 | 96 | checked 2 changesets with 2 changes to 1 files |
|
97 | 97 | adding changesets |
|
98 | 98 | adding manifests |
|
99 | 99 | adding file changes |
|
100 | 100 | added 3 changesets with 3 changes to 1 files |
|
101 | 101 | new changesets bfaf4b5cbf01:f03ae5a9b979 |
|
102 | 102 | updating to branch default |
|
103 | 103 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
104 | 104 | checking changesets |
|
105 | 105 | checking manifests |
|
106 | 106 | crosschecking files in changesets and manifests |
|
107 | 107 | checking files |
|
108 | 108 | checked 3 changesets with 3 changes to 1 files |
|
109 | 109 | adding changesets |
|
110 | 110 | adding manifests |
|
111 | 111 | adding file changes |
|
112 | 112 | added 4 changesets with 5 changes to 2 files |
|
113 | 113 | new changesets bfaf4b5cbf01:095cb14b1b4d |
|
114 | 114 | updating to branch default |
|
115 | 115 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
116 | 116 | checking changesets |
|
117 | 117 | checking manifests |
|
118 | 118 | crosschecking files in changesets and manifests |
|
119 | 119 | checking files |
|
120 | 120 | checked 4 changesets with 5 changes to 2 files |
|
121 | 121 | adding changesets |
|
122 | 122 | adding manifests |
|
123 | 123 | adding file changes |
|
124 | 124 | added 5 changesets with 6 changes to 3 files |
|
125 | 125 | new changesets bfaf4b5cbf01:faa2e4234c7a |
|
126 | 126 | updating to branch default |
|
127 | 127 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
128 | 128 | checking changesets |
|
129 | 129 | checking manifests |
|
130 | 130 | crosschecking files in changesets and manifests |
|
131 | 131 | checking files |
|
132 | 132 | checked 5 changesets with 6 changes to 3 files |
|
133 | 133 | adding changesets |
|
134 | 134 | adding manifests |
|
135 | 135 | adding file changes |
|
136 | 136 | added 5 changesets with 5 changes to 2 files |
|
137 | 137 | new changesets bfaf4b5cbf01:916f1afdef90 |
|
138 | 138 | updating to branch default |
|
139 | 139 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
140 | 140 | checking changesets |
|
141 | 141 | checking manifests |
|
142 | 142 | crosschecking files in changesets and manifests |
|
143 | 143 | checking files |
|
144 | 144 | checked 5 changesets with 5 changes to 2 files |
|
145 | 145 | $ cd test-8 |
|
146 | 146 | $ hg pull ../test-7 |
|
147 | 147 | pulling from ../test-7 |
|
148 | 148 | searching for changes |
|
149 | 149 | adding changesets |
|
150 | 150 | adding manifests |
|
151 | 151 | adding file changes |
|
152 | 152 | added 4 changesets with 2 changes to 3 files (+1 heads) |
|
153 | 153 | new changesets c70afb1ee985:faa2e4234c7a |
|
154 | 154 | (run 'hg heads' to see heads, 'hg merge' to merge) |
|
155 | 155 | $ hg verify |
|
156 | 156 | checking changesets |
|
157 | 157 | checking manifests |
|
158 | 158 | crosschecking files in changesets and manifests |
|
159 | 159 | checking files |
|
160 | 160 | checked 9 changesets with 7 changes to 4 files |
|
161 | 161 | $ cd .. |
|
162 | 162 | $ cd test-1 |
|
163 |
$ hg pull - |
|
|
163 | $ hg pull -r 4 ssh://user@dummy/remote | |
|
164 | 164 | pulling from ssh://user@dummy/remote |
|
165 | 165 | searching for changes |
|
166 | 166 | adding changesets |
|
167 | 167 | adding manifests |
|
168 | 168 | adding file changes |
|
169 | 169 | added 1 changesets with 0 changes to 0 files (+1 heads) |
|
170 | 170 | new changesets c70afb1ee985 |
|
171 | 171 | (run 'hg heads' to see heads, 'hg merge' to merge) |
|
172 | 172 | $ hg verify |
|
173 | 173 | checking changesets |
|
174 | 174 | checking manifests |
|
175 | 175 | crosschecking files in changesets and manifests |
|
176 | 176 | checking files |
|
177 | 177 | checked 3 changesets with 2 changes to 1 files |
|
178 |
$ hg pull |
|
|
178 | $ hg pull ssh://user@dummy/remote | |
|
179 | 179 | pulling from ssh://user@dummy/remote |
|
180 | 180 | searching for changes |
|
181 | 181 | adding changesets |
|
182 | 182 | adding manifests |
|
183 | 183 | adding file changes |
|
184 | 184 | added 6 changesets with 5 changes to 4 files |
|
185 | 185 | new changesets 4ce51a113780:916f1afdef90 |
|
186 | 186 | (run 'hg update' to get a working copy) |
|
187 | 187 | $ cd .. |
|
188 | 188 | $ cd test-2 |
|
189 |
$ hg pull - |
|
|
189 | $ hg pull -r 5 ssh://user@dummy/remote | |
|
190 | 190 | pulling from ssh://user@dummy/remote |
|
191 | 191 | searching for changes |
|
192 | 192 | adding changesets |
|
193 | 193 | adding manifests |
|
194 | 194 | adding file changes |
|
195 | 195 | added 2 changesets with 0 changes to 0 files (+1 heads) |
|
196 | 196 | new changesets c70afb1ee985:f03ae5a9b979 |
|
197 | 197 | (run 'hg heads' to see heads, 'hg merge' to merge) |
|
198 | 198 | $ hg verify |
|
199 | 199 | checking changesets |
|
200 | 200 | checking manifests |
|
201 | 201 | crosschecking files in changesets and manifests |
|
202 | 202 | checking files |
|
203 | 203 | checked 5 changesets with 3 changes to 1 files |
|
204 |
$ hg pull |
|
|
204 | $ hg pull ssh://user@dummy/remote | |
|
205 | 205 | pulling from ssh://user@dummy/remote |
|
206 | 206 | searching for changes |
|
207 | 207 | adding changesets |
|
208 | 208 | adding manifests |
|
209 | 209 | adding file changes |
|
210 | 210 | added 4 changesets with 4 changes to 4 files |
|
211 | 211 | new changesets 93ee6ab32777:916f1afdef90 |
|
212 | 212 | (run 'hg update' to get a working copy) |
|
213 | 213 | $ hg verify |
|
214 | 214 | checking changesets |
|
215 | 215 | checking manifests |
|
216 | 216 | crosschecking files in changesets and manifests |
|
217 | 217 | checking files |
|
218 | 218 | checked 9 changesets with 7 changes to 4 files |
|
219 | 219 | |
|
220 | 220 | $ cd .. |
@@ -1,737 +1,737 b'' | |||
|
1 | 1 | #testcases sshv1 sshv2 |
|
2 | 2 | |
|
3 | 3 | #if sshv2 |
|
4 | 4 | $ cat >> $HGRCPATH << EOF |
|
5 | 5 | > [experimental] |
|
6 | 6 | > sshpeer.advertise-v2 = true |
|
7 | 7 | > sshserver.support-v2 = true |
|
8 | 8 | > EOF |
|
9 | 9 | #endif |
|
10 | 10 | |
|
11 | 11 | This test tries to exercise the ssh functionality with a dummy script |
|
12 | 12 | |
|
13 | 13 | creating 'remote' repo |
|
14 | 14 | |
|
15 | 15 | $ hg init remote |
|
16 | 16 | $ cd remote |
|
17 | 17 | $ echo this > foo |
|
18 | 18 | $ echo this > fooO |
|
19 | 19 | $ hg ci -A -m "init" foo fooO |
|
20 | 20 | |
|
21 | 21 | insert a closed branch (issue4428) |
|
22 | 22 | |
|
23 | 23 | $ hg up null |
|
24 | 24 | 0 files updated, 0 files merged, 2 files removed, 0 files unresolved |
|
25 | 25 | $ hg branch closed |
|
26 | 26 | marked working directory as branch closed |
|
27 | 27 | (branches are permanent and global, did you want a bookmark?) |
|
28 | 28 | $ hg ci -mc0 |
|
29 | 29 | $ hg ci --close-branch -mc1 |
|
30 | 30 | $ hg up -q default |
|
31 | 31 | |
|
32 | 32 | configure for serving |
|
33 | 33 | |
|
34 | 34 | $ cat <<EOF > .hg/hgrc |
|
35 | 35 | > [server] |
|
36 | 36 | > uncompressed = True |
|
37 | 37 | > |
|
38 | 38 | > [hooks] |
|
39 | 39 | > changegroup = sh -c "printenv.py --line changegroup-in-remote 0 ../dummylog" |
|
40 | 40 | > EOF |
|
41 | 41 | $ cd $TESTTMP |
|
42 | 42 | |
|
43 | 43 | repo not found error |
|
44 | 44 | |
|
45 | 45 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local |
|
46 | 46 | remote: abort: repository nonexistent not found |
|
47 | 47 | abort: no suitable response from remote hg |
|
48 | 48 | [255] |
|
49 | 49 | $ hg clone -q -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local |
|
50 | 50 | remote: abort: repository nonexistent not found |
|
51 | 51 | abort: no suitable response from remote hg |
|
52 | 52 | [255] |
|
53 | 53 | |
|
54 | 54 | non-existent absolute path |
|
55 | 55 | |
|
56 | 56 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/`pwd`/nonexistent local |
|
57 | 57 | remote: abort: repository $TESTTMP/nonexistent not found |
|
58 | 58 | abort: no suitable response from remote hg |
|
59 | 59 | [255] |
|
60 | 60 | |
|
61 | 61 | clone remote via stream |
|
62 | 62 | |
|
63 | 63 | #if no-reposimplestore |
|
64 | 64 | |
|
65 | 65 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/remote local-stream |
|
66 | 66 | streaming all changes |
|
67 | 67 | 8 files to transfer, 827 bytes of data (no-zstd !) |
|
68 | 68 | transferred 827 bytes in * seconds (*) (glob) (no-zstd !) |
|
69 | 69 | 8 files to transfer, 846 bytes of data (zstd !) |
|
70 | 70 | transferred * bytes in * seconds (* */sec) (glob) (zstd !) |
|
71 | 71 | updating to branch default |
|
72 | 72 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
73 | 73 | $ cd local-stream |
|
74 | 74 | $ hg verify |
|
75 | 75 | checking changesets |
|
76 | 76 | checking manifests |
|
77 | 77 | crosschecking files in changesets and manifests |
|
78 | 78 | checking files |
|
79 | 79 | checked 3 changesets with 2 changes to 2 files |
|
80 | 80 | $ hg branches |
|
81 | 81 | default 0:1160648e36ce |
|
82 | 82 | $ cd $TESTTMP |
|
83 | 83 | |
|
84 | 84 | clone bookmarks via stream |
|
85 | 85 | |
|
86 | 86 | $ hg -R local-stream book mybook |
|
87 | 87 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" --stream ssh://user@dummy/local-stream stream2 |
|
88 | 88 | streaming all changes |
|
89 | 89 | 15 files to transfer, * of data (glob) |
|
90 | 90 | transferred * in * seconds (*) (glob) |
|
91 | 91 | updating to branch default |
|
92 | 92 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
93 | 93 | $ cd stream2 |
|
94 | 94 | $ hg book |
|
95 | 95 | mybook 0:1160648e36ce |
|
96 | 96 | $ cd $TESTTMP |
|
97 | 97 | $ rm -rf local-stream stream2 |
|
98 | 98 | |
|
99 | 99 | #endif |
|
100 | 100 | |
|
101 | 101 | clone remote via pull |
|
102 | 102 | |
|
103 | 103 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local |
|
104 | 104 | requesting all changes |
|
105 | 105 | adding changesets |
|
106 | 106 | adding manifests |
|
107 | 107 | adding file changes |
|
108 | 108 | added 3 changesets with 2 changes to 2 files |
|
109 | 109 | new changesets 1160648e36ce:ad076bfb429d |
|
110 | 110 | updating to branch default |
|
111 | 111 | 2 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
112 | 112 | |
|
113 | 113 | verify |
|
114 | 114 | |
|
115 | 115 | $ cd local |
|
116 | 116 | $ hg verify |
|
117 | 117 | checking changesets |
|
118 | 118 | checking manifests |
|
119 | 119 | crosschecking files in changesets and manifests |
|
120 | 120 | checking files |
|
121 | 121 | checked 3 changesets with 2 changes to 2 files |
|
122 | 122 | $ cat >> .hg/hgrc <<EOF |
|
123 | 123 | > [hooks] |
|
124 | 124 | > changegroup = sh -c "printenv.py changegroup-in-local 0 ../dummylog" |
|
125 | 125 | > EOF |
|
126 | 126 | |
|
127 | 127 | empty default pull |
|
128 | 128 | |
|
129 | 129 | $ hg paths |
|
130 | 130 | default = ssh://user@dummy/remote |
|
131 | 131 | $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" |
|
132 | 132 | pulling from ssh://user@dummy/remote |
|
133 | 133 | searching for changes |
|
134 | 134 | no changes found |
|
135 | 135 | |
|
136 | 136 | pull from wrong ssh URL |
|
137 | 137 | |
|
138 | 138 | $ hg pull -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist |
|
139 | 139 | pulling from ssh://user@dummy/doesnotexist |
|
140 | 140 | remote: abort: repository doesnotexist not found |
|
141 | 141 | abort: no suitable response from remote hg |
|
142 | 142 | [255] |
|
143 | 143 | |
|
144 | 144 | local change |
|
145 | 145 | |
|
146 | 146 | $ echo bleah > foo |
|
147 | 147 | $ hg ci -m "add" |
|
148 | 148 | |
|
149 | 149 | updating rc |
|
150 | 150 | |
|
151 | 151 | $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc |
|
152 | 152 | $ echo "[ui]" >> .hg/hgrc |
|
153 | 153 | $ echo "ssh = \"$PYTHON\" \"$TESTDIR/dummyssh\"" >> .hg/hgrc |
|
154 | 154 | |
|
155 | 155 | find outgoing |
|
156 | 156 | |
|
157 | 157 | $ hg out ssh://user@dummy/remote |
|
158 | 158 | comparing with ssh://user@dummy/remote |
|
159 | 159 | searching for changes |
|
160 | 160 | changeset: 3:a28a9d1a809c |
|
161 | 161 | tag: tip |
|
162 | 162 | parent: 0:1160648e36ce |
|
163 | 163 | user: test |
|
164 | 164 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
165 | 165 | summary: add |
|
166 | 166 | |
|
167 | 167 | |
|
168 | 168 | find incoming on the remote side |
|
169 | 169 | |
|
170 | 170 | $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/local |
|
171 | 171 | comparing with ssh://user@dummy/local |
|
172 | 172 | searching for changes |
|
173 | 173 | changeset: 3:a28a9d1a809c |
|
174 | 174 | tag: tip |
|
175 | 175 | parent: 0:1160648e36ce |
|
176 | 176 | user: test |
|
177 | 177 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
178 | 178 | summary: add |
|
179 | 179 | |
|
180 | 180 | |
|
181 | 181 | find incoming on the remote side (using absolute path) |
|
182 | 182 | |
|
183 | 183 | $ hg incoming -R ../remote -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`" |
|
184 | 184 | comparing with ssh://user@dummy/$TESTTMP/local |
|
185 | 185 | searching for changes |
|
186 | 186 | changeset: 3:a28a9d1a809c |
|
187 | 187 | tag: tip |
|
188 | 188 | parent: 0:1160648e36ce |
|
189 | 189 | user: test |
|
190 | 190 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
191 | 191 | summary: add |
|
192 | 192 | |
|
193 | 193 | |
|
194 | 194 | push |
|
195 | 195 | |
|
196 | 196 | $ hg push |
|
197 | 197 | pushing to ssh://user@dummy/remote |
|
198 | 198 | searching for changes |
|
199 | 199 | remote: adding changesets |
|
200 | 200 | remote: adding manifests |
|
201 | 201 | remote: adding file changes |
|
202 | 202 | remote: added 1 changesets with 1 changes to 1 files |
|
203 | 203 | $ cd $TESTTMP/remote |
|
204 | 204 | |
|
205 | 205 | check remote tip |
|
206 | 206 | |
|
207 | 207 | $ hg tip |
|
208 | 208 | changeset: 3:a28a9d1a809c |
|
209 | 209 | tag: tip |
|
210 | 210 | parent: 0:1160648e36ce |
|
211 | 211 | user: test |
|
212 | 212 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
213 | 213 | summary: add |
|
214 | 214 | |
|
215 | 215 | $ hg verify |
|
216 | 216 | checking changesets |
|
217 | 217 | checking manifests |
|
218 | 218 | crosschecking files in changesets and manifests |
|
219 | 219 | checking files |
|
220 | 220 | checked 4 changesets with 3 changes to 2 files |
|
221 | 221 | $ hg cat -r tip foo |
|
222 | 222 | bleah |
|
223 | 223 | $ echo z > z |
|
224 | 224 | $ hg ci -A -m z z |
|
225 | 225 | created new head |
|
226 | 226 | |
|
227 | 227 | test pushkeys and bookmarks |
|
228 | 228 | |
|
229 | 229 | $ cd $TESTTMP/local |
|
230 | 230 | $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces |
|
231 | 231 | bookmarks |
|
232 | 232 | namespaces |
|
233 | 233 | phases |
|
234 | 234 | $ hg book foo -r 0 |
|
235 | 235 | $ hg out -B --config paths.default=bogus://invalid --config paths.default:pushurl=`hg paths default` |
|
236 | 236 | comparing with ssh://user@dummy/remote |
|
237 | 237 | searching for changed bookmarks |
|
238 | 238 | foo 1160648e36ce |
|
239 | 239 | $ hg push -B foo |
|
240 | 240 | pushing to ssh://user@dummy/remote |
|
241 | 241 | searching for changes |
|
242 | 242 | no changes found |
|
243 | 243 | exporting bookmark foo |
|
244 | 244 | [1] |
|
245 | 245 | $ hg debugpushkey --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks |
|
246 | 246 | foo 1160648e36cec0054048a7edc4110c6f84fde594 |
|
247 | 247 | $ hg book -f foo |
|
248 | 248 | $ hg push --traceback |
|
249 | 249 | pushing to ssh://user@dummy/remote |
|
250 | 250 | searching for changes |
|
251 | 251 | no changes found |
|
252 | 252 | updating bookmark foo |
|
253 | 253 | [1] |
|
254 | 254 | $ hg book -d foo |
|
255 | 255 | $ hg in -B |
|
256 | 256 | comparing with ssh://user@dummy/remote |
|
257 | 257 | searching for changed bookmarks |
|
258 | 258 | foo a28a9d1a809c |
|
259 | 259 | $ hg book -f -r 0 foo |
|
260 | 260 | $ hg pull -B foo |
|
261 | 261 | pulling from ssh://user@dummy/remote |
|
262 | 262 | no changes found |
|
263 | 263 | updating bookmark foo |
|
264 | 264 | $ hg book -d foo |
|
265 | 265 | $ hg push -B foo |
|
266 | 266 | pushing to ssh://user@dummy/remote |
|
267 | 267 | searching for changes |
|
268 | 268 | no changes found |
|
269 | 269 | deleting remote bookmark foo |
|
270 | 270 | [1] |
|
271 | 271 | |
|
272 | 272 | a bad, evil hook that prints to stdout |
|
273 | 273 | |
|
274 | 274 | $ cat <<EOF > $TESTTMP/badhook |
|
275 | 275 | > import sys |
|
276 | 276 | > sys.stdout.write("KABOOM\n") |
|
277 | 277 | > sys.stdout.flush() |
|
278 | 278 | > EOF |
|
279 | 279 | |
|
280 | 280 | $ cat <<EOF > $TESTTMP/badpyhook.py |
|
281 | 281 | > import sys |
|
282 | 282 | > def hook(ui, repo, hooktype, **kwargs): |
|
283 | 283 | > sys.stdout.write("KABOOM IN PROCESS\n") |
|
284 | 284 | > sys.stdout.flush() |
|
285 | 285 | > EOF |
|
286 | 286 | |
|
287 | 287 | $ cat <<EOF >> ../remote/.hg/hgrc |
|
288 | 288 | > [hooks] |
|
289 | 289 | > changegroup.stdout = "$PYTHON" $TESTTMP/badhook |
|
290 | 290 | > changegroup.pystdout = python:$TESTTMP/badpyhook.py:hook |
|
291 | 291 | > EOF |
|
292 | 292 | $ echo r > r |
|
293 | 293 | $ hg ci -A -m z r |
|
294 | 294 | |
|
295 | 295 | push should succeed even though it has an unexpected response |
|
296 | 296 | |
|
297 | 297 | $ hg push |
|
298 | 298 | pushing to ssh://user@dummy/remote |
|
299 | 299 | searching for changes |
|
300 | 300 | remote has heads on branch 'default' that are not known locally: 6c0482d977a3 |
|
301 | 301 | remote: adding changesets |
|
302 | 302 | remote: adding manifests |
|
303 | 303 | remote: adding file changes |
|
304 | 304 | remote: added 1 changesets with 1 changes to 1 files (py3 !) |
|
305 | 305 | remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !) |
|
306 | 306 | remote: KABOOM |
|
307 | 307 | remote: KABOOM IN PROCESS |
|
308 | 308 | remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !) |
|
309 | 309 | $ hg -R ../remote heads |
|
310 | 310 | changeset: 5:1383141674ec |
|
311 | 311 | tag: tip |
|
312 | 312 | parent: 3:a28a9d1a809c |
|
313 | 313 | user: test |
|
314 | 314 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
315 | 315 | summary: z |
|
316 | 316 | |
|
317 | 317 | changeset: 4:6c0482d977a3 |
|
318 | 318 | parent: 0:1160648e36ce |
|
319 | 319 | user: test |
|
320 | 320 | date: Thu Jan 01 00:00:00 1970 +0000 |
|
321 | 321 | summary: z |
|
322 | 322 | |
|
323 | 323 | |
|
324 | 324 | #if chg |
|
325 | 325 | |
|
326 | 326 | try again with remote chg, which should succeed as well |
|
327 | 327 | |
|
328 | 328 | $ hg rollback -R ../remote |
|
329 | 329 | repository tip rolled back to revision 4 (undo serve) |
|
330 | 330 | |
|
331 | 331 | $ hg push --config ui.remotecmd=chg |
|
332 | 332 | pushing to ssh://user@dummy/remote |
|
333 | 333 | searching for changes |
|
334 | 334 | remote has heads on branch 'default' that are not known locally: 6c0482d977a3 |
|
335 | 335 | remote: adding changesets |
|
336 | 336 | remote: adding manifests |
|
337 | 337 | remote: adding file changes |
|
338 | 338 | remote: added 1 changesets with 1 changes to 1 files (py3 !) |
|
339 | 339 | remote: KABOOM |
|
340 | 340 | remote: KABOOM IN PROCESS |
|
341 | 341 | remote: added 1 changesets with 1 changes to 1 files (no-py3 !) |
|
342 | 342 | |
|
343 | 343 | #endif |
|
344 | 344 | |
|
345 | 345 | clone bookmarks |
|
346 | 346 | |
|
347 | 347 | $ hg -R ../remote bookmark test |
|
348 | 348 | $ hg -R ../remote bookmarks |
|
349 | 349 | * test 4:6c0482d977a3 |
|
350 | 350 | $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks |
|
351 | 351 | requesting all changes |
|
352 | 352 | adding changesets |
|
353 | 353 | adding manifests |
|
354 | 354 | adding file changes |
|
355 | 355 | added 6 changesets with 5 changes to 4 files (+1 heads) |
|
356 | 356 | new changesets 1160648e36ce:1383141674ec |
|
357 | 357 | updating to branch default |
|
358 | 358 | 3 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
359 | 359 | $ hg -R local-bookmarks bookmarks |
|
360 | 360 | test 4:6c0482d977a3 |
|
361 | 361 | |
|
362 | 362 | passwords in ssh urls are not supported |
|
363 | 363 | (we use a glob here because different Python versions give different |
|
364 | 364 | results here) |
|
365 | 365 | |
|
366 | 366 | $ hg push ssh://user:erroneouspwd@dummy/remote |
|
367 | 367 | pushing to ssh://user:*@dummy/remote (glob) |
|
368 | 368 | abort: password in URL not supported |
|
369 | 369 | [255] |
|
370 | 370 | |
|
371 | 371 | $ cd $TESTTMP |
|
372 | 372 | |
|
373 | 373 | hide outer repo |
|
374 | 374 | $ hg init |
|
375 | 375 | |
|
376 | 376 | Test remote paths with spaces (issue2983): |
|
377 | 377 | |
|
378 | 378 | $ hg init --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo" |
|
379 | 379 | $ touch "$TESTTMP/a repo/test" |
|
380 | 380 | $ hg -R 'a repo' commit -A -m "test" |
|
381 | 381 | adding test |
|
382 | 382 | $ hg -R 'a repo' tag tag |
|
383 | 383 | $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo" |
|
384 | 384 | 73649e48688a |
|
385 | 385 | |
|
386 | 386 | $ hg id --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO" |
|
387 | 387 | abort: unknown revision 'noNoNO' |
|
388 | 388 | [255] |
|
389 | 389 | |
|
390 | 390 | Test (non-)escaping of remote paths with spaces when cloning (issue3145): |
|
391 | 391 | |
|
392 | 392 | $ hg clone --ssh "\"$PYTHON\" \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo" |
|
393 | 393 | destination directory: a repo |
|
394 | 394 | abort: destination 'a repo' is not empty |
|
395 | 395 | [10] |
|
396 | 396 | |
|
397 | 397 | #if no-rhg |
|
398 | 398 | Make sure hg is really paranoid in serve --stdio mode. It used to be |
|
399 | 399 | possible to get a debugger REPL by specifying a repo named --debugger. |
|
400 | 400 | $ hg -R --debugger serve --stdio |
|
401 | 401 | abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio'] |
|
402 | 402 | [255] |
|
403 | 403 | $ hg -R --config=ui.debugger=yes serve --stdio |
|
404 | 404 | abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio'] |
|
405 | 405 | [255] |
|
406 | 406 | Abbreviations of 'serve' also don't work, to avoid shenanigans. |
|
407 | 407 | $ hg -R narf serv --stdio |
|
408 | 408 | abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio'] |
|
409 | 409 | [255] |
|
410 | 410 | #else |
|
411 | 411 | rhg aborts early on -R without a repository at that path |
|
412 | 412 | $ hg -R --debugger serve --stdio |
|
413 | 413 | abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio'] (missing-correct-output !) |
|
414 | 414 | abort: repository --debugger not found (known-bad-output !) |
|
415 | 415 | [255] |
|
416 | 416 | $ hg -R --config=ui.debugger=yes serve --stdio |
|
417 | 417 | abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio'] (missing-correct-output !) |
|
418 | 418 | abort: repository --config=ui.debugger=yes not found (known-bad-output !) |
|
419 | 419 | [255] |
|
420 | 420 | $ hg -R narf serv --stdio |
|
421 | 421 | abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio'] (missing-correct-output !) |
|
422 | 422 | abort: repository narf not found (known-bad-output !) |
|
423 | 423 | [255] |
|
424 | 424 | If the repo does exist, rhg finds an unsupported command and falls back to Python |
|
425 | 425 | which still does the right thing |
|
426 | 426 | $ hg init narf |
|
427 | 427 | $ hg -R narf serv --stdio |
|
428 | 428 | abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio'] |
|
429 | 429 | [255] |
|
430 | 430 | #endif |
|
431 | 431 | |
|
432 | 432 | Test hg-ssh using a helper script that will restore PYTHONPATH (which might |
|
433 | 433 | have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right |
|
434 | 434 | parameters: |
|
435 | 435 | |
|
436 | 436 | $ cat > ssh.sh << EOF |
|
437 | 437 | > userhost="\$1" |
|
438 | 438 | > SSH_ORIGINAL_COMMAND="\$2" |
|
439 | 439 | > export SSH_ORIGINAL_COMMAND |
|
440 | 440 | > PYTHONPATH="$PYTHONPATH" |
|
441 | 441 | > export PYTHONPATH |
|
442 | 442 | > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo" |
|
443 | 443 | > EOF |
|
444 | 444 | |
|
445 | 445 | $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo" |
|
446 | 446 | 73649e48688a |
|
447 | 447 | |
|
448 | 448 | $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo" |
|
449 | 449 | remote: Illegal repository "$TESTTMP/a'repo" |
|
450 | 450 | abort: no suitable response from remote hg |
|
451 | 451 | [255] |
|
452 | 452 | |
|
453 | 453 | $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo" |
|
454 | 454 | remote: Illegal command "hacking -R 'a'\''repo' serve --stdio" |
|
455 | 455 | abort: no suitable response from remote hg |
|
456 | 456 | [255] |
|
457 | 457 | |
|
458 | 458 | $ SSH_ORIGINAL_COMMAND="'hg' -R 'a'repo' serve --stdio" "$PYTHON" "$TESTDIR/../contrib/hg-ssh" |
|
459 | 459 | Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation |
|
460 | 460 | [255] |
|
461 | 461 | |
|
462 | 462 | Test hg-ssh in read-only mode: |
|
463 | 463 | |
|
464 | 464 | $ cat > ssh.sh << EOF |
|
465 | 465 | > userhost="\$1" |
|
466 | 466 | > SSH_ORIGINAL_COMMAND="\$2" |
|
467 | 467 | > export SSH_ORIGINAL_COMMAND |
|
468 | 468 | > PYTHONPATH="$PYTHONPATH" |
|
469 | 469 | > export PYTHONPATH |
|
470 | 470 | > "$PYTHON" "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote" |
|
471 | 471 | > EOF |
|
472 | 472 | |
|
473 | 473 | $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local |
|
474 | 474 | requesting all changes |
|
475 | 475 | adding changesets |
|
476 | 476 | adding manifests |
|
477 | 477 | adding file changes |
|
478 | 478 | added 6 changesets with 5 changes to 4 files (+1 heads) |
|
479 | 479 | new changesets 1160648e36ce:1383141674ec |
|
480 | 480 | updating to branch default |
|
481 | 481 | 3 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
482 | 482 | |
|
483 | 483 | $ cd read-only-local |
|
484 | 484 | $ echo "baz" > bar |
|
485 | 485 | $ hg ci -A -m "unpushable commit" bar |
|
486 | 486 | $ hg push --ssh "sh ../ssh.sh" |
|
487 | 487 | pushing to ssh://user@dummy/*/remote (glob) |
|
488 | 488 | searching for changes |
|
489 | 489 | remote: Permission denied |
|
490 | 490 | remote: pretxnopen.hg-ssh hook failed |
|
491 | 491 | abort: push failed on remote |
|
492 | 492 | [100] |
|
493 | 493 | |
|
494 | 494 | $ cd $TESTTMP |
|
495 | 495 | |
|
496 | 496 | stderr from remote commands should be printed before stdout from local code (issue4336) |
|
497 | 497 | |
|
498 | 498 | $ hg clone remote stderr-ordering |
|
499 | 499 | updating to branch default |
|
500 | 500 | 3 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
501 | 501 | $ cd stderr-ordering |
|
502 | 502 | $ cat >> localwrite.py << EOF |
|
503 | 503 | > from mercurial import exchange, extensions |
|
504 | 504 | > |
|
505 | 505 | > def wrappedpush(orig, repo, *args, **kwargs): |
|
506 | 506 | > res = orig(repo, *args, **kwargs) |
|
507 | 507 | > repo.ui.write(b'local stdout\n') |
|
508 | 508 | > repo.ui.flush() |
|
509 | 509 | > return res |
|
510 | 510 | > |
|
511 | 511 | > def extsetup(ui): |
|
512 | 512 | > extensions.wrapfunction(exchange, b'push', wrappedpush) |
|
513 | 513 | > EOF |
|
514 | 514 | |
|
515 | 515 | $ cat >> .hg/hgrc << EOF |
|
516 | 516 | > [paths] |
|
517 | 517 | > default-push = ssh://user@dummy/remote |
|
518 | 518 | > [ui] |
|
519 | 519 | > ssh = "$PYTHON" "$TESTDIR/dummyssh" |
|
520 | 520 | > [extensions] |
|
521 | 521 | > localwrite = localwrite.py |
|
522 | 522 | > EOF |
|
523 | 523 | |
|
524 | 524 | $ echo localwrite > foo |
|
525 | 525 | $ hg commit -m 'testing localwrite' |
|
526 | 526 | $ hg push |
|
527 | 527 | pushing to ssh://user@dummy/remote |
|
528 | 528 | searching for changes |
|
529 | 529 | remote: adding changesets |
|
530 | 530 | remote: adding manifests |
|
531 | 531 | remote: adding file changes |
|
532 | 532 | remote: added 1 changesets with 1 changes to 1 files (py3 !) |
|
533 | 533 | remote: added 1 changesets with 1 changes to 1 files (no-py3 no-chg !) |
|
534 | 534 | remote: KABOOM |
|
535 | 535 | remote: KABOOM IN PROCESS |
|
536 | 536 | remote: added 1 changesets with 1 changes to 1 files (no-py3 chg !) |
|
537 | 537 | local stdout |
|
538 | 538 | |
|
539 | 539 | debug output |
|
540 | 540 | |
|
541 | 541 | $ hg pull --debug ssh://user@dummy/remote --config devel.debug.peer-request=yes |
|
542 | 542 | pulling from ssh://user@dummy/remote |
|
543 |
running .* ".*/dummyssh" ['"]user@dummy['"] |
|
|
543 | running .* ".*[/\\]dummyssh" ['"]user@dummy['"] ['"]hg -R remote serve --stdio['"] (re) | |
|
544 | 544 | sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !) |
|
545 | 545 | devel-peer-request: hello+between |
|
546 | 546 | devel-peer-request: pairs: 81 bytes |
|
547 | 547 | sending hello command |
|
548 | 548 | sending between command |
|
549 | 549 | remote: 444 (sshv1 no-rust !) |
|
550 | 550 | remote: 463 (sshv1 rust !) |
|
551 | 551 | protocol upgraded to exp-ssh-v2-0003 (sshv2 !) |
|
552 | 552 | remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (no-rust !) |
|
553 | 553 | remote: capabilities: batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset getbundle known lookup protocaps pushkey streamreqs=generaldelta,persistent-nodemap,revlogv1,sparserevlog unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash (rust !) |
|
554 | 554 | remote: 1 (sshv1 !) |
|
555 | 555 | devel-peer-request: protocaps |
|
556 | 556 | devel-peer-request: caps: * bytes (glob) |
|
557 | 557 | sending protocaps command |
|
558 | 558 | query 1; heads |
|
559 | 559 | devel-peer-request: batched-content |
|
560 | 560 | devel-peer-request: - heads (0 arguments) |
|
561 | 561 | devel-peer-request: - known (1 arguments) |
|
562 | 562 | devel-peer-request: batch |
|
563 | 563 | devel-peer-request: cmds: 141 bytes |
|
564 | 564 | sending batch command |
|
565 | 565 | searching for changes |
|
566 | 566 | all remote heads known locally |
|
567 | 567 | no changes found |
|
568 | 568 | devel-peer-request: getbundle |
|
569 | 569 | devel-peer-request: bookmarks: 1 bytes |
|
570 | 570 | devel-peer-request: bundlecaps: 270 bytes |
|
571 | 571 | devel-peer-request: cg: 1 bytes |
|
572 | 572 | devel-peer-request: common: 122 bytes |
|
573 | 573 | devel-peer-request: heads: 122 bytes |
|
574 | 574 | devel-peer-request: listkeys: 9 bytes |
|
575 | 575 | devel-peer-request: phases: 1 bytes |
|
576 | 576 | sending getbundle command |
|
577 | 577 | bundle2-input-bundle: with-transaction |
|
578 | 578 | bundle2-input-part: "bookmarks" supported |
|
579 | 579 | bundle2-input-part: total payload size 26 |
|
580 | 580 | bundle2-input-part: "listkeys" (params: 1 mandatory) supported |
|
581 | 581 | bundle2-input-part: total payload size 45 |
|
582 | 582 | bundle2-input-part: "phase-heads" supported |
|
583 | 583 | bundle2-input-part: total payload size 72 |
|
584 | 584 | bundle2-input-bundle: 3 parts total |
|
585 | 585 | checking for updated bookmarks |
|
586 | 586 | |
|
587 | 587 | $ cd $TESTTMP |
|
588 | 588 | |
|
589 | 589 | $ cat dummylog |
|
590 | 590 | Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio |
|
591 | 591 | Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio |
|
592 | 592 | Got arguments 1:user@dummy 2:hg -R $TESTTMP/nonexistent serve --stdio |
|
593 | 593 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
594 | 594 | Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio (no-reposimplestore !) |
|
595 | 595 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !) |
|
596 | 596 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio (no-reposimplestore !) |
|
597 | 597 | Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio |
|
598 | 598 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
599 | 599 | Got arguments 1:user@dummy 2:hg -R local serve --stdio |
|
600 | 600 | Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio |
|
601 | 601 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
602 | 602 | changegroup-in-remote hook: HG_BUNDLE2=1 |
|
603 | 603 | HG_HOOKNAME=changegroup |
|
604 | 604 | HG_HOOKTYPE=changegroup |
|
605 | 605 | HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 |
|
606 | 606 | HG_NODE_LAST=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 |
|
607 | 607 | HG_SOURCE=serve |
|
608 | 608 | HG_TXNID=TXN:$ID$ |
|
609 | 609 | HG_TXNNAME=serve |
|
610 | 610 | HG_URL=remote:ssh:$LOCALIP |
|
611 | 611 | |
|
612 | 612 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
613 | 613 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
614 | 614 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
615 | 615 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
616 | 616 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
617 | 617 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
618 | 618 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
619 | 619 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
620 | 620 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
621 | 621 | changegroup-in-remote hook: HG_BUNDLE2=1 |
|
622 | 622 | HG_HOOKNAME=changegroup |
|
623 | 623 | HG_HOOKTYPE=changegroup |
|
624 | 624 | HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 |
|
625 | 625 | HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 |
|
626 | 626 | HG_SOURCE=serve |
|
627 | 627 | HG_TXNID=TXN:$ID$ |
|
628 | 628 | HG_TXNNAME=serve |
|
629 | 629 | HG_URL=remote:ssh:$LOCALIP |
|
630 | 630 | |
|
631 | 631 | Got arguments 1:user@dummy 2:chg -R remote serve --stdio (chg !) |
|
632 | 632 | changegroup-in-remote hook: HG_BUNDLE2=1 (chg !) |
|
633 | 633 | HG_HOOKNAME=changegroup (chg !) |
|
634 | 634 | HG_HOOKTYPE=changegroup (chg !) |
|
635 | 635 | HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 (chg !) |
|
636 | 636 | HG_NODE_LAST=1383141674ec756a6056f6a9097618482fe0f4a6 (chg !) |
|
637 | 637 | HG_SOURCE=serve (chg !) |
|
638 | 638 | HG_TXNID=TXN:$ID$ (chg !) |
|
639 | 639 | HG_TXNNAME=serve (chg !) |
|
640 | 640 | HG_URL=remote:ssh:$LOCALIP (chg !) |
|
641 | 641 | (chg !) |
|
642 | 642 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
643 | 643 | Got arguments 1:user@dummy 2:hg init 'a repo' |
|
644 | 644 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
645 | 645 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
646 | 646 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
647 | 647 | Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio |
|
648 | 648 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
649 | 649 | changegroup-in-remote hook: HG_BUNDLE2=1 |
|
650 | 650 | HG_HOOKNAME=changegroup |
|
651 | 651 | HG_HOOKTYPE=changegroup |
|
652 | 652 | HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8 |
|
653 | 653 | HG_NODE_LAST=65c38f4125f9602c8db4af56530cc221d93b8ef8 |
|
654 | 654 | HG_SOURCE=serve |
|
655 | 655 | HG_TXNID=TXN:$ID$ |
|
656 | 656 | HG_TXNNAME=serve |
|
657 | 657 | HG_URL=remote:ssh:$LOCALIP |
|
658 | 658 | |
|
659 | 659 | Got arguments 1:user@dummy 2:hg -R remote serve --stdio |
|
660 | 660 | |
|
661 | 661 | |
|
662 | 662 | remote hook failure is attributed to remote |
|
663 | 663 | |
|
664 | 664 | $ cat > $TESTTMP/failhook << EOF |
|
665 | 665 | > def hook(ui, repo, **kwargs): |
|
666 | 666 | > ui.write(b'hook failure!\n') |
|
667 | 667 | > ui.flush() |
|
668 | 668 | > return 1 |
|
669 | 669 | > EOF |
|
670 | 670 | |
|
671 | 671 | $ echo "pretxnchangegroup.fail = python:$TESTTMP/failhook:hook" >> remote/.hg/hgrc |
|
672 | 672 | |
|
673 | 673 | $ hg -q --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" clone ssh://user@dummy/remote hookout |
|
674 | 674 | $ cd hookout |
|
675 | 675 | $ touch hookfailure |
|
676 | 676 | $ hg -q commit -A -m 'remote hook failure' |
|
677 | 677 | $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" push |
|
678 | 678 | pushing to ssh://user@dummy/remote |
|
679 | 679 | searching for changes |
|
680 | 680 | remote: adding changesets |
|
681 | 681 | remote: adding manifests |
|
682 | 682 | remote: adding file changes |
|
683 | 683 | remote: hook failure! |
|
684 | 684 | remote: transaction abort! |
|
685 | 685 | remote: rollback completed |
|
686 | 686 | remote: pretxnchangegroup.fail hook failed |
|
687 | 687 | abort: push failed on remote |
|
688 | 688 | [100] |
|
689 | 689 | |
|
690 | 690 | abort during pull is properly reported as such |
|
691 | 691 | |
|
692 | 692 | $ echo morefoo >> ../remote/foo |
|
693 | 693 | $ hg -R ../remote commit --message "more foo to be pulled" |
|
694 | 694 | $ cat >> ../remote/.hg/hgrc << EOF |
|
695 | 695 | > [extensions] |
|
696 | 696 | > crash = ${TESTDIR}/crashgetbundler.py |
|
697 | 697 | > EOF |
|
698 | 698 | $ hg --config ui.ssh="\"$PYTHON\" $TESTDIR/dummyssh" pull |
|
699 | 699 | pulling from ssh://user@dummy/remote |
|
700 | 700 | searching for changes |
|
701 | 701 | remote: abort: this is an exercise |
|
702 | 702 | abort: pull failed on remote |
|
703 | 703 | [100] |
|
704 | 704 | |
|
705 | 705 | abort with no error hint when there is a ssh problem when pulling |
|
706 | 706 | |
|
707 | 707 | $ hg pull ssh://brokenrepository -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" |
|
708 | 708 | pulling from ssh://brokenrepository/ |
|
709 | 709 | abort: no suitable response from remote hg |
|
710 | 710 | [255] |
|
711 | 711 | |
|
712 | 712 | abort with configured error hint when there is a ssh problem when pulling |
|
713 | 713 | |
|
714 | 714 | $ hg pull ssh://brokenrepository -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" \ |
|
715 | 715 | > --config ui.ssherrorhint="Please see http://company/internalwiki/ssh.html" |
|
716 | 716 | pulling from ssh://brokenrepository/ |
|
717 | 717 | abort: no suitable response from remote hg |
|
718 | 718 | (Please see http://company/internalwiki/ssh.html) |
|
719 | 719 | [255] |
|
720 | 720 | |
|
721 | 721 | test that custom environment is passed down to ssh executable |
|
722 | 722 | $ cat >>dumpenv <<EOF |
|
723 | 723 | > #! /bin/sh |
|
724 | 724 | > echo \$VAR >&2 |
|
725 | 725 | > EOF |
|
726 | 726 | $ chmod +x dumpenv |
|
727 | 727 | $ hg pull ssh://something --config ui.ssh="sh dumpenv" |
|
728 | 728 | pulling from ssh://something/ |
|
729 | 729 | remote: |
|
730 | 730 | abort: no suitable response from remote hg |
|
731 | 731 | [255] |
|
732 | 732 | $ hg pull ssh://something --config ui.ssh="sh dumpenv" --config sshenv.VAR=17 |
|
733 | 733 | pulling from ssh://something/ |
|
734 | 734 | remote: 17 |
|
735 | 735 | abort: no suitable response from remote hg |
|
736 | 736 | [255] |
|
737 | 737 |
General Comments 0
You need to be logged in to leave comments.
Login now