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