1
# Copyright (C) 2009, 2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Shell-like test scripts.
19
See developers/testing.html for more explanations.
28
from cStringIO import StringIO
37
"""Split a command line respecting quotes."""
38
scanner = shlex.shlex(s)
39
scanner.quotes = '\'"`'
40
scanner.whitespace_split = True
41
for t in list(scanner):
45
def _script_to_commands(text, file_name=None):
46
"""Turn a script into a list of commands with their associated IOs.
48
Each command appears on a line by itself starting with '$ '. It can be
49
associated with an input that will feed it and an expected output.
51
Comments starts with '#' until the end of line.
52
Empty lines are ignored.
54
Input and output are full lines terminated by a '\n'.
56
Input lines start with '<'.
57
Output lines start with nothing.
58
Error lines start with '2>'.
60
:return: A sequence of ([args], input, output, errors), where the args are
61
split in to words, and the input, output, and errors are just strings,
62
typically containing newlines.
67
def add_command(cmd, input, output, error):
70
input = ''.join(input)
71
if output is not None:
72
output = ''.join(output)
74
error = ''.join(error)
75
commands.append((cmd, input, output, error))
80
input, output, error = None, None, None
81
text = textwrap.dedent(text)
82
lines = text.split('\n')
83
# to make use of triple-quoted strings easier, we ignore a blank line
84
# right at the start and right at the end; the rest are meaningful
85
if lines and lines[0] == '':
87
if lines and lines[-1] == '':
91
# Keep a copy for error reporting
93
comment = line.find('#')
96
# NB: this syntax means comments are allowed inside output, which
98
line = line[0:comment]
102
if line.startswith('$'):
103
# Time to output the current command
104
add_command(cmd_cur, input, output, error)
105
# And start a new one
106
cmd_cur = list(split(line[1:]))
108
input, output, error = None, None, None
109
elif line.startswith('<'):
112
raise SyntaxError('No command for that input',
113
(file_name, lineno, 1, orig))
115
input.append(line[1:] + '\n')
116
elif line.startswith('2>'):
119
raise SyntaxError('No command for that error',
120
(file_name, lineno, 1, orig))
122
error.append(line[2:] + '\n')
124
# can happen if the first line is not recognized as a command, eg
125
# if the prompt has leading whitespace
128
raise SyntaxError('No command for line %r' % (line,),
129
(file_name, lineno, 1, orig))
131
output.append(line + '\n')
132
# Add the last seen command
133
add_command(cmd_cur, input, output, error)
137
def _scan_redirection_options(args):
138
"""Recognize and process input and output redirections.
140
:param args: The command line arguments
142
:return: A tuple containing:
143
- The file name redirected from or None
144
- The file name redirected to or None
145
- The mode to open the output file or None
146
- The reamining arguments
148
def redirected_file_name(direction, name, args):
153
# We leave the error handling to higher levels, an empty name
160
out_name, out_mode = None, None
163
if arg.startswith('<'):
164
in_name = redirected_file_name('<', arg[1:], args)
165
elif arg.startswith('>>'):
166
out_name = redirected_file_name('>>', arg[2:], args)
168
elif arg.startswith('>',):
169
out_name = redirected_file_name('>', arg[1:], args)
172
remaining.append(arg)
173
return in_name, out_name, out_mode, remaining
176
class ScriptRunner(object):
177
"""Run a shell-like script from a test.
181
from bzrlib.tests import script
185
def test_bug_nnnnn(self):
186
sr = script.ScriptRunner()
187
sr.run_script(self, '''
195
self.output_checker = doctest.OutputChecker()
196
self.check_options = doctest.ELLIPSIS
198
def run_script(self, test_case, text):
199
"""Run a shell-like script as a test.
201
:param test_case: A TestCase instance that should provide the fail(),
202
assertEqualDiff and _run_bzr_core() methods as well as a 'test_dir'
203
attribute used as a jail root.
205
:param text: A shell-like script (see _script_to_commands for syntax).
207
for cmd, input, output, error in _script_to_commands(text):
208
self.run_command(test_case, cmd, input, output, error)
210
def run_command(self, test_case, cmd, input, output, error):
211
mname = 'do_' + cmd[0]
212
method = getattr(self, mname, None)
214
raise SyntaxError('Command not found "%s"' % (cmd[0],),
215
None, 1, ' '.join(cmd))
219
str_input = ''.join(input)
220
args = list(self._pre_process_args(cmd[1:]))
221
retcode, actual_output, actual_error = method(test_case,
225
self._check_output(output, actual_output, test_case)
226
except AssertionError, e:
227
raise AssertionError(str(e) + " in stdout of command %s" % cmd)
229
self._check_output(error, actual_error, test_case)
230
except AssertionError, e:
231
raise AssertionError(str(e) +
232
" in stderr of running command %s" % cmd)
233
if retcode and not error and actual_error:
234
test_case.fail('In \n\t%s\nUnexpected error: %s'
235
% (' '.join(cmd), actual_error))
236
return retcode, actual_output, actual_error
238
def _check_output(self, expected, actual, test_case):
242
elif expected == '...\n':
245
test_case.fail('expected output: %r, but found nothing'
247
expected = expected or ''
248
matching = self.output_checker.check_output(
249
expected, actual, self.check_options)
251
# Note that we can't use output_checker.output_difference() here
252
# because... the API is broken ('expected' must be a doctest
253
# specific object of which a 'want' attribute will be our
254
# 'expected' parameter. So we just fallback to our good old
255
# assertEqualDiff since we know there *are* differences and the
256
# output should be decently readable.
258
# As a special case, we allow output that's missing a final
259
# newline to match an expected string that does have one, so that
260
# we can match a prompt printed on one line, then input given on
262
if expected == actual + '\n':
265
test_case.assertEqualDiff(expected, actual)
267
def _pre_process_args(self, args):
270
# Strip the simple and double quotes since we don't care about
271
# them. We leave the backquotes in place though since they have a
272
# different semantic.
273
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
276
if glob.has_magic(arg):
277
matches = glob.glob(arg)
279
# We care more about order stability than performance
287
def _read_input(self, input, in_name):
288
if in_name is not None:
289
infile = open(in_name, 'rb')
291
# Command redirection takes precedence over provided input
292
input = infile.read()
297
def _write_output(self, output, out_name, out_mode):
298
if out_name is not None:
299
outfile = open(out_name, out_mode)
301
outfile.write(output)
307
def do_bzr(self, test_case, input, args):
308
retcode, out, err = test_case._run_bzr_core(
309
args, retcode=None, encoding=None, stdin=input, working_dir=None)
310
return retcode, out, err
312
def do_cat(self, test_case, input, args):
313
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
314
if args and in_name is not None:
315
raise SyntaxError('Specify a file OR use redirection')
323
for in_name in input_names:
325
inputs.append(self._read_input(None, in_name))
327
# Some filenames are illegal on Windows and generate EINVAL
328
# rather than just saying the filename doesn't exist
329
if e.errno in (errno.ENOENT, errno.EINVAL):
331
'%s: No such file or directory\n' % (in_name,))
333
# Basically cat copy input to output
334
output = ''.join(inputs)
335
# Handle output redirections
337
output = self._write_output(output, out_name, out_mode)
339
# If out_name cannot be created, we may get 'ENOENT', however if
340
# out_name is something like '', we can get EINVAL
341
if e.errno in (errno.ENOENT, errno.EINVAL):
342
return 1, None, '%s: No such file or directory\n' % (out_name,)
344
return 0, output, None
346
def do_echo(self, test_case, input, args):
347
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
349
raise SyntaxError('echo doesn\'t read from stdin')
351
input = ' '.join(args)
352
# Always append a \n'
356
# Handle output redirections
358
output = self._write_output(output, out_name, out_mode)
360
if e.errno in (errno.ENOENT, errno.EINVAL):
361
return 1, None, '%s: No such file or directory\n' % (out_name,)
363
return 0, output, None
365
def _get_jail_root(self, test_case):
366
return test_case.test_dir
368
def _ensure_in_jail(self, test_case, path):
369
jail_root = self._get_jail_root(test_case)
370
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
371
raise ValueError('%s is not inside %s' % (path, jail_root))
373
def do_cd(self, test_case, input, args):
375
raise SyntaxError('Usage: cd [dir]')
378
self._ensure_in_jail(test_case, d)
380
# The test "home" directory is the root of its jail
381
d = self._get_jail_root(test_case)
385
def do_mkdir(self, test_case, input, args):
386
if not args or len(args) != 1:
387
raise SyntaxError('Usage: mkdir dir')
389
self._ensure_in_jail(test_case, d)
393
def do_rm(self, test_case, input, args):
396
def error(msg, path):
397
return "rm: cannot remove '%s': %s\n" % (path, msg)
399
force, recursive = False, False
401
if args and args[0][0] == '-':
402
opts = args.pop(0)[1:]
405
opts = opts.replace('f', '', 1)
408
opts = opts.replace('r', '', 1)
410
raise SyntaxError('Usage: rm [-fr] path+')
412
self._ensure_in_jail(test_case, p)
413
# FIXME: Should we put that in osutils ?
417
# Various OSes raises different exceptions (linux: EISDIR,
418
# win32: EACCES, OSX: EPERM) when invoked on a directory
419
if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
423
err = error('Is a directory', p)
425
elif e.errno == errno.ENOENT:
427
err = error('No such file or directory', p)
435
return retcode, None, err
437
def do_mv(self, test_case, input, args):
439
def error(msg, src, dst):
440
return "mv: cannot move %s to %s: %s\n" % (src, dst, msg)
442
if not args or len(args) != 2:
443
raise SyntaxError("Usage: mv path1 path2")
447
if os.path.isdir(dst):
448
real_dst = os.path.join(dst, os.path.basename(src))
449
os.rename(src, real_dst)
451
if e.errno == errno.ENOENT:
452
err = error('No such file or directory', src, dst)
459
return retcode, None, err
463
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
464
"""Helper class to experiment shell-like test and memory fs.
466
This not intended to be used outside of experiments in implementing memoy
467
based file systems and evolving bzr so that test can use only memory based
472
super(TestCaseWithMemoryTransportAndScript, self).setUp()
473
self.script_runner = ScriptRunner()
475
def run_script(self, script):
476
return self.script_runner.run_script(self, script)
478
def run_command(self, cmd, input, output, error):
479
return self.script_runner.run_command(self, cmd, input, output, error)
482
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
483
"""Helper class to quickly define shell-like tests.
487
from bzrlib.tests import script
490
class TestBug(script.TestCaseWithTransportAndScript):
492
def test_bug_nnnnn(self):
501
super(TestCaseWithTransportAndScript, self).setUp()
502
self.script_runner = ScriptRunner()
504
def run_script(self, script):
505
return self.script_runner.run_script(self, script)
507
def run_command(self, cmd, input, output, error):
508
return self.script_runner.run_command(self, cmd, input, output, error)
511
def run_script(test_case, script_string):
512
"""Run the given script within a testcase"""
513
return ScriptRunner().run_script(test_case, script_string)