1
# Copyright (C) 2009, 2010, 2011 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.
36
"""Split a command line respecting quotes."""
37
scanner = shlex.shlex(s)
38
scanner.quotes = '\'"`'
39
scanner.whitespace_split = True
40
for t in list(scanner):
44
def _script_to_commands(text, file_name=None):
45
"""Turn a script into a list of commands with their associated IOs.
47
Each command appears on a line by itself starting with '$ '. It can be
48
associated with an input that will feed it and an expected output.
50
Comments starts with '#' until the end of line.
51
Empty lines are ignored.
53
Input and output are full lines terminated by a '\n'.
55
Input lines start with '<'.
56
Output lines start with nothing.
57
Error lines start with '2>'.
59
:return: A sequence of ([args], input, output, errors), where the args are
60
split in to words, and the input, output, and errors are just strings,
61
typically containing newlines.
66
def add_command(cmd, input, output, error):
69
input = ''.join(input)
70
if output is not None:
71
output = ''.join(output)
73
error = ''.join(error)
74
commands.append((cmd, input, output, error))
79
input, output, error = None, None, None
80
text = textwrap.dedent(text)
81
lines = text.split('\n')
82
# to make use of triple-quoted strings easier, we ignore a blank line
83
# right at the start and right at the end; the rest are meaningful
84
if lines and lines[0] == '':
86
if lines and lines[-1] == '':
90
# Keep a copy for error reporting
92
comment = line.find('#')
95
# NB: this syntax means comments are allowed inside output, which
97
line = line[0:comment]
101
if line.startswith('$'):
102
# Time to output the current command
103
add_command(cmd_cur, input, output, error)
104
# And start a new one
105
cmd_cur = list(split(line[1:]))
107
input, output, error = None, None, None
108
elif line.startswith('<'):
111
raise SyntaxError('No command for that input',
112
(file_name, lineno, 1, orig))
114
input.append(line[1:] + '\n')
115
elif line.startswith('2>'):
118
raise SyntaxError('No command for that error',
119
(file_name, lineno, 1, orig))
121
error.append(line[2:] + '\n')
123
# can happen if the first line is not recognized as a command, eg
124
# if the prompt has leading whitespace
127
raise SyntaxError('No command for line %r' % (line,),
128
(file_name, lineno, 1, orig))
130
output.append(line + '\n')
131
# Add the last seen command
132
add_command(cmd_cur, input, output, error)
136
def _scan_redirection_options(args):
137
"""Recognize and process input and output redirections.
139
:param args: The command line arguments
141
:return: A tuple containing:
142
- The file name redirected from or None
143
- The file name redirected to or None
144
- The mode to open the output file or None
145
- The reamining arguments
147
def redirected_file_name(direction, name, args):
152
# We leave the error handling to higher levels, an empty name
159
out_name, out_mode = None, None
162
if arg.startswith('<'):
163
in_name = redirected_file_name('<', arg[1:], args)
164
elif arg.startswith('>>'):
165
out_name = redirected_file_name('>>', arg[2:], args)
167
elif arg.startswith('>',):
168
out_name = redirected_file_name('>', arg[1:], args)
171
remaining.append(arg)
172
return in_name, out_name, out_mode, remaining
175
class ScriptRunner(object):
176
"""Run a shell-like script from a test.
180
from bzrlib.tests import script
184
def test_bug_nnnnn(self):
185
sr = script.ScriptRunner()
186
sr.run_script(self, '''
194
self.output_checker = doctest.OutputChecker()
195
self.check_options = doctest.ELLIPSIS
197
def run_script(self, test_case, text, null_output_matches_anything=False):
198
"""Run a shell-like script as a test.
200
:param test_case: A TestCase instance that should provide the fail(),
201
assertEqualDiff and _run_bzr_core() methods as well as a 'test_dir'
202
attribute used as a jail root.
204
:param text: A shell-like script (see _script_to_commands for syntax).
206
:param null_output_matches_anything: For commands with no specified
207
output, ignore any output that does happen, including output on
210
self.null_output_matches_anything = null_output_matches_anything
211
for cmd, input, output, error in _script_to_commands(text):
212
self.run_command(test_case, cmd, input, output, error)
214
def run_command(self, test_case, cmd, input, output, error):
215
mname = 'do_' + cmd[0]
216
method = getattr(self, mname, None)
218
raise SyntaxError('Command not found "%s"' % (cmd[0],),
219
(None, 1, 1, ' '.join(cmd)))
223
str_input = ''.join(input)
224
args = list(self._pre_process_args(cmd[1:]))
225
retcode, actual_output, actual_error = method(test_case,
229
self._check_output(output, actual_output, test_case)
230
except AssertionError, e:
231
raise AssertionError(str(e) + " in stdout of command %s" % cmd)
233
self._check_output(error, actual_error, test_case)
234
except AssertionError, e:
235
raise AssertionError(str(e) +
236
" in stderr of running command %s" % cmd)
237
if retcode and not error and actual_error:
238
test_case.fail('In \n\t%s\nUnexpected error: %s'
239
% (' '.join(cmd), actual_error))
240
return retcode, actual_output, actual_error
242
def _check_output(self, expected, actual, test_case):
246
elif expected == '...\n':
249
test_case.fail('expected output: %r, but found nothing'
252
null_output_matches_anything = getattr(
253
self, 'null_output_matches_anything', False)
254
if null_output_matches_anything and expected is None:
257
expected = expected or ''
258
matching = self.output_checker.check_output(
259
expected, actual, self.check_options)
261
# Note that we can't use output_checker.output_difference() here
262
# because... the API is broken ('expected' must be a doctest
263
# specific object of which a 'want' attribute will be our
264
# 'expected' parameter. So we just fallback to our good old
265
# assertEqualDiff since we know there *are* differences and the
266
# output should be decently readable.
268
# As a special case, we allow output that's missing a final
269
# newline to match an expected string that does have one, so that
270
# we can match a prompt printed on one line, then input given on
272
if expected == actual + '\n':
275
test_case.assertEqualDiff(expected, actual)
277
def _pre_process_args(self, args):
280
# Strip the simple and double quotes since we don't care about
281
# them. We leave the backquotes in place though since they have a
282
# different semantic.
283
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
286
if glob.has_magic(arg):
287
matches = glob.glob(arg)
289
# We care more about order stability than performance
297
def _read_input(self, input, in_name):
298
if in_name is not None:
299
infile = open(in_name, 'rb')
301
# Command redirection takes precedence over provided input
302
input = infile.read()
307
def _write_output(self, output, out_name, out_mode):
308
if out_name is not None:
309
outfile = open(out_name, out_mode)
311
outfile.write(output)
317
def do_bzr(self, test_case, input, args):
318
retcode, out, err = test_case._run_bzr_core(
319
args, retcode=None, encoding=None, stdin=input, working_dir=None)
320
return retcode, out, err
322
def do_cat(self, test_case, input, args):
323
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
324
if args and in_name is not None:
325
raise SyntaxError('Specify a file OR use redirection')
333
for in_name in input_names:
335
inputs.append(self._read_input(None, in_name))
337
# Some filenames are illegal on Windows and generate EINVAL
338
# rather than just saying the filename doesn't exist
339
if e.errno in (errno.ENOENT, errno.EINVAL):
341
'%s: No such file or directory\n' % (in_name,))
343
# Basically cat copy input to output
344
output = ''.join(inputs)
345
# Handle output redirections
347
output = self._write_output(output, out_name, out_mode)
349
# If out_name cannot be created, we may get 'ENOENT', however if
350
# out_name is something like '', we can get EINVAL
351
if e.errno in (errno.ENOENT, errno.EINVAL):
352
return 1, None, '%s: No such file or directory\n' % (out_name,)
354
return 0, output, None
356
def do_echo(self, test_case, input, args):
357
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
359
raise SyntaxError('echo doesn\'t read from stdin')
361
input = ' '.join(args)
362
# Always append a \n'
366
# Handle output redirections
368
output = self._write_output(output, out_name, out_mode)
370
if e.errno in (errno.ENOENT, errno.EINVAL):
371
return 1, None, '%s: No such file or directory\n' % (out_name,)
373
return 0, output, None
375
def _get_jail_root(self, test_case):
376
return test_case.test_dir
378
def _ensure_in_jail(self, test_case, path):
379
jail_root = self._get_jail_root(test_case)
380
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
381
raise ValueError('%s is not inside %s' % (path, jail_root))
383
def do_cd(self, test_case, input, args):
385
raise SyntaxError('Usage: cd [dir]')
388
self._ensure_in_jail(test_case, d)
390
# The test "home" directory is the root of its jail
391
d = self._get_jail_root(test_case)
395
def do_mkdir(self, test_case, input, args):
396
if not args or len(args) != 1:
397
raise SyntaxError('Usage: mkdir dir')
399
self._ensure_in_jail(test_case, d)
403
def do_rm(self, test_case, input, args):
406
def error(msg, path):
407
return "rm: cannot remove '%s': %s\n" % (path, msg)
409
force, recursive = False, False
411
if args and args[0][0] == '-':
412
opts = args.pop(0)[1:]
415
opts = opts.replace('f', '', 1)
418
opts = opts.replace('r', '', 1)
420
raise SyntaxError('Usage: rm [-fr] path+')
422
self._ensure_in_jail(test_case, p)
423
# FIXME: Should we put that in osutils ?
427
# Various OSes raises different exceptions (linux: EISDIR,
428
# win32: EACCES, OSX: EPERM) when invoked on a directory
429
if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
433
err = error('Is a directory', p)
435
elif e.errno == errno.ENOENT:
437
err = error('No such file or directory', p)
445
return retcode, None, err
447
def do_mv(self, test_case, input, args):
449
def error(msg, src, dst):
450
return "mv: cannot move %s to %s: %s\n" % (src, dst, msg)
452
if not args or len(args) != 2:
453
raise SyntaxError("Usage: mv path1 path2")
457
if os.path.isdir(dst):
458
real_dst = os.path.join(dst, os.path.basename(src))
459
os.rename(src, real_dst)
461
if e.errno == errno.ENOENT:
462
err = error('No such file or directory', src, dst)
469
return retcode, None, err
473
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
474
"""Helper class to experiment shell-like test and memory fs.
476
This not intended to be used outside of experiments in implementing memoy
477
based file systems and evolving bzr so that test can use only memory based
482
super(TestCaseWithMemoryTransportAndScript, self).setUp()
483
self.script_runner = ScriptRunner()
485
def run_script(self, script, null_output_matches_anything=False):
486
return self.script_runner.run_script(self, script,
487
null_output_matches_anything=null_output_matches_anything)
489
def run_command(self, cmd, input, output, error):
490
return self.script_runner.run_command(self, cmd, input, output, error)
493
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
494
"""Helper class to quickly define shell-like tests.
498
from bzrlib.tests import script
501
class TestBug(script.TestCaseWithTransportAndScript):
503
def test_bug_nnnnn(self):
512
super(TestCaseWithTransportAndScript, self).setUp()
513
self.script_runner = ScriptRunner()
515
def run_script(self, script, null_output_matches_anything=False):
516
return self.script_runner.run_script(self, script,
517
null_output_matches_anything=null_output_matches_anything)
519
def run_command(self, cmd, input, output, error):
520
return self.script_runner.run_command(self, cmd, input, output, error)
523
def run_script(test_case, script_string, null_output_matches_anything=False):
524
"""Run the given script within a testcase"""
525
return ScriptRunner().run_script(test_case, script_string,
526
null_output_matches_anything=null_output_matches_anything)