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('We expected output: %r, but found None'
222
matching = self.output_checker.check_output(
223
expected, actual, self.check_options)
225
# Note that we can't use output_checker.output_difference() here
226
# because... the API is broken ('expected' must be a doctest
227
# specific object of which a 'want' attribute will be our
228
# 'expected' parameter. So we just fallback to our good old
229
# assertEqualDiff since we know there *are* differences and the
230
# output should be decently readable.
231
test_case.assertEqualDiff(expected, actual)
233
def _pre_process_args(self, args):
236
# Strip the simple and double quotes since we don't care about
237
# them. We leave the backquotes in place though since they have a
238
# different semantic.
239
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
242
if glob.has_magic(arg):
243
matches = glob.glob(arg)
245
# We care more about order stability than performance
253
def _read_input(self, input, in_name):
254
if in_name is not None:
255
infile = open(in_name, 'rb')
257
# Command redirection takes precedence over provided input
258
input = infile.read()
263
def _write_output(self, output, out_name, out_mode):
264
if out_name is not None:
265
outfile = open(out_name, out_mode)
267
outfile.write(output)
273
def do_bzr(self, test_case, input, args):
274
retcode, out, err = test_case._run_bzr_core(
275
args, retcode=None, encoding=None, stdin=input, working_dir=None)
276
return retcode, out, err
278
def do_cat(self, test_case, input, args):
279
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
280
if args and in_name is not None:
281
raise SyntaxError('Specify a file OR use redirection')
289
for in_name in input_names:
291
inputs.append(self._read_input(None, in_name))
293
# Some filenames are illegal on Windows and generate EINVAL
294
# rather than just saying the filename doesn't exist
295
if e.errno in (errno.ENOENT, errno.EINVAL):
297
'%s: No such file or directory\n' % (in_name,))
299
# Basically cat copy input to output
300
output = ''.join(inputs)
301
# Handle output redirections
303
output = self._write_output(output, out_name, out_mode)
305
# If out_name cannot be created, we may get 'ENOENT', however if
306
# out_name is something like '', we can get EINVAL
307
if e.errno in (errno.ENOENT, errno.EINVAL):
308
return 1, None, '%s: No such file or directory\n' % (out_name,)
310
return 0, output, None
312
def do_echo(self, test_case, input, args):
313
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
315
raise SyntaxError('echo doesn\'t read from stdin')
317
input = ' '.join(args)
318
# Always append a \n'
322
# Handle output redirections
324
output = self._write_output(output, out_name, out_mode)
326
if e.errno in (errno.ENOENT, errno.EINVAL):
327
return 1, None, '%s: No such file or directory\n' % (out_name,)
329
return 0, output, None
331
def _get_jail_root(self, test_case):
332
return test_case.test_dir
334
def _ensure_in_jail(self, test_case, path):
335
jail_root = self._get_jail_root(test_case)
336
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
337
raise ValueError('%s is not inside %s' % (path, jail_root))
339
def do_cd(self, test_case, input, args):
341
raise SyntaxError('Usage: cd [dir]')
344
self._ensure_in_jail(test_case, d)
346
# The test "home" directory is the root of its jail
347
d = self._get_jail_root(test_case)
351
def do_mkdir(self, test_case, input, args):
352
if not args or len(args) != 1:
353
raise SyntaxError('Usage: mkdir dir')
355
self._ensure_in_jail(test_case, d)
359
def do_rm(self, test_case, input, args):
362
def error(msg, path):
363
return "rm: cannot remove '%s': %s\n" % (path, msg)
365
force, recursive = False, False
367
if args and args[0][0] == '-':
368
opts = args.pop(0)[1:]
371
opts = opts.replace('f', '', 1)
374
opts = opts.replace('r', '', 1)
376
raise SyntaxError('Usage: rm [-fr] path+')
378
self._ensure_in_jail(test_case, p)
379
# FIXME: Should we put that in osutils ?
383
# Various OSes raises different exceptions (linux: EISDIR,
384
# win32: EACCES, OSX: EPERM) when invoked on a directory
385
if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
389
err = error('Is a directory', p)
391
elif e.errno == errno.ENOENT:
393
err = error('No such file or directory', p)
401
return retcode, None, err
404
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
405
"""Helper class to experiment shell-like test and memory fs.
407
This not intended to be used outside of experiments in implementing memoy
408
based file systems and evolving bzr so that test can use only memory based
413
super(TestCaseWithMemoryTransportAndScript, self).setUp()
414
self.script_runner = ScriptRunner()
416
def run_script(self, script):
417
return self.script_runner.run_script(self, script)
419
def run_command(self, cmd, input, output, error):
420
return self.script_runner.run_command(self, cmd, input, output, error)
423
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
424
"""Helper class to quickly define shell-like tests.
428
from bzrlib.tests import script
431
class TestBug(script.TestCaseWithTransportAndScript):
433
def test_bug_nnnnn(self):
442
super(TestCaseWithTransportAndScript, self).setUp()
443
self.script_runner = ScriptRunner()
445
def run_script(self, script):
446
return self.script_runner.run_script(self, script)
448
def run_command(self, cmd, input, output, error):
449
return self.script_runner.run_command(self, cmd, input, output, error)