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 developpers/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. It can be associated with an
48
input that will feed it and an expected output.
49
Comments starts with '#' until the end of line.
50
Empty lines are ignored.
51
Input and output are full lines terminated by a '\n'.
52
Input lines start with '<'.
53
Output lines start with '>'.
54
Error lines start with '2>'.
59
def add_command(cmd, input, output, error):
62
input = ''.join(input)
63
if output is not None:
64
output = ''.join(output)
66
error = ''.join(error)
67
commands.append((cmd, input, output, error))
72
input, output, error = None, None, None
73
for line in text.split('\n'):
75
# Keep a copy for error reporting
77
comment = line.find('#')
80
line = line[0:comment]
85
if line.startswith('$'):
86
# Time to output the current command
87
add_command(cmd_cur, input, output, error)
89
cmd_cur = list(split(line[1:]))
91
input, output, error = None, None, None
92
elif line.startswith('<'):
95
raise SyntaxError('No command for that input',
96
(file_name, lineno, 1, orig))
98
input.append(line[1:] + '\n')
99
elif line.startswith('2>'):
102
raise SyntaxError('No command for that error',
103
(file_name, lineno, 1, orig))
105
error.append(line[2:] + '\n')
109
raise SyntaxError('No command for that output',
110
(file_name, lineno, 1, orig))
112
output.append(line + '\n')
113
# Add the last seen command
114
add_command(cmd_cur, input, output, error)
118
def _scan_redirection_options(args):
119
"""Recognize and process input and output redirections.
121
:param args: The command line arguments
123
:return: A tuple containing:
124
- The file name redirected from or None
125
- The file name redirected to or None
126
- The mode to open the output file or None
127
- The reamining arguments
129
def redirected_file_name(direction, name, args):
134
# We leave the error handling to higher levels, an empty name
141
out_name, out_mode = None, None
144
if arg.startswith('<'):
145
in_name = redirected_file_name('<', arg[1:], args)
146
elif arg.startswith('>>'):
147
out_name = redirected_file_name('>>', arg[2:], args)
149
elif arg.startswith('>',):
150
out_name = redirected_file_name('>', arg[1:], args)
153
remaining.append(arg)
154
return in_name, out_name, out_mode, remaining
157
class ScriptRunner(object):
159
def __init__(self, test_case):
160
self.test_case = test_case
161
self.output_checker = doctest.OutputChecker()
162
self.check_options = doctest.ELLIPSIS
164
def run_script(self, text):
165
for cmd, input, output, error in _script_to_commands(text):
166
self.run_command(cmd, input, output, error)
168
def _check_output(self, expected, actual):
170
# Specifying None means: any output is accepted
173
self.test_case.fail('Unexpected: %s' % actual)
174
matching = self.output_checker.check_output(
175
expected, actual, self.check_options)
177
# Note that we can't use output_checker.output_difference() here
178
# because... the API is broken ('expected' must be a doctest
179
# specific object of which a 'want' attribute will be our
180
# 'expected' parameter. So we just fallback to our good old
181
# assertEqualDiff since we know there *are* differences and the
182
# output should be decently readable.
183
self.test_case.assertEqualDiff(expected, actual)
185
def _pre_process_args(self, args):
188
# Strip the simple and double quotes since we don't care about
189
# them. We leave the backquotes in place though since they have a
190
# different semantic.
191
if arg[0] in ('"', "'") and arg[0] == arg[-1]:
194
if glob.has_magic(arg):
195
matches = glob.glob(arg)
197
# We care more about order stability than performance
205
def run_command(self, cmd, input, output, error):
206
mname = 'do_' + cmd[0]
207
method = getattr(self, mname, None)
209
raise SyntaxError('Command not found "%s"' % (cmd[0],),
210
None, 1, ' '.join(cmd))
214
str_input = ''.join(input)
215
args = list(self._pre_process_args(cmd[1:]))
216
retcode, actual_output, actual_error = method(str_input, args)
218
self._check_output(output, actual_output)
219
self._check_output(error, actual_error)
220
if retcode and not error and actual_error:
221
self.test_case.fail('In \n\t%s\nUnexpected error: %s'
222
% (' '.join(cmd), actual_error))
223
return retcode, actual_output, actual_error
225
def _read_input(self, input, in_name):
226
if in_name is not None:
227
infile = open(in_name, 'rb')
229
# Command redirection takes precedence over provided input
230
input = infile.read()
235
def _write_output(self, output, out_name, out_mode):
236
if out_name is not None:
237
outfile = open(out_name, out_mode)
239
outfile.write(output)
245
def do_bzr(self, input, args):
246
retcode, out, err = self.test_case._run_bzr_core(
247
args, retcode=None, encoding=None, stdin=input, working_dir=None)
248
return retcode, out, err
250
def do_cat(self, input, args):
251
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
252
if args and in_name is not None:
253
raise SyntaxError('Specify a file OR use redirection')
261
for in_name in input_names:
263
inputs.append(self._read_input(None, in_name))
265
if e.errno == errno.ENOENT:
267
'%s: No such file or directory\n' % (in_name,))
268
# Basically cat copy input to output
269
output = ''.join(inputs)
270
# Handle output redirections
272
output = self._write_output(output, out_name, out_mode)
274
if e.errno == errno.ENOENT:
275
return 1, None, '%s: No such file or directory\n' % (out_name,)
276
return 0, output, None
278
def do_echo(self, input, args):
279
(in_name, out_name, out_mode, args) = _scan_redirection_options(args)
281
raise SyntaxError('Specify parameters OR use redirection')
283
input = ' '.join(args)
285
input = self._read_input(input, in_name)
287
if e.errno == errno.ENOENT:
288
return 1, None, '%s: No such file or directory\n' % (in_name,)
289
# Always append a \n'
293
# Handle output redirections
295
output = self._write_output(output, out_name, out_mode)
297
if e.errno == errno.ENOENT:
298
return 1, None, '%s: No such file or directory\n' % (out_name,)
299
return 0, output, None
301
def _ensure_in_jail(self, path):
302
jail_root = self.test_case.get_jail_root()
303
if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
304
raise ValueError('%s is not inside %s' % (path, jail_root))
306
def do_cd(self, input, args):
308
raise SyntaxError('Usage: cd [dir]')
311
self._ensure_in_jail(d)
313
d = self.test_case.get_jail_root()
317
def do_mkdir(self, input, args):
318
if not args or len(args) != 1:
319
raise SyntaxError('Usage: mkdir dir')
321
self._ensure_in_jail(d)
325
def do_rm(self, input, args):
328
def error(msg, path):
329
return "rm: cannot remove '%s': %s\n" % (path, msg)
331
force, recursive = False, False
333
if args and args[0][0] == '-':
334
opts = args.pop(0)[1:]
337
opts = opts.replace('f', '', 1)
340
opts = opts.replace('r', '', 1)
342
raise SyntaxError('Usage: rm [-fr] path+')
344
self._ensure_in_jail(p)
345
# FIXME: Should we put that in osutils ?
349
if e.errno == errno.EISDIR:
353
err = error('Is a directory', p)
355
elif e.errno == errno.ENOENT:
357
err = error('No such file or directory', p)
365
return retcode, None, err
368
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
371
super(TestCaseWithMemoryTransportAndScript, self).setUp()
372
self.script_runner = ScriptRunner(self)
373
# Break the circular dependency
374
def break_dependency():
375
self.script_runner = None
376
self.addCleanup(break_dependency)
378
def get_jail_root(self):
379
raise NotImplementedError(self.get_jail_root)
381
def run_script(self, script):
382
return self.script_runner.run_script(script)
384
def run_command(self, cmd, input, output, error):
385
return self.script_runner.run_command(cmd, input, output, error)
388
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
391
super(TestCaseWithTransportAndScript, self).setUp()
392
self.script_runner = ScriptRunner(self)
393
# Break the circular dependency
394
def break_dependency():
395
self.script_runner = None
396
self.addCleanup(break_dependency)
398
def get_jail_root(self):
401
def run_script(self, script):
402
return self.script_runner.run_script(script)
404
def run_command(self, cmd, input, output, error):
405
return self.script_runner.run_command(cmd, input, output, error)