##// END OF EJS Templates
tests: avoid test-hup hanging on AIX...
Jim Hague -
r16364:f64b25f1 stable
parent child Browse files
Show More
@@ -1,430 +1,432
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 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 import re, glob, os, sys
11 11 import keyword
12 12 import optparse
13 13
14 14 def repquote(m):
15 15 t = re.sub(r"\w", "x", m.group('text'))
16 16 t = re.sub(r"[^\s\nx]", "o", t)
17 17 return m.group('quote') + t + m.group('quote')
18 18
19 19 def reppython(m):
20 20 comment = m.group('comment')
21 21 if comment:
22 22 return "#" * len(comment)
23 23 return repquote(m)
24 24
25 25 def repcomment(m):
26 26 return m.group(1) + "#" * len(m.group(2))
27 27
28 28 def repccomment(m):
29 29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
30 30 return m.group(1) + t + "*/"
31 31
32 32 def repcallspaces(m):
33 33 t = re.sub(r"\n\s+", "\n", m.group(2))
34 34 return m.group(1) + t
35 35
36 36 def repinclude(m):
37 37 return m.group(1) + "<foo>"
38 38
39 39 def rephere(m):
40 40 t = re.sub(r"\S", "x", m.group(2))
41 41 return m.group(1) + t
42 42
43 43
44 44 testpats = [
45 45 [
46 46 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
47 47 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
48 48 (r'^function', "don't use 'function', use old style"),
49 49 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
50 50 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
51 51 (r'echo -n', "don't use 'echo -n', use printf"),
52 52 (r'^diff.*-\w*N', "don't use 'diff -N'"),
53 53 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
54 54 (r'head -c', "don't use 'head -c', use 'dd'"),
55 55 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
56 56 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
57 57 (r'printf.*\\\d{1,3}', "don't use 'printf \NNN', use Python"),
58 58 (r'printf.*\\x', "don't use printf \\x, use Python"),
59 59 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
60 60 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
61 61 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
62 62 "use egrep for extended grep syntax"),
63 63 (r'/bin/', "don't use explicit paths for tools"),
64 64 (r'\$PWD', "don't use $PWD, use `pwd`"),
65 65 (r'[^\n]\Z', "no trailing newline"),
66 66 (r'export.*=', "don't export and assign at once"),
67 67 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\\^', "^ must be quoted"),
68 68 (r'^source\b', "don't use 'source', use '.'"),
69 69 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
70 70 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
71 71 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
72 72 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
73 73 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
74 74 (r'^alias\b.*=', "don't use alias, use a function"),
75 75 ],
76 76 # warnings
77 77 []
78 78 ]
79 79
80 80 testfilters = [
81 81 (r"( *)(#([^\n]*\S)?)", repcomment),
82 82 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
83 83 ]
84 84
85 85 uprefix = r"^ \$ "
86 86 uprefixc = r"^ > "
87 87 utestpats = [
88 88 [
89 89 (r'^(\S| $ ).*(\S[ \t]+|^[ \t]+)\n', "trailing whitespace on non-output"),
90 90 (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"),
91 91 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
92 92 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
93 93 (uprefix + r'.*\|\| echo.*(fail|error)',
94 94 "explicit exit code checks unnecessary"),
95 95 (uprefix + r'set -e', "don't use set -e"),
96 96 (uprefixc + r'( *)\t', "don't use tabs to indent"),
97 (uprefixc + r'.*do\s*true;\s*done',
98 "don't use true as loop body, use sleep 0"),
97 99 ],
98 100 # warnings
99 101 []
100 102 ]
101 103
102 104 for i in [0, 1]:
103 105 for p, m in testpats[i]:
104 106 if p.startswith(r'^'):
105 107 p = uprefix + p[1:]
106 108 else:
107 109 p = uprefix + ".*" + p
108 110 utestpats[i].append((p, m))
109 111
110 112 utestfilters = [
111 113 (r"( *)(#([^\n]*\S)?)", repcomment),
112 114 ]
113 115
114 116 pypats = [
115 117 [
116 118 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
117 119 "tuple parameter unpacking not available in Python 3+"),
118 120 (r'lambda\s*\(.*,.*\)',
119 121 "tuple parameter unpacking not available in Python 3+"),
120 122 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
121 123 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
122 124 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
123 125 (r'^\s*\t', "don't use tabs"),
124 126 (r'\S;\s*\n', "semicolon"),
125 127 (r'\w,\w', "missing whitespace after ,"),
126 128 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
127 129 (r'^\s+\w+=\w+[^,)\n]$', "missing whitespace in assignment"),
128 130 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
129 131 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Py2.4'),
130 132 (r'.{85}', "line too long"),
131 133 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
132 134 (r'[^\n]\Z', "no trailing newline"),
133 135 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
134 136 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=', "don't use underbars in identifiers"),
135 137 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
136 138 "don't use camelcase in identifiers"),
137 139 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
138 140 "linebreak after :"),
139 141 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
140 142 (r'class\s[^( \n]+\(\):',
141 143 "class foo() not available in Python 2.4, use class foo(object)"),
142 144 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
143 145 "Python keyword is not a function"),
144 146 (r',]', "unneeded trailing ',' in list"),
145 147 # (r'class\s[A-Z][^\(]*\((?!Exception)',
146 148 # "don't capitalize non-exception classes"),
147 149 # (r'in range\(', "use xrange"),
148 150 # (r'^\s*print\s+', "avoid using print in core and extensions"),
149 151 (r'[\x80-\xff]', "non-ASCII character literal"),
150 152 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
151 153 (r'^\s*with\s+', "with not available in Python 2.4"),
152 154 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
153 155 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
154 156 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
155 157 (r'(?<!def)\s+(any|all|format)\(',
156 158 "any/all/format not available in Python 2.4"),
157 159 (r'(?<!def)\s+(callable)\(',
158 160 "callable not available in Python 3, use getattr(f, '__call__', None)"),
159 161 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
160 162 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
161 163 "gratuitous whitespace after Python keyword"),
162 164 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
163 165 # (r'\s\s=', "gratuitous whitespace before ="),
164 166 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
165 167 "missing whitespace around operator"),
166 168 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
167 169 "missing whitespace around operator"),
168 170 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
169 171 "missing whitespace around operator"),
170 172 (r'[^+=*/!<>&| -](\s=|=\s)[^= ]',
171 173 "wrong whitespace around ="),
172 174 (r'raise Exception', "don't raise generic exceptions"),
173 175 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
174 176 (r' [=!]=\s+(True|False|None)',
175 177 "comparison with singleton, use 'is' or 'is not' instead"),
176 178 (r'^\s*(while|if) [01]:',
177 179 "use True/False for constant Boolean expression"),
178 180 (r'(?<!def)\s+hasattr',
179 181 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
180 182 (r'opener\([^)]*\).read\(',
181 183 "use opener.read() instead"),
182 184 (r'BaseException', 'not in Py2.4, use Exception'),
183 185 (r'os\.path\.relpath', 'os.path.relpath is not in Py2.5'),
184 186 (r'opener\([^)]*\).write\(',
185 187 "use opener.write() instead"),
186 188 (r'[\s\(](open|file)\([^)]*\)\.read\(',
187 189 "use util.readfile() instead"),
188 190 (r'[\s\(](open|file)\([^)]*\)\.write\(',
189 191 "use util.readfile() instead"),
190 192 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
191 193 "always assign an opened file to a variable, and close it afterwards"),
192 194 (r'[\s\(](open|file)\([^)]*\)\.',
193 195 "always assign an opened file to a variable, and close it afterwards"),
194 196 (r'(?i)descendent', "the proper spelling is descendAnt"),
195 197 (r'\.debug\(\_', "don't mark debug messages for translation"),
196 198 ],
197 199 # warnings
198 200 [
199 201 (r'.{81}', "warning: line over 80 characters"),
200 202 (r'^\s*except:$', "warning: naked except clause"),
201 203 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
202 204 "warning: unwrapped ui message"),
203 205 ]
204 206 ]
205 207
206 208 pyfilters = [
207 209 (r"""(?msx)(?P<comment>\#.*?$)|
208 210 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
209 211 (?P<text>(([^\\]|\\.)*?))
210 212 (?P=quote))""", reppython),
211 213 ]
212 214
213 215 cpats = [
214 216 [
215 217 (r'//', "don't use //-style comments"),
216 218 (r'^ ', "don't use spaces to indent"),
217 219 (r'\S\t', "don't use tabs except for indent"),
218 220 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
219 221 (r'.{85}', "line too long"),
220 222 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
221 223 (r'return\(', "return is not a function"),
222 224 (r' ;', "no space before ;"),
223 225 (r'\w+\* \w+', "use int *foo, not int* foo"),
224 226 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
225 227 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
226 228 (r'\w,\w', "missing whitespace after ,"),
227 229 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
228 230 (r'^#\s+\w', "use #foo, not # foo"),
229 231 (r'[^\n]\Z', "no trailing newline"),
230 232 (r'^\s*#import\b', "use only #include in standard C code"),
231 233 ],
232 234 # warnings
233 235 []
234 236 ]
235 237
236 238 cfilters = [
237 239 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
238 240 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
239 241 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
240 242 (r'(\()([^)]+\))', repcallspaces),
241 243 ]
242 244
243 245 inutilpats = [
244 246 [
245 247 (r'\bui\.', "don't use ui in util"),
246 248 ],
247 249 # warnings
248 250 []
249 251 ]
250 252
251 253 inrevlogpats = [
252 254 [
253 255 (r'\brepo\.', "don't use repo in revlog"),
254 256 ],
255 257 # warnings
256 258 []
257 259 ]
258 260
259 261 checks = [
260 262 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
261 263 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
262 264 ('c', r'.*\.c$', cfilters, cpats),
263 265 ('unified test', r'.*\.t$', utestfilters, utestpats),
264 266 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
265 267 inrevlogpats),
266 268 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
267 269 inutilpats),
268 270 ]
269 271
270 272 class norepeatlogger(object):
271 273 def __init__(self):
272 274 self._lastseen = None
273 275
274 276 def log(self, fname, lineno, line, msg, blame):
275 277 """print error related a to given line of a given file.
276 278
277 279 The faulty line will also be printed but only once in the case
278 280 of multiple errors.
279 281
280 282 :fname: filename
281 283 :lineno: line number
282 284 :line: actual content of the line
283 285 :msg: error message
284 286 """
285 287 msgid = fname, lineno, line
286 288 if msgid != self._lastseen:
287 289 if blame:
288 290 print "%s:%d (%s):" % (fname, lineno, blame)
289 291 else:
290 292 print "%s:%d:" % (fname, lineno)
291 293 print " > %s" % line
292 294 self._lastseen = msgid
293 295 print " " + msg
294 296
295 297 _defaultlogger = norepeatlogger()
296 298
297 299 def getblame(f):
298 300 lines = []
299 301 for l in os.popen('hg annotate -un %s' % f):
300 302 start, line = l.split(':', 1)
301 303 user, rev = start.split()
302 304 lines.append((line[1:-1], user, rev))
303 305 return lines
304 306
305 307 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
306 308 blame=False, debug=False, lineno=True):
307 309 """checks style and portability of a given file
308 310
309 311 :f: filepath
310 312 :logfunc: function used to report error
311 313 logfunc(filename, linenumber, linecontent, errormessage)
312 314 :maxerr: number of error to display before arborting.
313 315 Set to false (default) to report all errors
314 316
315 317 return True if no error is found, False otherwise.
316 318 """
317 319 blamecache = None
318 320 result = True
319 321 for name, match, filters, pats in checks:
320 322 if debug:
321 323 print name, f
322 324 fc = 0
323 325 if not re.match(match, f):
324 326 if debug:
325 327 print "Skipping %s for %s it doesn't match %s" % (
326 328 name, match, f)
327 329 continue
328 330 fp = open(f)
329 331 pre = post = fp.read()
330 332 fp.close()
331 333 if "no-" + "check-code" in pre:
332 334 if debug:
333 335 print "Skipping %s for %s it has no- and check-code" % (
334 336 name, f)
335 337 break
336 338 for p, r in filters:
337 339 post = re.sub(p, r, post)
338 340 if warnings:
339 341 pats = pats[0] + pats[1]
340 342 else:
341 343 pats = pats[0]
342 344 # print post # uncomment to show filtered version
343 345
344 346 if debug:
345 347 print "Checking %s for %s" % (name, f)
346 348
347 349 prelines = None
348 350 errors = []
349 351 for p, msg in pats:
350 352 # fix-up regexes for multiline searches
351 353 po = p
352 354 # \s doesn't match \n
353 355 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
354 356 # [^...] doesn't match newline
355 357 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
356 358
357 359 #print po, '=>', p
358 360
359 361 pos = 0
360 362 n = 0
361 363 for m in re.finditer(p, post, re.MULTILINE):
362 364 if prelines is None:
363 365 prelines = pre.splitlines()
364 366 postlines = post.splitlines(True)
365 367
366 368 start = m.start()
367 369 while n < len(postlines):
368 370 step = len(postlines[n])
369 371 if pos + step > start:
370 372 break
371 373 pos += step
372 374 n += 1
373 375 l = prelines[n]
374 376
375 377 if "check-code" + "-ignore" in l:
376 378 if debug:
377 379 print "Skipping %s for %s:%s (check-code -ignore)" % (
378 380 name, f, n)
379 381 continue
380 382 bd = ""
381 383 if blame:
382 384 bd = 'working directory'
383 385 if not blamecache:
384 386 blamecache = getblame(f)
385 387 if n < len(blamecache):
386 388 bl, bu, br = blamecache[n]
387 389 if bl == l:
388 390 bd = '%s@%s' % (bu, br)
389 391 errors.append((f, lineno and n + 1, l, msg, bd))
390 392 result = False
391 393
392 394 errors.sort()
393 395 for e in errors:
394 396 logfunc(*e)
395 397 fc += 1
396 398 if maxerr and fc >= maxerr:
397 399 print " (too many errors, giving up)"
398 400 break
399 401
400 402 return result
401 403
402 404 if __name__ == "__main__":
403 405 parser = optparse.OptionParser("%prog [options] [files]")
404 406 parser.add_option("-w", "--warnings", action="store_true",
405 407 help="include warning-level checks")
406 408 parser.add_option("-p", "--per-file", type="int",
407 409 help="max warnings per file")
408 410 parser.add_option("-b", "--blame", action="store_true",
409 411 help="use annotate to generate blame info")
410 412 parser.add_option("", "--debug", action="store_true",
411 413 help="show debug information")
412 414 parser.add_option("", "--nolineno", action="store_false",
413 415 dest='lineno', help="don't show line numbers")
414 416
415 417 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
416 418 lineno=True)
417 419 (options, args) = parser.parse_args()
418 420
419 421 if len(args) == 0:
420 422 check = glob.glob("*")
421 423 else:
422 424 check = args
423 425
424 426 ret = 0
425 427 for f in check:
426 428 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
427 429 blame=options.blame, debug=options.debug,
428 430 lineno=options.lineno):
429 431 ret = 1
430 432 sys.exit(ret)
@@ -1,28 +1,28
1 1 Test hangup signal in the middle of transaction
2 2
3 3 $ "$TESTDIR/hghave" serve fifo || exit 80
4 4 $ hg init
5 5 $ mkfifo p
6 6 $ hg serve --stdio < p 1>out 2>&1 &
7 7 $ P=$!
8 8
9 9 Do test while holding fifo open
10 10
11 11 $ (
12 12 > echo lock
13 13 > echo addchangegroup
14 > while [ ! -s .hg/store/journal ]; do true; done
14 > while [ ! -s .hg/store/journal ]; do sleep 0; done
15 15 > kill -HUP $P
16 16 > ) > p
17 17
18 18 $ wait
19 19 $ cat out
20 20 0
21 21 0
22 22 adding changesets
23 23 transaction abort!
24 24 rollback completed
25 25 killed!
26 26
27 27 $ echo .hg/* .hg/store/*
28 28 .hg/00changelog.i .hg/journal.bookmarks .hg/journal.branch .hg/journal.desc .hg/journal.dirstate .hg/requires .hg/store .hg/store/00changelog.i .hg/store/00changelog.i.a .hg/store/journal.phaseroots
@@ -1,82 +1,82
1 1 $ "$TESTDIR/hghave" serve || exit 80
2 2
3 3 $ hgserve()
4 4 > {
5 5 > hg serve -a localhost -d --pid-file=hg.pid -E errors.log -v $@ \
6 6 > | sed -e "s/:$HGPORT1\\([^0-9]\\)/:HGPORT1\1/g" \
7 7 > -e "s/:$HGPORT2\\([^0-9]\\)/:HGPORT2\1/g" \
8 8 > -e 's/http:\/\/[^/]*\//http:\/\/localhost\//'
9 9 > cat hg.pid >> "$DAEMON_PIDS"
10 10 > echo % errors
11 11 > cat errors.log
12 12 > if [ "$KILLQUIETLY" = "Y" ]; then
13 13 > kill `cat hg.pid` 2>/dev/null
14 14 > else
15 15 > kill `cat hg.pid`
16 16 > fi
17 > while kill -0 `cat hg.pid` 2>/dev/null; do true; done
17 > while kill -0 `cat hg.pid` 2>/dev/null; do sleep 0; done
18 18 > }
19 19
20 20 $ hg init test
21 21 $ cd test
22 22 $ echo '[web]' > .hg/hgrc
23 23 $ echo 'accesslog = access.log' >> .hg/hgrc
24 24 $ echo "port = $HGPORT1" >> .hg/hgrc
25 25
26 26 Without -v
27 27
28 28 $ hg serve -a localhost -p $HGPORT -d --pid-file=hg.pid -E errors.log
29 29 $ cat hg.pid >> "$DAEMON_PIDS"
30 30 $ if [ -f access.log ]; then
31 31 $ echo 'access log created - .hg/hgrc respected'
32 32 access log created - .hg/hgrc respected
33 33 $ fi
34 34
35 35 errors
36 36
37 37 $ cat errors.log
38 38
39 39 With -v
40 40
41 41 $ hgserve
42 42 listening at http://localhost/ (bound to 127.0.0.1:HGPORT1)
43 43 % errors
44 44
45 45 With -v and -p HGPORT2
46 46
47 47 $ hgserve -p "$HGPORT2"
48 48 listening at http://localhost/ (bound to 127.0.0.1:HGPORT2)
49 49 % errors
50 50
51 51 With -v and -p daytime (should fail because low port)
52 52
53 53 $ KILLQUIETLY=Y
54 54 $ hgserve -p daytime
55 55 abort: cannot start server at 'localhost:13': Permission denied
56 56 abort: child process failed to start
57 57 % errors
58 58 $ KILLQUIETLY=N
59 59
60 60 With --prefix foo
61 61
62 62 $ hgserve --prefix foo
63 63 listening at http://localhost/foo/ (bound to 127.0.0.1:HGPORT1)
64 64 % errors
65 65
66 66 With --prefix /foo
67 67
68 68 $ hgserve --prefix /foo
69 69 listening at http://localhost/foo/ (bound to 127.0.0.1:HGPORT1)
70 70 % errors
71 71
72 72 With --prefix foo/
73 73
74 74 $ hgserve --prefix foo/
75 75 listening at http://localhost/foo/ (bound to 127.0.0.1:HGPORT1)
76 76 % errors
77 77
78 78 With --prefix /foo/
79 79
80 80 $ hgserve --prefix /foo/
81 81 listening at http://localhost/foo/ (bound to 127.0.0.1:HGPORT1)
82 82 % errors
General Comments 0
You need to be logged in to leave comments. Login now