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,
224
self._check_output(output, actual_output, test_case)
225
self._check_output(error, actual_error, test_case)
226
if retcode and not error and actual_error:
227
test_case.fail('In \n\t%s\nUnexpected error: %s'
228
% (' '.join(cmd), actual_error))
229
return retcode, actual_output, actual_error
231
def _check_output(self, expected, actual, test_case):
233
# Specifying None means: any output is accepted
236
test_case.fail('We expected output: %r, but found None'
238
matching = self.output_checker.check_output(
239
expected, actual, self.check_options)
241
# Note that we can't use output_checker.output_difference() here
242
# because... the API is broken ('expected' must be a doctest
243
# specific object of which a 'want' attribute will be our
244
# 'expected' parameter. So we just fallback to our good old
245
# assertEqualDiff since we know there *are* differences and the
246
# output should be decently readable.
248
# As a special case, we allow output that's missing a final
249
# newline to match an expected string that does have one, so that
250
# we can match a prompt printed on one line, then input given on
252
if expected == actual + '\n':
255
test_case.assertEqualDiff(expected, actual)
257
def _pre_process_args(self, args):
260
# Strip the simple and double quotes since we don't care about
261
# them. We leave the backquotes in place though since they have a
262
# different semantic.
263
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
266
if glob.has_magic(arg):
267
matches = glob.glob(arg)
269
# We care more about order stability than performance
277
def _read_input(self, input, in_name):
278
if in_name is not None:
279
infile = open(in_name, 'rb')
281
# Command redirection takes precedence over provided input
282
input = infile.read()
287
def _write_output(self, output, out_name, out_mode):
288
if out_name is not None:
289
outfile = open(out_name, out_mode)
291
outfile.write(output)
297
def do_bzr(self, test_case, input, args):
298
retcode, out, err = test_case._run_bzr_core(
299
args, retcode=None, encoding=None, stdin=input, working_dir=None)
300
return retcode, out, err
302
def do_cat(self, test_case, input, args):
303
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
304
if args and in_name is not None:
305
raise SyntaxError('Specify a file OR use redirection')
313
for in_name in input_names:
315
inputs.append(self._read_input(None, in_name))
317
# Some filenames are illegal on Windows and generate EINVAL
318
# rather than just saying the filename doesn't exist
319
if e.errno in (errno.ENOENT, errno.EINVAL):
321
'%s: No such file or directory\n' % (in_name,))
323
# Basically cat copy input to output
324
output = ''.join(inputs)
325
# Handle output redirections
327
output = self._write_output(output, out_name, out_mode)
329
# If out_name cannot be created, we may get 'ENOENT', however if
330
# out_name is something like '', we can get EINVAL
331
if e.errno in (errno.ENOENT, errno.EINVAL):
332
return 1, None, '%s: No such file or directory\n' % (out_name,)
334
return 0, output, None
336
def do_echo(self, test_case, input, args):
337
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
339
raise SyntaxError('echo doesn\'t read from stdin')
341
input = ' '.join(args)
342
# Always append a \n'
346
# Handle output redirections
348
output = self._write_output(output, out_name, out_mode)
350
if e.errno in (errno.ENOENT, errno.EINVAL):
351
return 1, None, '%s: No such file or directory\n' % (out_name,)
353
return 0, output, None
355
def _get_jail_root(self, test_case):
356
return test_case.test_dir
358
def _ensure_in_jail(self, test_case, path):
359
jail_root = self._get_jail_root(test_case)
360
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
361
raise ValueError('%s is not inside %s' % (path, jail_root))
363
def do_cd(self, test_case, input, args):
365
raise SyntaxError('Usage: cd [dir]')
368
self._ensure_in_jail(test_case, d)
370
# The test "home" directory is the root of its jail
371
d = self._get_jail_root(test_case)
375
def do_mkdir(self, test_case, input, args):
376
if not args or len(args) != 1:
377
raise SyntaxError('Usage: mkdir dir')
379
self._ensure_in_jail(test_case, d)
383
def do_rm(self, test_case, input, args):
386
def error(msg, path):
387
return "rm: cannot remove '%s': %s\n" % (path, msg)
389
force, recursive = False, False
391
if args and args[0][0] == '-':
392
opts = args.pop(0)[1:]
395
opts = opts.replace('f', '', 1)
398
opts = opts.replace('r', '', 1)
400
raise SyntaxError('Usage: rm [-fr] path+')
402
self._ensure_in_jail(test_case, p)
403
# FIXME: Should we put that in osutils ?
407
# Various OSes raises different exceptions (linux: EISDIR,
408
# win32: EACCES, OSX: EPERM) when invoked on a directory
409
if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
413
err = error('Is a directory', p)
415
elif e.errno == errno.ENOENT:
417
err = error('No such file or directory', p)
425
return retcode, None, err
427
def do_mv(self, test_case, input, args):
429
def error(msg, src, dst):
430
return "mv: cannot move %s to %s: %s\n" % (src, dst, msg)
432
if not args or len(args) != 2:
433
raise SyntaxError("Usage: mv path1 path2")
437
if os.path.isdir(dst):
438
real_dst = os.path.join(dst, os.path.basename(src))
439
os.rename(src, real_dst)
441
if e.errno == errno.ENOENT:
442
err = error('No such file or directory', src, dst)
449
return retcode, None, err
453
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
454
"""Helper class to experiment shell-like test and memory fs.
456
This not intended to be used outside of experiments in implementing memoy
457
based file systems and evolving bzr so that test can use only memory based
462
super(TestCaseWithMemoryTransportAndScript, self).setUp()
463
self.script_runner = ScriptRunner()
465
def run_script(self, script):
466
return self.script_runner.run_script(self, script)
468
def run_command(self, cmd, input, output, error):
469
return self.script_runner.run_command(self, cmd, input, output, error)
472
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
473
"""Helper class to quickly define shell-like tests.
477
from bzrlib.tests import script
480
class TestBug(script.TestCaseWithTransportAndScript):
482
def test_bug_nnnnn(self):
491
super(TestCaseWithTransportAndScript, self).setUp()
492
self.script_runner = ScriptRunner()
494
def run_script(self, script):
495
return self.script_runner.run_script(self, script)
497
def run_command(self, cmd, input, output, error):
498
return self.script_runner.run_command(self, cmd, input, output, error)
501
def run_script(test_case, script_string):
502
"""Run the given script within a testcase"""
503
return ScriptRunner().run_script(test_case, script_string)