/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
1
# Copyright (C) 2009 Canonical Ltd
2
#
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.
7
#
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.
12
#
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
4665.5.20 by Vincent Ladeuil
Fixed as per Martin's review.
16
4665.5.9 by Vincent Ladeuil
Start adding doc.
17
"""Shell-like test scripts.
18
4665.5.21 by Vincent Ladeuil
Fix some typos mentioned by Martin.
19
See developers/testing.html for more explanations.
4665.5.9 by Vincent Ladeuil
Start adding doc.
20
"""
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
21
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
22
import doctest
4665.5.17 by Vincent Ladeuil
Implement 'rm' command.
23
import errno
4665.5.19 by Vincent Ladeuil
Implement globbing and enhance cat to accept multiple files.
24
import glob
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
25
import os
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
26
import shlex
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
27
from cStringIO import StringIO
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
28
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
29
from bzrlib import (
30
    osutils,
31
    tests,
32
    )
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
33
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
34
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
35
def split(s):
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):
4665.5.19 by Vincent Ladeuil
Implement globbing and enhance cat to accept multiple files.
41
        yield t
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
42
43
44
def _script_to_commands(text, file_name=None):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
45
    """Turn a script into a list of commands with their associated IOs.
46
4665.5.21 by Vincent Ladeuil
Fix some typos mentioned by Martin.
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.
49
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
50
    Comments starts with '#' until the end of line.
51
    Empty lines are ignored.
4665.5.21 by Vincent Ladeuil
Fix some typos mentioned by Martin.
52
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
53
    Input and output are full lines terminated by a '\n'.
4665.5.21 by Vincent Ladeuil
Fix some typos mentioned by Martin.
54
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
55
    Input lines start with '<'.
4665.5.21 by Vincent Ladeuil
Fix some typos mentioned by Martin.
56
    Output lines start with nothing.
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
57
    Error lines start with '2>'.
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
58
    """
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
59
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
60
    commands = []
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
61
62
    def add_command(cmd, input, output, error):
63
        if cmd is not None:
64
            if input is not None:
65
                input = ''.join(input)
66
            if output is not None:
67
                output = ''.join(output)
68
            if error is not None:
69
                error = ''.join(error)
70
            commands.append((cmd, input, output, error))
71
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
72
    cmd_cur = None
73
    cmd_line = 1
74
    lineno = 0
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
75
    input, output, error = None, None, None
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
76
    for line in text.split('\n'):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
77
        lineno += 1
78
        # Keep a copy for error reporting
79
        orig = line
80
        comment =  line.find('#')
81
        if comment >= 0:
82
            # Delete comments
83
            line = line[0:comment]
84
            line = line.rstrip()
85
        if line == '':
86
            # Ignore empty lines
87
            continue
4665.5.20 by Vincent Ladeuil
Fixed as per Martin's review.
88
        if line.startswith('$'):
89
            # Time to output the current command
90
            add_command(cmd_cur, input, output, error)
91
            # And start a new one
92
            cmd_cur = list(split(line[1:]))
93
            cmd_line = lineno
94
            input, output, error = None, None, None
95
        elif line.startswith('<'):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
96
            if input is None:
97
                if cmd_cur is None:
98
                    raise SyntaxError('No command for that input',
99
                                      (file_name, lineno, 1, orig))
100
                input = []
101
            input.append(line[1:] + '\n')
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
102
        elif line.startswith('2>'):
103
            if error is None:
104
                if cmd_cur is None:
105
                    raise SyntaxError('No command for that error',
106
                                      (file_name, lineno, 1, orig))
107
                error = []
108
            error.append(line[2:] + '\n')
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
109
        else:
4665.5.20 by Vincent Ladeuil
Fixed as per Martin's review.
110
            if output is None:
111
                if cmd_cur is None:
112
                    raise SyntaxError('No command for that output',
113
                                      (file_name, lineno, 1, orig))
114
                output = []
115
            output.append(line + '\n')
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
116
    # Add the last seen command
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
117
    add_command(cmd_cur, input, output, error)
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
118
    return commands
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
119
120
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
121
def _scan_redirection_options(args):
122
    """Recognize and process input and output redirections.
123
124
    :param args: The command line arguments
125
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
131
    """
4665.5.18 by Vincent Ladeuil
Better redirection handling.
132
    def redirected_file_name(direction, name, args):
133
        if name == '':
134
            try:
135
                name = args.pop(0)
136
            except IndexError:
137
                # We leave the error handling to higher levels, an empty name
138
                # can't be legal.
139
                name = ''
140
        return name
141
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
142
    remaining = []
143
    in_name = None
144
    out_name, out_mode = None, None
4665.5.18 by Vincent Ladeuil
Better redirection handling.
145
    while args:
146
        arg = args.pop(0)
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
147
        if arg.startswith('<'):
4665.5.18 by Vincent Ladeuil
Better redirection handling.
148
            in_name = redirected_file_name('<', arg[1:], args)
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
149
        elif arg.startswith('>>'):
4665.5.18 by Vincent Ladeuil
Better redirection handling.
150
            out_name = redirected_file_name('>>', arg[2:], args)
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
151
            out_mode = 'ab+'
4665.5.18 by Vincent Ladeuil
Better redirection handling.
152
        elif arg.startswith('>',):
153
            out_name = redirected_file_name('>', arg[1:], args)
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
154
            out_mode = 'wb+'
155
        else:
156
            remaining.append(arg)
157
    return in_name, out_name, out_mode, remaining
158
159
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
160
class ScriptRunner(object):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
161
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
162
    def __init__(self, test_case):
163
        self.test_case = test_case
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
164
        self.output_checker = doctest.OutputChecker()
165
        self.check_options = doctest.ELLIPSIS
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
166
167
    def run_script(self, text):
168
        for cmd, input, output, error in _script_to_commands(text):
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
169
            self.run_command(cmd, input, output, error)
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
170
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
171
    def _check_output(self, expected, actual):
172
        if expected is None:
4665.5.6 by Vincent Ladeuil
Implement 'bzr' command.
173
            # Specifying None means: any output is accepted
174
            return
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
175
        if actual is None:
176
            self.test_case.fail('Unexpected: %s' % actual)
177
        matching = self.output_checker.check_output(
178
            expected, actual, self.check_options)
179
        if not matching:
180
            # Note that we can't use output_checker.output_difference() here
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
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.
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
186
            self.test_case.assertEqualDiff(expected, actual)
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
187
4665.5.19 by Vincent Ladeuil
Implement globbing and enhance cat to accept multiple files.
188
    def _pre_process_args(self, args):
189
        new_args = []
190
        for arg in 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]:
195
                yield arg[1:-1]
196
            else:
197
                if glob.has_magic(arg):
198
                    matches = glob.glob(arg)
199
                    if matches:
200
                        # We care more about order stability than performance
201
                        # here
202
                        matches.sort()
203
                        for m in matches:
204
                            yield m
205
                else:
206
                    yield arg
207
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
208
    def run_command(self, cmd, input, output, error):
209
        mname = 'do_' + cmd[0]
210
        method = getattr(self, mname, None)
211
        if method is None:
212
            raise SyntaxError('Command not found "%s"' % (cmd[0],),
213
                              None, 1, ' '.join(cmd))
214
        if input is None:
215
            str_input = ''
216
        else:
217
            str_input = ''.join(input)
4665.5.19 by Vincent Ladeuil
Implement globbing and enhance cat to accept multiple files.
218
        args = list(self._pre_process_args(cmd[1:]))
219
        retcode, actual_output, actual_error = method(str_input, args)
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
220
221
        self._check_output(output, actual_output)
222
        self._check_output(error, actual_error)
4665.5.16 by Vincent Ladeuil
Continue script execution based on status not error output.
223
        if retcode and not error and actual_error:
4665.5.18 by Vincent Ladeuil
Better redirection handling.
224
            self.test_case.fail('In \n\t%s\nUnexpected error: %s'
225
                                % (' '.join(cmd), actual_error))
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
226
        return retcode, actual_output, actual_error
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
227
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
228
    def _read_input(self, input, in_name):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
229
        if in_name is not None:
230
            infile = open(in_name, 'rb')
231
            try:
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
232
                # Command redirection takes precedence over provided input
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
233
                input = infile.read()
234
            finally:
235
                infile.close()
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
236
        return input
237
238
    def _write_output(self, output, out_name, out_mode):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
239
        if out_name is not None:
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
240
            outfile = open(out_name, out_mode)
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
241
            try:
242
                outfile.write(output)
243
            finally:
244
                outfile.close()
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
245
            output = None
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
246
        return output
247
248
    def do_bzr(self, input, args):
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
249
        retcode, out, err = self.test_case._run_bzr_core(
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
250
            args, retcode=None, encoding=None, stdin=input, working_dir=None)
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
251
        return retcode, out, err
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
252
253
    def do_cat(self, input, args):
254
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
4665.5.19 by Vincent Ladeuil
Implement globbing and enhance cat to accept multiple files.
255
        if args and in_name is not None:
256
            raise SyntaxError('Specify a file OR use redirection')
257
258
        inputs = []
259
        if input:
260
            inputs.append(input)
261
        input_names = args
262
        if in_name:
263
            args.append(in_name)
264
        for in_name in input_names:
265
            try:
266
                inputs.append(self._read_input(None, in_name))
267
            except IOError, e:
268
                if e.errno == errno.ENOENT:
269
                    return (1, None,
270
                            '%s: No such file or directory\n' % (in_name,))
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
271
        # Basically cat copy input to output
4665.5.19 by Vincent Ladeuil
Implement globbing and enhance cat to accept multiple files.
272
        output = ''.join(inputs)
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
273
        # Handle output redirections
4665.5.18 by Vincent Ladeuil
Better redirection handling.
274
        try:
275
            output = self._write_output(output, out_name, out_mode)
276
        except IOError, e:
277
            if e.errno == errno.ENOENT:
278
                return 1, None, '%s: No such file or directory\n' % (out_name,)
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
279
        return 0, output, None
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
280
281
    def do_echo(self, input, args):
282
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
283
        if input and args:
284
                raise SyntaxError('Specify parameters OR use redirection')
285
        if args:
4665.5.20 by Vincent Ladeuil
Fixed as per Martin's review.
286
            input = ' '.join(args)
4665.5.18 by Vincent Ladeuil
Better redirection handling.
287
        try:
288
            input = self._read_input(input, in_name)
289
        except IOError, e:
290
            if e.errno == errno.ENOENT:
291
                return 1, None, '%s: No such file or directory\n' % (in_name,)
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
292
        # Always append a \n'
293
        input += '\n'
294
        # Process output
295
        output = input
296
        # Handle output redirections
4665.5.18 by Vincent Ladeuil
Better redirection handling.
297
        try:
298
            output = self._write_output(output, out_name, out_mode)
299
        except IOError, e:
300
            if e.errno == errno.ENOENT:
301
                return 1, None, '%s: No such file or directory\n' % (out_name,)
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
302
        return 0, output, None
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
303
304
    def _ensure_in_jail(self, path):
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
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))
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
308
309
    def do_cd(self, input, args):
310
        if len(args) > 1:
311
            raise SyntaxError('Usage: cd [dir]')
312
        if len(args) == 1:
313
            d = args[0]
314
            self._ensure_in_jail(d)
315
        else:
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
316
            d = self.test_case.get_jail_root()
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
317
        os.chdir(d)
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
318
        return 0, None, None
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
319
320
    def do_mkdir(self, input, args):
321
        if not args or len(args) != 1:
322
            raise SyntaxError('Usage: mkdir dir')
323
        d = args[0]
324
        self._ensure_in_jail(d)
325
        os.mkdir(d)
4665.5.15 by Vincent Ladeuil
Catch the retcode for all commands.
326
        return 0, None, None
4665.5.6 by Vincent Ladeuil
Implement 'bzr' command.
327
4665.5.17 by Vincent Ladeuil
Implement 'rm' command.
328
    def do_rm(self, input, args):
329
        err = None
330
331
        def error(msg, path):
332
            return  "rm: cannot remove '%s': %s\n" % (path, msg)
333
334
        force, recursive = False, False
335
        opts = None
336
        if args and args[0][0] == '-':
337
            opts = args.pop(0)[1:]
338
            if 'f' in opts:
339
                force = True
340
                opts = opts.replace('f', '', 1)
341
            if 'r' in opts:
342
                recursive = True
343
                opts = opts.replace('r', '', 1)
344
        if not args or opts:
345
            raise SyntaxError('Usage: rm [-fr] path+')
346
        for p in args:
347
            self._ensure_in_jail(p)
348
            # FIXME: Should we put that in osutils ?
349
            try:
350
                os.remove(p)
351
            except OSError, e:
4707.1.1 by Vincent Ladeuil
Fix OSX and FreeBSD failures.
352
                # Various OSes raises different exceptions (linux: EISDIR,
353
                #   win32: EACCES, OSX: EPERM) when invoked on a directory
354
                if e.errno in (errno.EISDIR, errno.EPERM, errno.EACCES):
4665.5.17 by Vincent Ladeuil
Implement 'rm' command.
355
                    if recursive:
356
                        osutils.rmtree(p)
357
                    else:
358
                        err = error('Is a directory', p)
359
                        break
360
                elif e.errno == errno.ENOENT:
361
                    if not force:
362
                        err =  error('No such file or directory', p)
363
                        break
364
                else:
365
                    raise
366
        if err:
367
            retcode = 1
368
        else:
369
            retcode = 0
370
        return retcode, None, err
371
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
372
4665.5.11 by Vincent Ladeuil
Create a new test case based on TestCaseWithMemoryTransport.
373
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
374
375
    def setUp(self):
376
        super(TestCaseWithMemoryTransportAndScript, self).setUp()
377
        self.script_runner = ScriptRunner(self)
378
        # Break the circular dependency
379
        def break_dependency():
380
            self.script_runner = None
381
        self.addCleanup(break_dependency)
382
383
    def get_jail_root(self):
384
        raise NotImplementedError(self.get_jail_root)
385
386
    def run_script(self, script):
387
        return self.script_runner.run_script(script)
388
389
    def run_command(self, cmd, input, output, error):
390
        return self.script_runner.run_command(cmd, input, output, error)
391
392
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
393
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
394
395
    def setUp(self):
396
        super(TestCaseWithTransportAndScript, self).setUp()
397
        self.script_runner = ScriptRunner(self)
398
        # Break the circular dependency
399
        def break_dependency():
400
            self.script_runner = None
401
        self.addCleanup(break_dependency)
402
403
    def get_jail_root(self):
404
        return self.test_dir
405
406
    def run_script(self, script):
407
        return self.script_runner.run_script(script)
408
409
    def run_command(self, cmd, input, output, error):
410
        return self.script_runner.run_command(cmd, input, output, error)