Show More
This diff has been collapsed as it changes many lines, (630 lines changed) Show them Hide them | |||||
@@ -0,0 +1,630 b'' | |||||
|
1 | # testparseutil.py - utilities to parse test script for check tools | |||
|
2 | # | |||
|
3 | # Copyright 2018 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms of the | |||
|
6 | # GNU General Public License version 2 or any later version. | |||
|
7 | ||||
|
8 | from __future__ import absolute_import, print_function | |||
|
9 | ||||
|
10 | import abc | |||
|
11 | import re | |||
|
12 | import sys | |||
|
13 | ||||
|
14 | #################### | |||
|
15 | # for Python3 compatibility (almost comes from mercurial/pycompat.py) | |||
|
16 | ||||
|
17 | ispy3 = (sys.version_info[0] >= 3) | |||
|
18 | ||||
|
19 | def identity(a): | |||
|
20 | return a | |||
|
21 | ||||
|
22 | def _rapply(f, xs): | |||
|
23 | if xs is None: | |||
|
24 | # assume None means non-value of optional data | |||
|
25 | return xs | |||
|
26 | if isinstance(xs, (list, set, tuple)): | |||
|
27 | return type(xs)(_rapply(f, x) for x in xs) | |||
|
28 | if isinstance(xs, dict): | |||
|
29 | return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items()) | |||
|
30 | return f(xs) | |||
|
31 | ||||
|
32 | def rapply(f, xs): | |||
|
33 | if f is identity: | |||
|
34 | # fast path mainly for py2 | |||
|
35 | return xs | |||
|
36 | return _rapply(f, xs) | |||
|
37 | ||||
|
38 | if ispy3: | |||
|
39 | import builtins | |||
|
40 | ||||
|
41 | # TODO: .buffer might not exist if std streams were replaced; we'll need | |||
|
42 | # a silly wrapper to make a bytes stream backed by a unicode one. | |||
|
43 | stdin = sys.stdin.buffer | |||
|
44 | stdout = sys.stdout.buffer | |||
|
45 | stderr = sys.stderr.buffer | |||
|
46 | ||||
|
47 | def bytestr(s): | |||
|
48 | # tiny version of pycompat.bytestr | |||
|
49 | return s.encode('latin1') | |||
|
50 | ||||
|
51 | def sysstr(s): | |||
|
52 | if isinstance(s, builtins.str): | |||
|
53 | return s | |||
|
54 | return s.decode(u'latin-1') | |||
|
55 | ||||
|
56 | def opentext(f): | |||
|
57 | return open(f, 'rb') | |||
|
58 | else: | |||
|
59 | stdin = sys.stdin | |||
|
60 | stdout = sys.stdout | |||
|
61 | stderr = sys.stderr | |||
|
62 | ||||
|
63 | bytestr = str | |||
|
64 | sysstr = identity | |||
|
65 | ||||
|
66 | opentext = open | |||
|
67 | ||||
|
68 | def b2s(x): | |||
|
69 | # convert BYTES elements in "x" to SYSSTR recursively | |||
|
70 | return rapply(sysstr, x) | |||
|
71 | ||||
|
72 | def writeout(data): | |||
|
73 | # write "data" in BYTES into stdout | |||
|
74 | stdout.write(data) | |||
|
75 | ||||
|
76 | def writeerr(data): | |||
|
77 | # write "data" in BYTES into stderr | |||
|
78 | stderr.write(data) | |||
|
79 | ||||
|
80 | #################### | |||
|
81 | ||||
|
82 | class embeddedmatcher(object): | |||
|
83 | """Base class to detect embedded code fragments in *.t test script | |||
|
84 | """ | |||
|
85 | __metaclass__ = abc.ABCMeta | |||
|
86 | ||||
|
87 | def __init__(self, desc): | |||
|
88 | self.desc = desc | |||
|
89 | ||||
|
90 | @abc.abstractmethod | |||
|
91 | def startsat(self, line): | |||
|
92 | """Examine whether embedded code starts at line | |||
|
93 | ||||
|
94 | This can return arbitrary object, and it is used as 'ctx' for | |||
|
95 | subsequent method invocations. | |||
|
96 | """ | |||
|
97 | ||||
|
98 | @abc.abstractmethod | |||
|
99 | def endsat(self, ctx, line): | |||
|
100 | """Examine whether embedded code ends at line""" | |||
|
101 | ||||
|
102 | @abc.abstractmethod | |||
|
103 | def isinside(self, ctx, line): | |||
|
104 | """Examine whether line is inside embedded code, if not yet endsat | |||
|
105 | """ | |||
|
106 | ||||
|
107 | @abc.abstractmethod | |||
|
108 | def ignores(self, ctx): | |||
|
109 | """Examine whether detected embedded code should be ignored""" | |||
|
110 | ||||
|
111 | @abc.abstractmethod | |||
|
112 | def filename(self, ctx): | |||
|
113 | """Return filename of embedded code | |||
|
114 | ||||
|
115 | If filename isn't specified for embedded code explicitly, this | |||
|
116 | returns None. | |||
|
117 | """ | |||
|
118 | ||||
|
119 | @abc.abstractmethod | |||
|
120 | def codeatstart(self, ctx, line): | |||
|
121 | """Return actual code at the start line of embedded code | |||
|
122 | ||||
|
123 | This might return None, if the start line doesn't contain | |||
|
124 | actual code. | |||
|
125 | """ | |||
|
126 | ||||
|
127 | @abc.abstractmethod | |||
|
128 | def codeatend(self, ctx, line): | |||
|
129 | """Return actual code at the end line of embedded code | |||
|
130 | ||||
|
131 | This might return None, if the end line doesn't contain actual | |||
|
132 | code. | |||
|
133 | """ | |||
|
134 | ||||
|
135 | @abc.abstractmethod | |||
|
136 | def codeinside(self, ctx, line): | |||
|
137 | """Return actual code at line inside embedded code""" | |||
|
138 | ||||
|
139 | def embedded(basefile, lines, errors, matchers): | |||
|
140 | """pick embedded code fragments up from given lines | |||
|
141 | ||||
|
142 | This is common parsing logic, which examines specified matchers on | |||
|
143 | given lines. | |||
|
144 | ||||
|
145 | :basefile: a name of a file, from which lines to be parsed come. | |||
|
146 | :lines: to be parsed (might be a value returned by "open(basefile)") | |||
|
147 | :errors: an array, into which messages for detected error are stored | |||
|
148 | :matchers: an array of embeddedmatcher objects | |||
|
149 | ||||
|
150 | This function yields '(filename, starts, ends, code)' tuple. | |||
|
151 | ||||
|
152 | :filename: a name of embedded code, if it is explicitly specified | |||
|
153 | (e.g. "foobar" of "cat >> foobar <<EOF"). | |||
|
154 | Otherwise, this is None | |||
|
155 | :starts: line number (1-origin), at which embedded code starts (inclusive) | |||
|
156 | :ends: line number (1-origin), at which embedded code ends (exclusive) | |||
|
157 | :code: extracted embedded code, which is single-stringified | |||
|
158 | ||||
|
159 | >>> class ambigmatcher(object): | |||
|
160 | ... # mock matcher class to examine implementation of | |||
|
161 | ... # "ambiguous matching" corner case | |||
|
162 | ... def __init__(self, desc, matchfunc): | |||
|
163 | ... self.desc = desc | |||
|
164 | ... self.matchfunc = matchfunc | |||
|
165 | ... def startsat(self, line): | |||
|
166 | ... return self.matchfunc(line) | |||
|
167 | >>> ambig1 = ambigmatcher(b'ambiguous #1', | |||
|
168 | ... lambda l: l.startswith(b' $ cat ')) | |||
|
169 | >>> ambig2 = ambigmatcher(b'ambiguous #2', | |||
|
170 | ... lambda l: l.endswith(b'<< EOF\\n')) | |||
|
171 | >>> lines = [b' $ cat > foo.py << EOF\\n'] | |||
|
172 | >>> errors = [] | |||
|
173 | >>> matchers = [ambig1, ambig2] | |||
|
174 | >>> list(t for t in embedded(b'<dummy>', lines, errors, matchers)) | |||
|
175 | [] | |||
|
176 | >>> b2s(errors) | |||
|
177 | ['<dummy>:1: ambiguous line for "ambiguous #1", "ambiguous #2"'] | |||
|
178 | ||||
|
179 | """ | |||
|
180 | matcher = None | |||
|
181 | ctx = filename = code = startline = None # for pyflakes | |||
|
182 | ||||
|
183 | for lineno, line in enumerate(lines, 1): | |||
|
184 | if not line.endswith(b'\n'): | |||
|
185 | line += b'\n' # to normalize EOF line | |||
|
186 | if matcher: # now, inside embedded code | |||
|
187 | if matcher.endsat(ctx, line): | |||
|
188 | codeatend = matcher.codeatend(ctx, line) | |||
|
189 | if codeatend is not None: | |||
|
190 | code.append(codeatend) | |||
|
191 | if not matcher.ignores(ctx): | |||
|
192 | yield (filename, startline, lineno, b''.join(code)) | |||
|
193 | matcher = None | |||
|
194 | # DO NOT "continue", because line might start next fragment | |||
|
195 | elif not matcher.isinside(ctx, line): | |||
|
196 | # this is an error of basefile | |||
|
197 | # (if matchers are implemented correctly) | |||
|
198 | errors.append(b'%s:%d: unexpected line for "%s"' | |||
|
199 | % (basefile, lineno, matcher.desc)) | |||
|
200 | # stop extracting embedded code by current 'matcher', | |||
|
201 | # because appearance of unexpected line might mean | |||
|
202 | # that expected end-of-embedded-code line might never | |||
|
203 | # appear | |||
|
204 | matcher = None | |||
|
205 | # DO NOT "continue", because line might start next fragment | |||
|
206 | else: | |||
|
207 | code.append(matcher.codeinside(ctx, line)) | |||
|
208 | continue | |||
|
209 | ||||
|
210 | # examine whether current line starts embedded code or not | |||
|
211 | assert not matcher | |||
|
212 | ||||
|
213 | matched = [] | |||
|
214 | for m in matchers: | |||
|
215 | ctx = m.startsat(line) | |||
|
216 | if ctx: | |||
|
217 | matched.append((m, ctx)) | |||
|
218 | if matched: | |||
|
219 | if len(matched) > 1: | |||
|
220 | # this is an error of matchers, maybe | |||
|
221 | errors.append(b'%s:%d: ambiguous line for %s' % | |||
|
222 | (basefile, lineno, | |||
|
223 | b', '.join([b'"%s"' % m.desc | |||
|
224 | for m, c in matched]))) | |||
|
225 | # omit extracting embedded code, because choosing | |||
|
226 | # arbitrary matcher from matched ones might fail to | |||
|
227 | # detect the end of embedded code as expected. | |||
|
228 | continue | |||
|
229 | matcher, ctx = matched[0] | |||
|
230 | filename = matcher.filename(ctx) | |||
|
231 | code = [] | |||
|
232 | codeatstart = matcher.codeatstart(ctx, line) | |||
|
233 | if codeatstart is not None: | |||
|
234 | code.append(codeatstart) | |||
|
235 | startline = lineno | |||
|
236 | else: | |||
|
237 | startline = lineno + 1 | |||
|
238 | ||||
|
239 | if matcher: | |||
|
240 | # examine whether EOF ends embedded code, because embedded | |||
|
241 | # code isn't yet ended explicitly | |||
|
242 | if matcher.endsat(ctx, b'\n'): | |||
|
243 | codeatend = matcher.codeatend(ctx, b'\n') | |||
|
244 | if codeatend is not None: | |||
|
245 | code.append(codeatend) | |||
|
246 | if not matcher.ignores(ctx): | |||
|
247 | yield (filename, startline, lineno + 1, b''.join(code)) | |||
|
248 | else: | |||
|
249 | # this is an error of basefile | |||
|
250 | # (if matchers are implemented correctly) | |||
|
251 | errors.append(b'%s:%d: unexpected end of file for "%s"' | |||
|
252 | % (basefile, lineno, matcher.desc)) | |||
|
253 | ||||
|
254 | # heredoc limit mark to ignore embedded code at check-code.py or so | |||
|
255 | heredocignorelimit = b'NO_CHECK_EOF' | |||
|
256 | ||||
|
257 | # the pattern to match against cases below, and to return a limit mark | |||
|
258 | # string as 'lname' group | |||
|
259 | # | |||
|
260 | # - << LIMITMARK | |||
|
261 | # - << "LIMITMARK" | |||
|
262 | # - << 'LIMITMARK' | |||
|
263 | heredoclimitpat = br'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)' | |||
|
264 | ||||
|
265 | class fileheredocmatcher(embeddedmatcher): | |||
|
266 | """Detect "cat > FILE << LIMIT" style embedded code | |||
|
267 | ||||
|
268 | >>> matcher = fileheredocmatcher(b'heredoc .py file', br'[^<]+\.py') | |||
|
269 | >>> b2s(matcher.startsat(b' $ cat > file.py << EOF\\n')) | |||
|
270 | ('file.py', ' > EOF\\n') | |||
|
271 | >>> b2s(matcher.startsat(b' $ cat >>file.py <<EOF\\n')) | |||
|
272 | ('file.py', ' > EOF\\n') | |||
|
273 | >>> b2s(matcher.startsat(b' $ cat> \\x27any file.py\\x27<< "EOF"\\n')) | |||
|
274 | ('any file.py', ' > EOF\\n') | |||
|
275 | >>> b2s(matcher.startsat(b" $ cat > file.py << 'ANYLIMIT'\\n")) | |||
|
276 | ('file.py', ' > ANYLIMIT\\n') | |||
|
277 | >>> b2s(matcher.startsat(b' $ cat<<ANYLIMIT>"file.py"\\n')) | |||
|
278 | ('file.py', ' > ANYLIMIT\\n') | |||
|
279 | >>> start = b' $ cat > file.py << EOF\\n' | |||
|
280 | >>> ctx = matcher.startsat(start) | |||
|
281 | >>> matcher.codeatstart(ctx, start) | |||
|
282 | >>> b2s(matcher.filename(ctx)) | |||
|
283 | 'file.py' | |||
|
284 | >>> matcher.ignores(ctx) | |||
|
285 | False | |||
|
286 | >>> inside = b' > foo = 1\\n' | |||
|
287 | >>> matcher.endsat(ctx, inside) | |||
|
288 | False | |||
|
289 | >>> matcher.isinside(ctx, inside) | |||
|
290 | True | |||
|
291 | >>> b2s(matcher.codeinside(ctx, inside)) | |||
|
292 | 'foo = 1\\n' | |||
|
293 | >>> end = b' > EOF\\n' | |||
|
294 | >>> matcher.endsat(ctx, end) | |||
|
295 | True | |||
|
296 | >>> matcher.codeatend(ctx, end) | |||
|
297 | >>> matcher.endsat(ctx, b' > EOFEOF\\n') | |||
|
298 | False | |||
|
299 | >>> ctx = matcher.startsat(b' $ cat > file.py << NO_CHECK_EOF\\n') | |||
|
300 | >>> matcher.ignores(ctx) | |||
|
301 | True | |||
|
302 | """ | |||
|
303 | _prefix = b' > ' | |||
|
304 | ||||
|
305 | def __init__(self, desc, namepat): | |||
|
306 | super(fileheredocmatcher, self).__init__(desc) | |||
|
307 | ||||
|
308 | # build the pattern to match against cases below (and ">>" | |||
|
309 | # variants), and to return a target filename string as 'name' | |||
|
310 | # group | |||
|
311 | # | |||
|
312 | # - > NAMEPAT | |||
|
313 | # - > "NAMEPAT" | |||
|
314 | # - > 'NAMEPAT' | |||
|
315 | namepat = (br'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)' | |||
|
316 | % namepat) | |||
|
317 | self._fileres = [ | |||
|
318 | # "cat > NAME << LIMIT" case | |||
|
319 | re.compile(br' \$ \s*cat' + namepat + heredoclimitpat), | |||
|
320 | # "cat << LIMIT > NAME" case | |||
|
321 | re.compile(br' \$ \s*cat' + heredoclimitpat + namepat), | |||
|
322 | ] | |||
|
323 | ||||
|
324 | def startsat(self, line): | |||
|
325 | # ctx is (filename, END-LINE-OF-EMBEDDED-CODE) tuple | |||
|
326 | for filere in self._fileres: | |||
|
327 | matched = filere.match(line) | |||
|
328 | if matched: | |||
|
329 | return (matched.group('name'), | |||
|
330 | b' > %s\n' % matched.group('limit')) | |||
|
331 | ||||
|
332 | def endsat(self, ctx, line): | |||
|
333 | return ctx[1] == line | |||
|
334 | ||||
|
335 | def isinside(self, ctx, line): | |||
|
336 | return line.startswith(self._prefix) | |||
|
337 | ||||
|
338 | def ignores(self, ctx): | |||
|
339 | return b' > %s\n' % heredocignorelimit == ctx[1] | |||
|
340 | ||||
|
341 | def filename(self, ctx): | |||
|
342 | return ctx[0] | |||
|
343 | ||||
|
344 | def codeatstart(self, ctx, line): | |||
|
345 | return None # no embedded code at start line | |||
|
346 | ||||
|
347 | def codeatend(self, ctx, line): | |||
|
348 | return None # no embedded code at end line | |||
|
349 | ||||
|
350 | def codeinside(self, ctx, line): | |||
|
351 | return line[len(self._prefix):] # strip prefix | |||
|
352 | ||||
|
353 | #### | |||
|
354 | # for embedded python script | |||
|
355 | ||||
|
356 | class pydoctestmatcher(embeddedmatcher): | |||
|
357 | """Detect ">>> code" style embedded python code | |||
|
358 | ||||
|
359 | >>> matcher = pydoctestmatcher() | |||
|
360 | >>> startline = b' >>> foo = 1\\n' | |||
|
361 | >>> matcher.startsat(startline) | |||
|
362 | True | |||
|
363 | >>> matcher.startsat(b' ... foo = 1\\n') | |||
|
364 | False | |||
|
365 | >>> ctx = matcher.startsat(startline) | |||
|
366 | >>> matcher.filename(ctx) | |||
|
367 | >>> matcher.ignores(ctx) | |||
|
368 | False | |||
|
369 | >>> b2s(matcher.codeatstart(ctx, startline)) | |||
|
370 | 'foo = 1\\n' | |||
|
371 | >>> inside = b' >>> foo = 1\\n' | |||
|
372 | >>> matcher.endsat(ctx, inside) | |||
|
373 | False | |||
|
374 | >>> matcher.isinside(ctx, inside) | |||
|
375 | True | |||
|
376 | >>> b2s(matcher.codeinside(ctx, inside)) | |||
|
377 | 'foo = 1\\n' | |||
|
378 | >>> inside = b' ... foo = 1\\n' | |||
|
379 | >>> matcher.endsat(ctx, inside) | |||
|
380 | False | |||
|
381 | >>> matcher.isinside(ctx, inside) | |||
|
382 | True | |||
|
383 | >>> b2s(matcher.codeinside(ctx, inside)) | |||
|
384 | 'foo = 1\\n' | |||
|
385 | >>> inside = b' expected output\\n' | |||
|
386 | >>> matcher.endsat(ctx, inside) | |||
|
387 | False | |||
|
388 | >>> matcher.isinside(ctx, inside) | |||
|
389 | True | |||
|
390 | >>> b2s(matcher.codeinside(ctx, inside)) | |||
|
391 | '\\n' | |||
|
392 | >>> inside = b' \\n' | |||
|
393 | >>> matcher.endsat(ctx, inside) | |||
|
394 | False | |||
|
395 | >>> matcher.isinside(ctx, inside) | |||
|
396 | True | |||
|
397 | >>> b2s(matcher.codeinside(ctx, inside)) | |||
|
398 | '\\n' | |||
|
399 | >>> end = b' $ foo bar\\n' | |||
|
400 | >>> matcher.endsat(ctx, end) | |||
|
401 | True | |||
|
402 | >>> matcher.codeatend(ctx, end) | |||
|
403 | >>> end = b'\\n' | |||
|
404 | >>> matcher.endsat(ctx, end) | |||
|
405 | True | |||
|
406 | >>> matcher.codeatend(ctx, end) | |||
|
407 | """ | |||
|
408 | _prefix = b' >>> ' | |||
|
409 | _prefixre = re.compile(br' (>>>|\.\.\.) ') | |||
|
410 | ||||
|
411 | # If a line matches against not _prefixre but _outputre, that line | |||
|
412 | # is "an expected output line" (= not a part of code fragment). | |||
|
413 | # | |||
|
414 | # Strictly speaking, a line matching against "(#if|#else|#endif)" | |||
|
415 | # is also treated similarly in "inline python code" semantics by | |||
|
416 | # run-tests.py. But "directive line inside inline python code" | |||
|
417 | # should be rejected by Mercurial reviewers. Therefore, this | |||
|
418 | # regexp does not matche against such directive lines. | |||
|
419 | _outputre = re.compile(br' $| [^$]') | |||
|
420 | ||||
|
421 | def __init__(self): | |||
|
422 | super(pydoctestmatcher, self).__init__(b"doctest style python code") | |||
|
423 | ||||
|
424 | def startsat(self, line): | |||
|
425 | # ctx is "True" | |||
|
426 | return line.startswith(self._prefix) | |||
|
427 | ||||
|
428 | def endsat(self, ctx, line): | |||
|
429 | return not (self._prefixre.match(line) or self._outputre.match(line)) | |||
|
430 | ||||
|
431 | def isinside(self, ctx, line): | |||
|
432 | return True # always true, if not yet ended | |||
|
433 | ||||
|
434 | def ignores(self, ctx): | |||
|
435 | return False # should be checked always | |||
|
436 | ||||
|
437 | def filename(self, ctx): | |||
|
438 | return None # no filename | |||
|
439 | ||||
|
440 | def codeatstart(self, ctx, line): | |||
|
441 | return line[len(self._prefix):] # strip prefix ' >>> '/' ... ' | |||
|
442 | ||||
|
443 | def codeatend(self, ctx, line): | |||
|
444 | return None # no embedded code at end line | |||
|
445 | ||||
|
446 | def codeinside(self, ctx, line): | |||
|
447 | if self._prefixre.match(line): | |||
|
448 | return line[len(self._prefix):] # strip prefix ' >>> '/' ... ' | |||
|
449 | return b'\n' # an expected output line is treated as an empty line | |||
|
450 | ||||
|
451 | class pyheredocmatcher(embeddedmatcher): | |||
|
452 | """Detect "python << LIMIT" style embedded python code | |||
|
453 | ||||
|
454 | >>> matcher = pyheredocmatcher() | |||
|
455 | >>> b2s(matcher.startsat(b' $ python << EOF\\n')) | |||
|
456 | ' > EOF\\n' | |||
|
457 | >>> b2s(matcher.startsat(b' $ $PYTHON <<EOF\\n')) | |||
|
458 | ' > EOF\\n' | |||
|
459 | >>> b2s(matcher.startsat(b' $ "$PYTHON"<< "EOF"\\n')) | |||
|
460 | ' > EOF\\n' | |||
|
461 | >>> b2s(matcher.startsat(b" $ $PYTHON << 'ANYLIMIT'\\n")) | |||
|
462 | ' > ANYLIMIT\\n' | |||
|
463 | >>> matcher.startsat(b' $ "$PYTHON" < EOF\\n') | |||
|
464 | >>> start = b' $ python << EOF\\n' | |||
|
465 | >>> ctx = matcher.startsat(start) | |||
|
466 | >>> matcher.codeatstart(ctx, start) | |||
|
467 | >>> matcher.filename(ctx) | |||
|
468 | >>> matcher.ignores(ctx) | |||
|
469 | False | |||
|
470 | >>> inside = b' > foo = 1\\n' | |||
|
471 | >>> matcher.endsat(ctx, inside) | |||
|
472 | False | |||
|
473 | >>> matcher.isinside(ctx, inside) | |||
|
474 | True | |||
|
475 | >>> b2s(matcher.codeinside(ctx, inside)) | |||
|
476 | 'foo = 1\\n' | |||
|
477 | >>> end = b' > EOF\\n' | |||
|
478 | >>> matcher.endsat(ctx, end) | |||
|
479 | True | |||
|
480 | >>> matcher.codeatend(ctx, end) | |||
|
481 | >>> matcher.endsat(ctx, b' > EOFEOF\\n') | |||
|
482 | False | |||
|
483 | >>> ctx = matcher.startsat(b' $ python << NO_CHECK_EOF\\n') | |||
|
484 | >>> matcher.ignores(ctx) | |||
|
485 | True | |||
|
486 | """ | |||
|
487 | _prefix = b' > ' | |||
|
488 | ||||
|
489 | _startre = re.compile(br' \$ (\$PYTHON|"\$PYTHON"|python).*' + | |||
|
490 | heredoclimitpat) | |||
|
491 | ||||
|
492 | def __init__(self): | |||
|
493 | super(pyheredocmatcher, self).__init__(b"heredoc python invocation") | |||
|
494 | ||||
|
495 | def startsat(self, line): | |||
|
496 | # ctx is END-LINE-OF-EMBEDDED-CODE | |||
|
497 | matched = self._startre.match(line) | |||
|
498 | if matched: | |||
|
499 | return b' > %s\n' % matched.group('limit') | |||
|
500 | ||||
|
501 | def endsat(self, ctx, line): | |||
|
502 | return ctx == line | |||
|
503 | ||||
|
504 | def isinside(self, ctx, line): | |||
|
505 | return line.startswith(self._prefix) | |||
|
506 | ||||
|
507 | def ignores(self, ctx): | |||
|
508 | return b' > %s\n' % heredocignorelimit == ctx | |||
|
509 | ||||
|
510 | def filename(self, ctx): | |||
|
511 | return None # no filename | |||
|
512 | ||||
|
513 | def codeatstart(self, ctx, line): | |||
|
514 | return None # no embedded code at start line | |||
|
515 | ||||
|
516 | def codeatend(self, ctx, line): | |||
|
517 | return None # no embedded code at end line | |||
|
518 | ||||
|
519 | def codeinside(self, ctx, line): | |||
|
520 | return line[len(self._prefix):] # strip prefix | |||
|
521 | ||||
|
522 | _pymatchers = [ | |||
|
523 | pydoctestmatcher(), | |||
|
524 | pyheredocmatcher(), | |||
|
525 | # use '[^<]+' instead of '\S+', in order to match against | |||
|
526 | # paths including whitespaces | |||
|
527 | fileheredocmatcher(b'heredoc .py file', br'[^<]+\.py'), | |||
|
528 | ] | |||
|
529 | ||||
|
530 | def pyembedded(basefile, lines, errors): | |||
|
531 | return embedded(basefile, lines, errors, _pymatchers) | |||
|
532 | ||||
|
533 | #### | |||
|
534 | # for embedded shell script | |||
|
535 | ||||
|
536 | _shmatchers = [ | |||
|
537 | # use '[^<]+' instead of '\S+', in order to match against | |||
|
538 | # paths including whitespaces | |||
|
539 | fileheredocmatcher(b'heredoc .sh file', br'[^<]+\.sh'), | |||
|
540 | ] | |||
|
541 | ||||
|
542 | def shembedded(basefile, lines, errors): | |||
|
543 | return embedded(basefile, lines, errors, _shmatchers) | |||
|
544 | ||||
|
545 | #### | |||
|
546 | # for embedded hgrc configuration | |||
|
547 | ||||
|
548 | _hgrcmatchers = [ | |||
|
549 | # use '[^<]+' instead of '\S+', in order to match against | |||
|
550 | # paths including whitespaces | |||
|
551 | fileheredocmatcher(b'heredoc hgrc file', | |||
|
552 | br'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'), | |||
|
553 | ] | |||
|
554 | ||||
|
555 | def hgrcembedded(basefile, lines, errors): | |||
|
556 | return embedded(basefile, lines, errors, _hgrcmatchers) | |||
|
557 | ||||
|
558 | #### | |||
|
559 | ||||
|
560 | if __name__ == "__main__": | |||
|
561 | import optparse | |||
|
562 | import sys | |||
|
563 | ||||
|
564 | def showembedded(basefile, lines, embeddedfunc, opts): | |||
|
565 | errors = [] | |||
|
566 | for name, starts, ends, code in embeddedfunc(basefile, lines, errors): | |||
|
567 | if not name: | |||
|
568 | name = b'<anonymous>' | |||
|
569 | writeout(b"%s:%d: %s starts\n" % (basefile, starts, name)) | |||
|
570 | if opts.verbose and code: | |||
|
571 | writeout(b" |%s\n" % | |||
|
572 | b"\n |".join(l for l in code.splitlines())) | |||
|
573 | writeout(b"%s:%d: %s ends\n" % (basefile, ends, name)) | |||
|
574 | for e in errors: | |||
|
575 | writeerr(b"%s\n" % e) | |||
|
576 | return len(errors) | |||
|
577 | ||||
|
578 | def applyembedded(args, embeddedfunc, opts): | |||
|
579 | ret = 0 | |||
|
580 | if args: | |||
|
581 | for f in args: | |||
|
582 | with opentext(f) as fp: | |||
|
583 | if showembedded(bytestr(f), fp, embeddedfunc, opts): | |||
|
584 | ret = 1 | |||
|
585 | else: | |||
|
586 | lines = [l for l in stdin.readlines()] | |||
|
587 | if showembedded(b'<stdin>', lines, embeddedfunc, opts): | |||
|
588 | ret = 1 | |||
|
589 | return ret | |||
|
590 | ||||
|
591 | commands = {} | |||
|
592 | def command(name, desc): | |||
|
593 | def wrap(func): | |||
|
594 | commands[name] = (desc, func) | |||
|
595 | return wrap | |||
|
596 | ||||
|
597 | @command("pyembedded", "detect embedded python script") | |||
|
598 | def pyembeddedcmd(args, opts): | |||
|
599 | return applyembedded(args, pyembedded, opts) | |||
|
600 | ||||
|
601 | @command("shembedded", "detect embedded shell script") | |||
|
602 | def shembeddedcmd(args, opts): | |||
|
603 | return applyembedded(args, shembedded, opts) | |||
|
604 | ||||
|
605 | @command("hgrcembedded", "detect embedded hgrc configuration") | |||
|
606 | def hgrcembeddedcmd(args, opts): | |||
|
607 | return applyembedded(args, hgrcembedded, opts) | |||
|
608 | ||||
|
609 | availablecommands = "\n".join([" - %s: %s" % (key, value[0]) | |||
|
610 | for key, value in commands.items()]) | |||
|
611 | ||||
|
612 | parser = optparse.OptionParser("""%prog COMMAND [file ...] | |||
|
613 | ||||
|
614 | Pick up embedded code fragments from given file(s) or stdin, and list | |||
|
615 | up start/end lines of them in standard compiler format | |||
|
616 | ("FILENAME:LINENO:"). | |||
|
617 | ||||
|
618 | Available commands are: | |||
|
619 | """ + availablecommands + """ | |||
|
620 | """) | |||
|
621 | parser.add_option("-v", "--verbose", | |||
|
622 | help="enable additional output (e.g. actual code)", | |||
|
623 | action="store_true") | |||
|
624 | (opts, args) = parser.parse_args() | |||
|
625 | ||||
|
626 | if not args or args[0] not in commands: | |||
|
627 | parser.print_help() | |||
|
628 | sys.exit(255) | |||
|
629 | ||||
|
630 | sys.exit(commands[args[0]][1](args[1:], opts)) |
@@ -0,0 +1,192 b'' | |||||
|
1 | $ testparseutil="$TESTDIR"/../contrib/testparseutil.py | |||
|
2 | ||||
|
3 | Internal test by doctest | |||
|
4 | ||||
|
5 | $ "$PYTHON" -m doctest "$testparseutil" | |||
|
6 | ||||
|
7 | Tests for embedded python script | |||
|
8 | ||||
|
9 | Typical cases | |||
|
10 | ||||
|
11 | $ "$PYTHON" "$testparseutil" -v pyembedded <<NO_CHECK_EOF | |||
|
12 | > >>> for f in [1, 2, 3]: | |||
|
13 | > ... foo = 1 | |||
|
14 | > >>> foo = 2 | |||
|
15 | > $ echo "doctest is terminated by command, empty line, or comment" | |||
|
16 | > >>> foo = 31 | |||
|
17 | > expected output of doctest fragment | |||
|
18 | > >>> foo = 32 | |||
|
19 | > | |||
|
20 | > >>> foo = 33 | |||
|
21 | > | |||
|
22 | > >>> foo = 34 | |||
|
23 | > comment | |||
|
24 | > >>> foo = 35 | |||
|
25 | > | |||
|
26 | > $ "\$PYTHON" <<EOF | |||
|
27 | > > foo = 4 | |||
|
28 | > > | |||
|
29 | > > EOF | |||
|
30 | > $ cat > foo.py <<EOF | |||
|
31 | > > foo = 5 | |||
|
32 | > > EOF | |||
|
33 | > $ cat >> foo.py <<EOF | |||
|
34 | > > foo = 6 # appended | |||
|
35 | > > EOF | |||
|
36 | > | |||
|
37 | > NO_CHECK_EOF limit mark makes parsing ignore corresponded fragment | |||
|
38 | > (this is useful to use bad code intentionally) | |||
|
39 | > | |||
|
40 | > $ "\$PYTHON" <<NO_CHECK_EOF | |||
|
41 | > > foo = 7 # this should be ignored at detection | |||
|
42 | > > NO_CHECK_EOF | |||
|
43 | > $ cat > foo.py <<NO_CHECK_EOF | |||
|
44 | > > foo = 8 # this should be ignored at detection | |||
|
45 | > > NO_CHECK_EOF | |||
|
46 | > | |||
|
47 | > doctest fragment ended by EOF | |||
|
48 | > | |||
|
49 | > >>> foo = 9 | |||
|
50 | > NO_CHECK_EOF | |||
|
51 | <stdin>:1: <anonymous> starts | |||
|
52 | |for f in [1, 2, 3]: | |||
|
53 | | foo = 1 | |||
|
54 | |foo = 2 | |||
|
55 | <stdin>:4: <anonymous> ends | |||
|
56 | <stdin>:5: <anonymous> starts | |||
|
57 | |foo = 31 | |||
|
58 | | | |||
|
59 | |foo = 32 | |||
|
60 | | | |||
|
61 | |foo = 33 | |||
|
62 | <stdin>:10: <anonymous> ends | |||
|
63 | <stdin>:11: <anonymous> starts | |||
|
64 | |foo = 34 | |||
|
65 | <stdin>:12: <anonymous> ends | |||
|
66 | <stdin>:13: <anonymous> starts | |||
|
67 | |foo = 35 | |||
|
68 | <stdin>:14: <anonymous> ends | |||
|
69 | <stdin>:16: <anonymous> starts | |||
|
70 | |foo = 4 | |||
|
71 | | | |||
|
72 | <stdin>:18: <anonymous> ends | |||
|
73 | <stdin>:20: foo.py starts | |||
|
74 | |foo = 5 | |||
|
75 | <stdin>:21: foo.py ends | |||
|
76 | <stdin>:23: foo.py starts | |||
|
77 | |foo = 6 # appended | |||
|
78 | <stdin>:24: foo.py ends | |||
|
79 | <stdin>:38: <anonymous> starts | |||
|
80 | |foo = 9 | |||
|
81 | <stdin>:39: <anonymous> ends | |||
|
82 | ||||
|
83 | Invalid test script | |||
|
84 | ||||
|
85 | (similar test for shell script and hgrc configuration is omitted, | |||
|
86 | because this tests common base class of them) | |||
|
87 | ||||
|
88 | $ "$PYTHON" "$testparseutil" -v pyembedded <<NO_CHECK_EOF > detected | |||
|
89 | > $ "\$PYTHON" <<EOF | |||
|
90 | > > foo = 1 | |||
|
91 | > | |||
|
92 | > $ "\$PYTHON" <<EOF | |||
|
93 | > > foo = 2 | |||
|
94 | > $ cat > bar.py <<EOF | |||
|
95 | > > bar = 2 # this fragment will be detected as expected | |||
|
96 | > > EOF | |||
|
97 | > | |||
|
98 | > $ cat > foo.py <<EOF | |||
|
99 | > > foo = 3 | |||
|
100 | > NO_CHECK_EOF | |||
|
101 | <stdin>:3: unexpected line for "heredoc python invocation" | |||
|
102 | <stdin>:6: unexpected line for "heredoc python invocation" | |||
|
103 | <stdin>:11: unexpected end of file for "heredoc .py file" | |||
|
104 | [1] | |||
|
105 | $ cat detected | |||
|
106 | <stdin>:7: bar.py starts | |||
|
107 | |bar = 2 # this fragment will be detected as expected | |||
|
108 | <stdin>:8: bar.py ends | |||
|
109 | ||||
|
110 | Tests for embedded shell script | |||
|
111 | ||||
|
112 | $ "$PYTHON" "$testparseutil" -v shembedded <<NO_CHECK_EOF | |||
|
113 | > $ cat > foo.sh <<EOF | |||
|
114 | > > foo = 1 | |||
|
115 | > > | |||
|
116 | > > foo = 2 | |||
|
117 | > > EOF | |||
|
118 | > $ cat >> foo.sh <<EOF | |||
|
119 | > > foo = 3 # appended | |||
|
120 | > > EOF | |||
|
121 | > | |||
|
122 | > NO_CHECK_EOF limit mark makes parsing ignore corresponded fragment | |||
|
123 | > (this is useful to use bad code intentionally) | |||
|
124 | > | |||
|
125 | > $ cat > foo.sh <<NO_CHECK_EOF | |||
|
126 | > > # this should be ignored at detection | |||
|
127 | > > foo = 4 | |||
|
128 | > > NO_CHECK_EOF | |||
|
129 | > | |||
|
130 | > NO_CHECK_EOF | |||
|
131 | <stdin>:2: foo.sh starts | |||
|
132 | |foo = 1 | |||
|
133 | | | |||
|
134 | |foo = 2 | |||
|
135 | <stdin>:5: foo.sh ends | |||
|
136 | <stdin>:7: foo.sh starts | |||
|
137 | |foo = 3 # appended | |||
|
138 | <stdin>:8: foo.sh ends | |||
|
139 | ||||
|
140 | Tests for embedded hgrc configuration | |||
|
141 | ||||
|
142 | $ "$PYTHON" "$testparseutil" -v hgrcembedded <<NO_CHECK_EOF | |||
|
143 | > $ cat > .hg/hgrc <<EOF | |||
|
144 | > > [ui] | |||
|
145 | > > verbose = true | |||
|
146 | > > | |||
|
147 | > > # end of local configuration | |||
|
148 | > > EOF | |||
|
149 | > | |||
|
150 | > $ cat > \$HGRCPATH <<EOF | |||
|
151 | > > [extensions] | |||
|
152 | > > rebase = | |||
|
153 | > > # end of global configuration | |||
|
154 | > > EOF | |||
|
155 | > | |||
|
156 | > $ cat >> \$HGRCPATH <<EOF | |||
|
157 | > > # appended | |||
|
158 | > > [extensions] | |||
|
159 | > > rebase =! | |||
|
160 | > > EOF | |||
|
161 | > | |||
|
162 | > NO_CHECK_EOF limit mark makes parsing ignore corresponded fragment | |||
|
163 | > (this is useful to use bad code intentionally) | |||
|
164 | > | |||
|
165 | > $ cat > .hg/hgrc <<NO_CHECK_EOF | |||
|
166 | > > # this local configuration should be ignored at detection | |||
|
167 | > > [ui] | |||
|
168 | > > username = foo bar | |||
|
169 | > > NO_CHECK_EOF | |||
|
170 | > | |||
|
171 | > $ cat > \$HGRCPATH <<NO_CHECK_EOF | |||
|
172 | > > # this global configuration should be ignored at detection | |||
|
173 | > > [extensions] | |||
|
174 | > > foobar = | |||
|
175 | > > NO_CHECK_EOF | |||
|
176 | > NO_CHECK_EOF | |||
|
177 | <stdin>:2: .hg/hgrc starts | |||
|
178 | |[ui] | |||
|
179 | |verbose = true | |||
|
180 | | | |||
|
181 | |# end of local configuration | |||
|
182 | <stdin>:6: .hg/hgrc ends | |||
|
183 | <stdin>:9: $HGRCPATH starts | |||
|
184 | |[extensions] | |||
|
185 | |rebase = | |||
|
186 | |# end of global configuration | |||
|
187 | <stdin>:12: $HGRCPATH ends | |||
|
188 | <stdin>:15: $HGRCPATH starts | |||
|
189 | |# appended | |||
|
190 | |[extensions] | |||
|
191 | |rebase =! | |||
|
192 | <stdin>:18: $HGRCPATH ends |
General Comments 0
You need to be logged in to leave comments.
Login now