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):
162
def __init__(self, test_case):
163
self.test_case = test_case
164
self.output_checker = doctest.OutputChecker()
165
self.check_options = doctest.ELLIPSIS
167
def run_script(self, text):
168
for cmd, input, output, error in _script_to_commands(text):
169
self.run_command(cmd, input, output, error)
171
def _check_output(self, expected, actual):
173
# Specifying None means: any output is accepted
176
self.test_case.fail('Unexpected: %s' % actual)
177
matching = self.output_checker.check_output(
178
expected, actual, self.check_options)
180
# Note that we can't use output_checker.output_difference() here
181
# because... the API is broken ('expected' must be a doctest
182
# specific object of which a 'want' attribute will be our
183
# 'expected' parameter. So we just fallback to our good old
184
# assertEqualDiff since we know there *are* differences and the
185
# output should be decently readable.
186
self.test_case.assertEqualDiff(expected, actual)
188
def _pre_process_args(self, args):
191
# Strip the simple and double quotes since we don't care about
192
# them. We leave the backquotes in place though since they have a
193
# different semantic.
194
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
197
if glob.has_magic(arg):
198
matches = glob.glob(arg)
200
# We care more about order stability than performance
208
def run_command(self, cmd, input, output, error):
209
mname = 'do_' + cmd[0]
210
method = getattr(self, mname, None)
212
raise SyntaxError('Command not found "%s"' % (cmd[0],),
213
None, 1, ' '.join(cmd))
217
str_input = ''.join(input)
218
args = list(self._pre_process_args(cmd[1:]))
219
retcode, actual_output, actual_error = method(str_input, args)
221
self._check_output(output, actual_output)
222
self._check_output(error, actual_error)
223
if retcode and not error and actual_error:
224
self.test_case.fail('In \n\t%s\nUnexpected error: %s'
225
% (' '.join(cmd), actual_error))
226
return retcode, actual_output, actual_error
228
def _read_input(self, input, in_name):
229
if in_name is not None:
230
infile = open(in_name, 'rb')
232
# Command redirection takes precedence over provided input
233
input = infile.read()
238
def _write_output(self, output, out_name, out_mode):
239
if out_name is not None:
240
outfile = open(out_name, out_mode)
242
outfile.write(output)
248
def do_bzr(self, input, args):
249
retcode, out, err = self.test_case._run_bzr_core(
250
args, retcode=None, encoding=None, stdin=input, working_dir=None)
251
return retcode, out, err
253
def do_cat(self, input, args):
254
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
255
if args and in_name is not None:
256
raise SyntaxError('Specify a file OR use redirection')
264
for in_name in input_names:
266
inputs.append(self._read_input(None, in_name))
268
if e.errno == errno.ENOENT:
270
'%s: No such file or directory\n' % (in_name,))
271
# Basically cat copy input to output
272
output = ''.join(inputs)
273
# Handle output redirections
275
output = self._write_output(output, out_name, out_mode)
277
if e.errno == errno.ENOENT:
278
return 1, None, '%s: No such file or directory\n' % (out_name,)
279
return 0, output, None
281
def do_echo(self, input, args):
282
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
284
raise SyntaxError('Specify parameters OR use redirection')
286
input = ' '.join(args)
288
input = self._read_input(input, in_name)
290
if e.errno == errno.ENOENT:
291
return 1, None, '%s: No such file or directory\n' % (in_name,)
292
# Always append a \n'
296
# Handle output redirections
298
output = self._write_output(output, out_name, out_mode)
300
if e.errno == errno.ENOENT:
301
return 1, None, '%s: No such file or directory\n' % (out_name,)
302
return 0, output, None
304
def _ensure_in_jail(self, path):
305
jail_root = self.test_case.get_jail_root()
306
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
307
raise ValueError('%s is not inside %s' % (path, jail_root))
309
def do_cd(self, input, args):
311
raise SyntaxError('Usage: cd [dir]')
314
self._ensure_in_jail(d)
316
d = self.test_case.get_jail_root()
320
def do_mkdir(self, input, args):
321
if not args or len(args) != 1:
322
raise SyntaxError('Usage: mkdir dir')
324
self._ensure_in_jail(d)
328
def do_rm(self, input, args):
331
def error(msg, path):
332
return "rm: cannot remove '%s': %s\n" % (path, msg)
334
force, recursive = False, False
336
if args and args[0][0] == '-':
337
opts = args.pop(0)[1:]
340
opts = opts.replace('f', '', 1)
343
opts = opts.replace('r', '', 1)
345
raise SyntaxError('Usage: rm [-fr] path+')
347
self._ensure_in_jail(p)
348
# FIXME: Should we put that in osutils ?
352
if e.errno == errno.EISDIR:
356
err = error('Is a directory', p)
358
elif e.errno == errno.ENOENT:
360
err = error('No such file or directory', p)
368
return retcode, None, err
371
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
374
super(TestCaseWithMemoryTransportAndScript, self).setUp()
375
self.script_runner = ScriptRunner(self)
376
# Break the circular dependency
377
def break_dependency():
378
self.script_runner = None
379
self.addCleanup(break_dependency)
381
def get_jail_root(self):
382
raise NotImplementedError(self.get_jail_root)
384
def run_script(self, script):
385
return self.script_runner.run_script(script)
387
def run_command(self, cmd, input, output, error):
388
return self.script_runner.run_command(cmd, input, output, error)
391
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
394
super(TestCaseWithTransportAndScript, self).setUp()
395
self.script_runner = ScriptRunner(self)
396
# Break the circular dependency
397
def break_dependency():
398
self.script_runner = None
399
self.addCleanup(break_dependency)
401
def get_jail_root(self):
404
def run_script(self, script):
405
return self.script_runner.run_script(script)
407
def run_command(self, cmd, input, output, error):
408
return self.script_runner.run_command(cmd, input, output, error)