1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
|
# Copyright (C) 2009 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""Shell-like test scripts.
This allows users to write tests in a syntax very close to a shell session,
using a restricted and limited set of commands that should be enough to mimic
most of the behaviours.
A script is a set of commands, each command is composed of:
- one mandatory command line,
- one optional set of input lines to feed the command,
- one optional set of output expected lines,
- one optional set of error expected lines.
The optional lines starts with a special string (mnemonic: shell redirection):
- '<' for input,
- '>' for output,
- '2>' for errors,
The execution stops as soon as an expected output or an expected error is not
matched.
When no output is specified, any ouput from the command is accepted
and let the execution continue.
If an error occurs and no expected error is specified, the execution stops.
The matching is done on a full string comparison basis unless '...' is used, in
which case expected output/errors can be lees precise.
Examples:
The following will succeeds only if 'bzr add' outputs 'adding file'.
bzr add file
>adding file
If you want the command to succeed for any output, just use:
bzr add file
The following will stop with an error:
bzr not-a-command
If you want it to succeed, use:
bzr not-a-command
2> bzr: ERROR: unknown command "not-a-command"
You can use ellipsis (...) to replace any piece of text you don't want to be
matched exactly:
bzr branch not-a-branch
2>bzr: ERROR: Not a branch...not-a-branch/".
This can be used to ignore entire lines too:
cat
<first line
<second line
<third line
<fourth line
<last line
>first line
>...
>last line
You can check the content of a file with cat:
cat <file
>expected content
You can also check the existence of a file with cat, the following will fail if
the file doesn't exist:
cat file
"""
import doctest
import os
import shlex
from cStringIO import StringIO
from bzrlib import (
osutils,
tests,
)
def split(s):
"""Split a command line respecting quotes."""
scanner = shlex.shlex(s)
scanner.quotes = '\'"`'
scanner.whitespace_split = True
for t in list(scanner):
# Strip the simple and double quotes since we don't care about them.
# We leave the backquotes in place though since they have a different
# semantic.
if t[0] in ('"', "'") and t[0] == t[-1]:
yield t[1:-1]
else:
yield t
def _script_to_commands(text, file_name=None):
"""Turn a script into a list of commands with their associated IOs.
Each command appears on a line by itself. It can be associated with an
input that will feed it and an expected output.
Comments starts with '#' until the end of line.
Empty lines are ignored.
Input and output are full lines terminated by a '\n'.
Input lines start with '<'.
Output lines start with '>'.
Error lines start with '2>'.
"""
commands = []
def add_command(cmd, input, output, error):
if cmd is not None:
if input is not None:
input = ''.join(input)
if output is not None:
output = ''.join(output)
if error is not None:
error = ''.join(error)
commands.append((cmd, input, output, error))
cmd_cur = None
cmd_line = 1
lineno = 0
input, output, error = None, None, None
for line in text.split('\n'):
lineno += 1
# Keep a copy for error reporting
orig = line
comment = line.find('#')
if comment >= 0:
# Delete comments
line = line[0:comment]
line = line.rstrip()
if line == '':
# Ignore empty lines
continue
if line.startswith('<'):
if input is None:
if cmd_cur is None:
raise SyntaxError('No command for that input',
(file_name, lineno, 1, orig))
input = []
input.append(line[1:] + '\n')
continue
elif line.startswith('>'):
if output is None:
if cmd_cur is None:
raise SyntaxError('No command for that output',
(file_name, lineno, 1, orig))
output = []
output.append(line[1:] + '\n')
continue
elif line.startswith('2>'):
if error is None:
if cmd_cur is None:
raise SyntaxError('No command for that error',
(file_name, lineno, 1, orig))
error = []
error.append(line[2:] + '\n')
continue
else:
# Time to output the current command
add_command(cmd_cur, input, output, error)
# And start a new one
cmd_cur = list(split(line))
cmd_line = lineno
input, output, error = None, None, None
# Add the last seen command
add_command(cmd_cur, input, output, error)
return commands
def _scan_redirection_options(args):
"""Recognize and process input and output redirections.
:param args: The command line arguments
:return: A tuple containing:
- The file name redirected from or None
- The file name redirected to or None
- The mode to open the output file or None
- The reamining arguments
"""
remaining = []
in_name = None
out_name, out_mode = None, None
for arg in args:
if arg.startswith('<'):
in_name = arg[1:]
elif arg.startswith('>>'):
out_name = arg[2:]
out_mode = 'ab+'
elif arg.startswith('>'):
out_name = arg[1:]
out_mode = 'wb+'
else:
remaining.append(arg)
return in_name, out_name, out_mode, remaining
class ScriptRunner(object):
def __init__(self, test_case):
self.test_case = test_case
self.output_checker = doctest.OutputChecker()
self.check_options = doctest.ELLIPSIS
def run_script(self, text):
for cmd, input, output, error in _script_to_commands(text):
out, err = self.run_command(cmd, input, output, error)
def _check_output(self, expected, actual):
if expected is None:
# Specifying None means: any output is accepted
return
if actual is None:
self.test_case.fail('Unexpected: %s' % actual)
matching = self.output_checker.check_output(
expected, actual, self.check_options)
if not matching:
# Note that we can't use output_checker.output_difference() here
# because... the API is boken (expected must be a doctest specific
# object of whicha 'want' attribute will be our 'expected'
# parameter. So we just fallbacl to our good old assertEqualDiff
# since we know there are differences and the output should be
# decently readable.
self.test_case.assertEqualDiff(expected, actual)
def run_command(self, cmd, input, output, error):
mname = 'do_' + cmd[0]
method = getattr(self, mname, None)
if method is None:
raise SyntaxError('Command not found "%s"' % (cmd[0],),
None, 1, ' '.join(cmd))
if input is None:
str_input = ''
else:
str_input = ''.join(input)
actual_output, actual_error = method(str_input, cmd[1:])
self._check_output(output, actual_output)
self._check_output(error, actual_error)
if not error and actual_error:
self.test_case.fail('Unexpected error: %s' % actual_error)
return actual_output, actual_error
def _read_input(self, input, in_name):
if in_name is not None:
infile = open(in_name, 'rb')
try:
# Command redirection takes precedence over provided input
input = infile.read()
finally:
infile.close()
return input
def _write_output(self, output, out_name, out_mode):
if out_name is not None:
outfile = open(out_name, out_mode)
try:
outfile.write(output)
finally:
outfile.close()
output = None
return output
def do_bzr(self, input, args):
out, err = self.test_case._run_bzr_core(
args, retcode=None, encoding=None, stdin=input, working_dir=None)
return out, err
def do_cat(self, input, args):
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
if len(args) > 1:
raise SyntaxError('Usage: cat [file1]')
if args:
if in_name is not None:
raise SyntaxError('Specify a file OR use redirection')
in_name = args[0]
input = self._read_input(input, in_name)
# Basically cat copy input to output
output = input
# Handle output redirections
output = self._write_output(output, out_name, out_mode)
return output, None
def do_echo(self, input, args):
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
if input and args:
raise SyntaxError('Specify parameters OR use redirection')
if args:
input = ''.join(args)
input = self._read_input(input, in_name)
# Always append a \n'
input += '\n'
# Process output
output = input
# Handle output redirections
output = self._write_output(output, out_name, out_mode)
return output, None
def _ensure_in_jail(self, path):
jail_root = self.test_case.get_jail_root()
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
raise ValueError('%s is not inside %s' % (path, jail_root))
def do_cd(self, input, args):
if len(args) > 1:
raise SyntaxError('Usage: cd [dir]')
if len(args) == 1:
d = args[0]
self._ensure_in_jail(d)
else:
d = self.test_case.get_jail_root()
os.chdir(d)
return None, None
def do_mkdir(self, input, args):
if not args or len(args) != 1:
raise SyntaxError('Usage: mkdir dir')
d = args[0]
self._ensure_in_jail(d)
os.mkdir(d)
return None, None
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
def setUp(self):
super(TestCaseWithMemoryTransportAndScript, self).setUp()
self.script_runner = ScriptRunner(self)
# Break the circular dependency
def break_dependency():
self.script_runner = None
self.addCleanup(break_dependency)
def get_jail_root(self):
raise NotImplementedError(self.get_jail_root)
def run_script(self, script):
return self.script_runner.run_script(script)
def run_command(self, cmd, input, output, error):
return self.script_runner.run_command(cmd, input, output, error)
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
def setUp(self):
super(TestCaseWithTransportAndScript, self).setUp()
self.script_runner = ScriptRunner(self)
# Break the circular dependency
def break_dependency():
self.script_runner = None
self.addCleanup(break_dependency)
def get_jail_root(self):
return self.test_dir
def run_script(self, script):
return self.script_runner.run_script(script)
def run_command(self, cmd, input, output, error):
return self.script_runner.run_command(cmd, input, output, error)
|