1
# Copyright (C) 2009 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.
27
from cStringIO import StringIO
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>'.
62
def add_command(cmd, input, output, error):
65
input = ''.join(input)
66
if output is not None:
67
output = ''.join(output)
69
error = ''.join(error)
70
commands.append((cmd, input, output, error))
75
input, output, error = None, None, None
76
for line in text.split('\n'):
78
# Keep a copy for error reporting
80
comment = line.find('#')
83
line = line[0:comment]
88
if line.startswith('$'):
89
# Time to output the current command
90
add_command(cmd_cur, input, output, error)
92
cmd_cur = list(split(line[1:]))
94
input, output, error = None, None, None
95
elif line.startswith('<'):
98
raise SyntaxError('No command for that input',
99
(file_name, lineno, 1, orig))
101
input.append(line[1:] + '\n')
102
elif line.startswith('2>'):
105
raise SyntaxError('No command for that error',
106
(file_name, lineno, 1, orig))
108
error.append(line[2:] + '\n')
112
raise SyntaxError('No command for that output',
113
(file_name, lineno, 1, orig))
115
output.append(line + '\n')
116
# Add the last seen command
117
add_command(cmd_cur, input, output, error)
121
def _scan_redirection_options(args):
122
"""Recognize and process input and output redirections.
124
:param args: The command line arguments
126
:return: A tuple containing:
127
- The file name redirected from or None
128
- The file name redirected to or None
129
- The mode to open the output file or None
130
- The reamining arguments
132
def redirected_file_name(direction, name, args):
137
# We leave the error handling to higher levels, an empty name
144
out_name, out_mode = None, None
147
if arg.startswith('<'):
148
in_name = redirected_file_name('<', arg[1:], args)
149
elif arg.startswith('>>'):
150
out_name = redirected_file_name('>>', arg[2:], args)
152
elif arg.startswith('>',):
153
out_name = redirected_file_name('>', arg[1:], args)
156
remaining.append(arg)
157
return in_name, out_name, out_mode, remaining
160
class ScriptRunner(object):
161
"""Run a shell-like script from a test.
165
from bzrlib.tests import script
169
def test_bug_nnnnn(self):
170
sr = script.ScriptRunner()
171
sr.run_script(self, '''
179
self.output_checker = doctest.OutputChecker()
180
self.check_options = doctest.ELLIPSIS
182
def run_script(self, test_case, text):
183
"""Run a shell-like script as a test.
185
:param test_case: A TestCase instance that should provide the fail(),
186
assertEqualDiff and _run_bzr_core() methods as well as a 'test_dir'
187
attribute used as a jail root.
189
:param text: A shell-like script (see _script_to_commands for syntax).
191
for cmd, input, output, error in _script_to_commands(text):
192
self.run_command(test_case, cmd, input, output, error)
194
def run_command(self, test_case, cmd, input, output, error):
195
mname = 'do_' + cmd[0]
196
method = getattr(self, mname, None)
198
raise SyntaxError('Command not found "%s"' % (cmd[0],),
199
None, 1, ' '.join(cmd))
203
str_input = ''.join(input)
204
args = list(self._pre_process_args(cmd[1:]))
205
retcode, actual_output, actual_error = method(test_case,
208
self._check_output(output, actual_output, test_case)
209
self._check_output(error, actual_error, test_case)
210
if retcode and not error and actual_error:
211
test_case.fail('In \n\t%s\nUnexpected error: %s'
212
% (' '.join(cmd), actual_error))
213
return retcode, actual_output, actual_error
215
def _check_output(self, expected, actual, test_case):
217
# Specifying None means: any output is accepted
220
test_case.fail('Unexpected: %s' % actual)
221
matching = self.output_checker.check_output(
222
expected, actual, self.check_options)
224
# Note that we can't use output_checker.output_difference() here
225
# because... the API is broken ('expected' must be a doctest
226
# specific object of which a 'want' attribute will be our
227
# 'expected' parameter. So we just fallback to our good old
228
# assertEqualDiff since we know there *are* differences and the
229
# output should be decently readable.
230
test_case.assertEqualDiff(expected, actual)
232
def _pre_process_args(self, args):
235
# Strip the simple and double quotes since we don't care about
236
# them. We leave the backquotes in place though since they have a
237
# different semantic.
238
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
241
if glob.has_magic(arg):
242
matches = glob.glob(arg)
244
# We care more about order stability than performance
252
def _read_input(self, input, in_name):
253
if in_name is not None:
254
infile = open(in_name, 'rb')
256
# Command redirection takes precedence over provided input
257
input = infile.read()
262
def _write_output(self, output, out_name, out_mode):
263
if out_name is not None:
264
outfile = open(out_name, out_mode)
266
outfile.write(output)
272
def do_bzr(self, test_case, input, args):
273
retcode, out, err = test_case._run_bzr_core(
274
args, retcode=None, encoding=None, stdin=input, working_dir=None)
275
return retcode, out, err
277
def do_cat(self, test_case, input, args):
278
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
279
if args and in_name is not None:
280
raise SyntaxError('Specify a file OR use redirection')
288
for in_name in input_names:
290
inputs.append(self._read_input(None, in_name))
292
if e.errno == errno.ENOENT:
294
'%s: No such file or directory\n' % (in_name,))
295
# Basically cat copy input to output
296
output = ''.join(inputs)
297
# Handle output redirections
299
output = self._write_output(output, out_name, out_mode)
301
if e.errno == errno.ENOENT:
302
return 1, None, '%s: No such file or directory\n' % (out_name,)
303
return 0, output, None
305
def do_echo(self, test_case, input, args):
306
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
308
raise SyntaxError('Specify parameters OR use redirection')
310
input = ' '.join(args)
312
input = self._read_input(input, in_name)
314
if e.errno == errno.ENOENT:
315
return 1, None, '%s: No such file or directory\n' % (in_name,)
316
# Always append a \n'
320
# Handle output redirections
322
output = self._write_output(output, out_name, out_mode)
324
if e.errno == errno.ENOENT:
325
return 1, None, '%s: No such file or directory\n' % (out_name,)
326
return 0, output, None
328
def _get_jail_root(self, test_case):
329
return test_case.test_dir
331
def _ensure_in_jail(self, test_case, path):
332
jail_root = self._get_jail_root(test_case)
333
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
334
raise ValueError('%s is not inside %s' % (path, jail_root))
336
def do_cd(self, test_case, input, args):
338
raise SyntaxError('Usage: cd [dir]')
341
self._ensure_in_jail(test_case, d)
343
# The test "home" directory is the root of its jail
344
d = self._get_jail_root(test_case)
348
def do_mkdir(self, test_case, input, args):
349
if not args or len(args) != 1:
350
raise SyntaxError('Usage: mkdir dir')
352
self._ensure_in_jail(test_case, d)
356
def do_rm(self, test_case, input, args):
359
def error(msg, path):
360
return "rm: cannot remove '%s': %s\n" % (path, msg)
362
force, recursive = False, False
364
if args and args[0][0] == '-':
365
opts = args.pop(0)[1:]
368
opts = opts.replace('f', '', 1)
371
opts = opts.replace('r', '', 1)
373
raise SyntaxError('Usage: rm [-fr] path+')
375
self._ensure_in_jail(test_case, p)
376
# FIXME: Should we put that in osutils ?
380
# Various OSes raises different exceptions (linux: EISDIR,
381
# win32: EACCES, OSX: EPERM) when invoked on a directory
382
if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
386
err = error('Is a directory', p)
388
elif e.errno == errno.ENOENT:
390
err = error('No such file or directory', p)
398
return retcode, None, err
401
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
402
"""Helper class to experiment shell-like test and memory fs.
404
This not intended to be used outside of experiments in implementing memoy
405
based file systems and evolving bzr so that test can use only memory based
410
super(TestCaseWithMemoryTransportAndScript, self).setUp()
411
self.script_runner = ScriptRunner()
413
def run_script(self, script):
414
return self.script_runner.run_script(self, script)
416
def run_command(self, cmd, input, output, error):
417
return self.script_runner.run_command(self, cmd, input, output, error)
420
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
421
"""Helper class to quickly define shell-like tests.
425
from bzrlib.tests import script
428
class TestBug(script.TestCaseWithTransportAndScript):
430
def test_bug_nnnnn(self):
439
super(TestCaseWithTransportAndScript, self).setUp()
440
self.script_runner = ScriptRunner()
442
def run_script(self, script):
443
return self.script_runner.run_script(self, script)
445
def run_command(self, cmd, input, output, error):
446
return self.script_runner.run_command(self, cmd, input, output, error)