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.
 
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')
 
110
 
            # can happen if the first line is not recognized as a command, eg
 
111
 
            # if the prompt has leading whitespace
 
114
 
                    raise SyntaxError('No command for line %r' % (line,),
 
115
 
                                      (file_name, lineno, 1, orig))
 
117
 
            output.append(line + '\n')
 
118
 
    # Add the last seen command
 
119
 
    add_command(cmd_cur, input, output, error)
 
123
 
def _scan_redirection_options(args):
 
124
 
    """Recognize and process input and output redirections.
 
126
 
    :param args: The command line arguments
 
128
 
    :return: A tuple containing: 
 
129
 
        - The file name redirected from or None
 
130
 
        - The file name redirected to or None
 
131
 
        - The mode to open the output file or None
 
132
 
        - The reamining arguments
 
134
 
    def redirected_file_name(direction, name, args):
 
139
 
                # We leave the error handling to higher levels, an empty name
 
146
 
    out_name, out_mode = None, None
 
149
 
        if arg.startswith('<'):
 
150
 
            in_name = redirected_file_name('<', arg[1:], args)
 
151
 
        elif arg.startswith('>>'):
 
152
 
            out_name = redirected_file_name('>>', arg[2:], args)
 
154
 
        elif arg.startswith('>',):
 
155
 
            out_name = redirected_file_name('>', arg[1:], args)
 
158
 
            remaining.append(arg)
 
159
 
    return in_name, out_name, out_mode, remaining
 
162
 
class ScriptRunner(object):
 
163
 
    """Run a shell-like script from a test.
 
167
 
    from bzrlib.tests import script
 
171
 
        def test_bug_nnnnn(self):
 
172
 
            sr = script.ScriptRunner()
 
173
 
            sr.run_script(self, '''
 
181
 
        self.output_checker = doctest.OutputChecker()
 
182
 
        self.check_options = doctest.ELLIPSIS
 
184
 
    def run_script(self, test_case, text):
 
185
 
        """Run a shell-like script as a test.
 
187
 
        :param test_case: A TestCase instance that should provide the fail(),
 
188
 
            assertEqualDiff and _run_bzr_core() methods as well as a 'test_dir'
 
189
 
            attribute used as a jail root.
 
191
 
        :param text: A shell-like script (see _script_to_commands for syntax).
 
193
 
        for cmd, input, output, error in _script_to_commands(text):
 
194
 
            self.run_command(test_case, cmd, input, output, error)
 
196
 
    def run_command(self, test_case, cmd, input, output, error):
 
197
 
        mname = 'do_' + cmd[0]
 
198
 
        method = getattr(self, mname, None)
 
200
 
            raise SyntaxError('Command not found "%s"' % (cmd[0],),
 
201
 
                              None, 1, ' '.join(cmd))
 
205
 
            str_input = ''.join(input)
 
206
 
        args = list(self._pre_process_args(cmd[1:]))
 
207
 
        retcode, actual_output, actual_error = method(test_case,
 
210
 
        self._check_output(output, actual_output, test_case)
 
211
 
        self._check_output(error, actual_error, test_case)
 
212
 
        if retcode and not error and actual_error:
 
213
 
            test_case.fail('In \n\t%s\nUnexpected error: %s'
 
214
 
                           % (' '.join(cmd), actual_error))
 
215
 
        return retcode, actual_output, actual_error
 
217
 
    def _check_output(self, expected, actual, test_case):
 
219
 
            # Specifying None means: any output is accepted
 
222
 
            test_case.fail('We expected output: %r, but found None'
 
224
 
        matching = self.output_checker.check_output(
 
225
 
            expected, actual, self.check_options)
 
227
 
            # Note that we can't use output_checker.output_difference() here
 
228
 
            # because... the API is broken ('expected' must be a doctest
 
229
 
            # specific object of which a 'want' attribute will be our
 
230
 
            # 'expected' parameter. So we just fallback to our good old
 
231
 
            # assertEqualDiff since we know there *are* differences and the
 
232
 
            # output should be decently readable.
 
233
 
            test_case.assertEqualDiff(expected, actual)
 
235
 
    def _pre_process_args(self, args):
 
238
 
            # Strip the simple and double quotes since we don't care about
 
239
 
            # them.  We leave the backquotes in place though since they have a
 
240
 
            # different semantic.
 
241
 
            if arg[0] in  ('"', "'") and arg[0] == arg[-1]:
 
244
 
                if glob.has_magic(arg):
 
245
 
                    matches = glob.glob(arg)
 
247
 
                        # We care more about order stability than performance
 
255
 
    def _read_input(self, input, in_name):
 
256
 
        if in_name is not None:
 
257
 
            infile = open(in_name, 'rb')
 
259
 
                # Command redirection takes precedence over provided input
 
260
 
                input = infile.read()
 
265
 
    def _write_output(self, output, out_name, out_mode):
 
266
 
        if out_name is not None:
 
267
 
            outfile = open(out_name, out_mode)
 
269
 
                outfile.write(output)
 
275
 
    def do_bzr(self, test_case, input, args):
 
276
 
        retcode, out, err = test_case._run_bzr_core(
 
277
 
            args, retcode=None, encoding=None, stdin=input, working_dir=None)
 
278
 
        return retcode, out, err
 
280
 
    def do_cat(self, test_case, input, args):
 
281
 
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
 
282
 
        if args and in_name is not None:
 
283
 
            raise SyntaxError('Specify a file OR use redirection')
 
291
 
        for in_name in input_names:
 
293
 
                inputs.append(self._read_input(None, in_name))
 
295
 
                # Some filenames are illegal on Windows and generate EINVAL
 
296
 
                # rather than just saying the filename doesn't exist
 
297
 
                if e.errno in (errno.ENOENT, errno.EINVAL):
 
299
 
                            '%s: No such file or directory\n' % (in_name,))
 
301
 
        # Basically cat copy input to output
 
302
 
        output = ''.join(inputs)
 
303
 
        # Handle output redirections
 
305
 
            output = self._write_output(output, out_name, out_mode)
 
307
 
            # If out_name cannot be created, we may get 'ENOENT', however if
 
308
 
            # out_name is something like '', we can get EINVAL
 
309
 
            if e.errno in (errno.ENOENT, errno.EINVAL):
 
310
 
                return 1, None, '%s: No such file or directory\n' % (out_name,)
 
312
 
        return 0, output, None
 
314
 
    def do_echo(self, test_case, input, args):
 
315
 
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
 
317
 
            raise SyntaxError('echo doesn\'t read from stdin')
 
319
 
            input = ' '.join(args)
 
320
 
        # Always append a \n'
 
324
 
        # Handle output redirections
 
326
 
            output = self._write_output(output, out_name, out_mode)
 
328
 
            if e.errno in (errno.ENOENT, errno.EINVAL):
 
329
 
                return 1, None, '%s: No such file or directory\n' % (out_name,)
 
331
 
        return 0, output, None
 
333
 
    def _get_jail_root(self, test_case):
 
334
 
        return test_case.test_dir
 
336
 
    def _ensure_in_jail(self, test_case, path):
 
337
 
        jail_root = self._get_jail_root(test_case)
 
338
 
        if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
 
339
 
            raise ValueError('%s is not inside %s' % (path, jail_root))
 
341
 
    def do_cd(self, test_case, input, args):
 
343
 
            raise SyntaxError('Usage: cd [dir]')
 
346
 
            self._ensure_in_jail(test_case, d)
 
348
 
            # The test "home" directory is the root of its jail
 
349
 
            d = self._get_jail_root(test_case)
 
353
 
    def do_mkdir(self, test_case, input, args):
 
354
 
        if not args or len(args) != 1:
 
355
 
            raise SyntaxError('Usage: mkdir dir')
 
357
 
        self._ensure_in_jail(test_case, d)
 
361
 
    def do_rm(self, test_case, input, args):
 
364
 
        def error(msg, path):
 
365
 
            return  "rm: cannot remove '%s': %s\n" % (path, msg)
 
367
 
        force, recursive = False, False
 
369
 
        if args and args[0][0] == '-':
 
370
 
            opts = args.pop(0)[1:]
 
373
 
                opts = opts.replace('f', '', 1)
 
376
 
                opts = opts.replace('r', '', 1)
 
378
 
            raise SyntaxError('Usage: rm [-fr] path+')
 
380
 
            self._ensure_in_jail(test_case, p)
 
381
 
            # FIXME: Should we put that in osutils ?
 
385
 
                # Various OSes raises different exceptions (linux: EISDIR,
 
386
 
                #   win32: EACCES, OSX: EPERM) when invoked on a directory
 
387
 
                if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
 
391
 
                        err = error('Is a directory', p)
 
393
 
                elif e.errno == errno.ENOENT:
 
395
 
                        err =  error('No such file or directory', p)
 
403
 
        return retcode, None, err
 
405
 
    def do_mv(self, test_case, input, args):
 
407
 
        def error(msg, src, dst):
 
408
 
            return "mv: cannot move %s to %s: %s\n" % (src, dst, msg)
 
410
 
        if not args or len(args) != 2:
 
411
 
            raise SyntaxError("Usage: mv path1 path2")
 
415
 
            if os.path.isdir(dst):
 
416
 
                real_dst = os.path.join(dst, os.path.basename(src))
 
417
 
            os.rename(src, real_dst)
 
419
 
            if e.errno == errno.ENOENT:
 
420
 
                err = error('No such file or directory', src, dst)
 
427
 
        return retcode, None, err
 
431
 
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
 
432
 
    """Helper class to experiment shell-like test and memory fs.
 
434
 
    This not intended to be used outside of experiments in implementing memoy
 
435
 
    based file systems and evolving bzr so that test can use only memory based
 
440
 
        super(TestCaseWithMemoryTransportAndScript, self).setUp()
 
441
 
        self.script_runner = ScriptRunner()
 
443
 
    def run_script(self, script):
 
444
 
        return self.script_runner.run_script(self, script)
 
446
 
    def run_command(self, cmd, input, output, error):
 
447
 
        return self.script_runner.run_command(self, cmd, input, output, error)
 
450
 
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
 
451
 
    """Helper class to quickly define shell-like tests.
 
455
 
    from bzrlib.tests import script
 
458
 
    class TestBug(script.TestCaseWithTransportAndScript):
 
460
 
        def test_bug_nnnnn(self):
 
469
 
        super(TestCaseWithTransportAndScript, self).setUp()
 
470
 
        self.script_runner = ScriptRunner()
 
472
 
    def run_script(self, script):
 
473
 
        return self.script_runner.run_script(self, script)
 
475
 
    def run_command(self, cmd, input, output, error):
 
476
 
        return self.script_runner.run_command(self, cmd, input, output, error)