/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.9 by Vincent Ladeuil
Start adding doc.
16
"""Shell-like test scripts.
17
18
This allows users to write tests in a syntax very close to a shell session,
19
using a restricted and limited set of commands that should be enough to mimic
20
most of the behaviours.
21
22
A script is a set of commands, each command is composed of:
23
- one mandatory command line,
24
- one optional set of input lines to feed the command,
25
- one optional set of output expected lines,
26
- one optional set of error expected lines.
27
28
The optional lines starts with a special string (mnemonic: shell redirection):
29
- '<' for input,
30
- '>' for output,
31
- '2>' for errors,
32
33
The execution stops as soon as an expected output or an expected error is not
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
34
matched. 
35
36
When no output is specified, any ouput from the command is accepted
37
and let the execution continue. 
38
39
If an error occurs and no expected error is specified, the execution stops.
40
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
41
An error is defined by a returned status different from zero, not by the
42
presence of text on the error stream.
43
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
44
The matching is done on a full string comparison basis unless '...' is used, in
45
which case expected output/errors can be lees precise.
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
46
47
Examples:
48
49
The following will succeeds only if 'bzr add' outputs 'adding file'.
4665.5.9 by Vincent Ladeuil
Start adding doc.
50
51
  bzr add file
52
  >adding file
53
54
If you want the command to succeed for any output, just use:
55
56
  bzr add file
57
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
58
The following will stop with an error:
59
60
  bzr not-a-command
61
62
If you want it to succeed, use:
63
64
  bzr not-a-command
65
  2> bzr: ERROR: unknown command "not-a-command"
66
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
67
You can use ellipsis (...) to replace any piece of text you don't want to be
4665.5.13 by Vincent Ladeuil
Script execution must stop on unexpected errors.
68
matched exactly:
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
69
70
  bzr branch not-a-branch
71
  2>bzr: ERROR: Not a branch...not-a-branch/".
72
73
4665.5.13 by Vincent Ladeuil
Script execution must stop on unexpected errors.
74
This can be used to ignore entire lines too:
75
76
cat
77
<first line
78
<second line
79
<third line
80
<fourth line
81
<last line
82
>first line
83
>...
84
>last line
85
86
You can check the content of a file with cat:
87
88
  cat <file
89
  >expected content
90
91
You can also check the existence of a file with cat, the following will fail if
92
the file doesn't exist:
93
94
  cat file
95
4665.5.9 by Vincent Ladeuil
Start adding doc.
96
"""
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
97
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
98
import doctest
4597.4.6 by Vincent Ladeuil
Merge shell-like rm implementation
99
import errno
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
100
import os
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
101
import shlex
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
102
from cStringIO import StringIO
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
103
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
104
from bzrlib import (
105
    osutils,
106
    tests,
107
    )
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
108
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
109
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
110
def split(s):
111
    """Split a command line respecting quotes."""
112
    scanner = shlex.shlex(s)
113
    scanner.quotes = '\'"`'
114
    scanner.whitespace_split = True
115
    for t in list(scanner):
116
        # Strip the simple and double quotes since we don't care about them.
117
        # We leave the backquotes in place though since they have a different
118
        # semantic.
119
        if t[0] in  ('"', "'") and t[0] == t[-1]:
120
            yield t[1:-1]
121
        else:
122
            yield t
123
124
125
def _script_to_commands(text, file_name=None):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
126
    """Turn a script into a list of commands with their associated IOs.
127
128
    Each command appears on a line by itself. It can be associated with an
129
    input that will feed it and an expected output.
130
    Comments starts with '#' until the end of line.
131
    Empty lines are ignored.
132
    Input and output are full lines terminated by a '\n'.
133
    Input lines start with '<'.
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
134
    Output lines start with '>'.
135
    Error lines start with '2>'.
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
136
    """
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
137
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
138
    commands = []
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
139
140
    def add_command(cmd, input, output, error):
141
        if cmd is not None:
142
            if input is not None:
143
                input = ''.join(input)
144
            if output is not None:
145
                output = ''.join(output)
146
            if error is not None:
147
                error = ''.join(error)
148
            commands.append((cmd, input, output, error))
149
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
150
    cmd_cur = None
151
    cmd_line = 1
152
    lineno = 0
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
153
    input, output, error = None, None, None
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
154
    for line in text.split('\n'):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
155
        lineno += 1
156
        # Keep a copy for error reporting
157
        orig = line
158
        comment =  line.find('#')
159
        if comment >= 0:
160
            # Delete comments
161
            line = line[0:comment]
162
            line = line.rstrip()
163
        if line == '':
164
            # Ignore empty lines
165
            continue
166
        if line.startswith('<'):
167
            if input is None:
168
                if cmd_cur is None:
169
                    raise SyntaxError('No command for that input',
170
                                      (file_name, lineno, 1, orig))
171
                input = []
172
            input.append(line[1:] + '\n')
173
            continue
174
        elif line.startswith('>'):
175
            if output is None:
176
                if cmd_cur is None:
177
                    raise SyntaxError('No command for that output',
178
                                      (file_name, lineno, 1, orig))
179
                output = []
180
            output.append(line[1:] + '\n')
181
            continue
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
182
        elif line.startswith('2>'):
183
            if error is None:
184
                if cmd_cur is None:
185
                    raise SyntaxError('No command for that error',
186
                                      (file_name, lineno, 1, orig))
187
                error = []
188
            error.append(line[2:] + '\n')
189
            continue
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
190
        else:
191
            # Time to output the current command
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
192
            add_command(cmd_cur, input, output, error)
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
193
            # And start a new one
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
194
            cmd_cur = list(split(line))
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
195
            cmd_line = lineno
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
196
            input, output, error = None, None, None
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
197
    # Add the last seen command
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
198
    add_command(cmd_cur, input, output, error)
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
199
    return commands
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
200
201
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
202
def _scan_redirection_options(args):
203
    """Recognize and process input and output redirections.
204
205
    :param args: The command line arguments
206
207
    :return: A tuple containing: 
208
        - The file name redirected from or None
209
        - The file name redirected to or None
210
        - The mode to open the output file or None
211
        - The reamining arguments
212
    """
213
    remaining = []
214
    in_name = None
215
    out_name, out_mode = None, None
216
    for arg in  args:
217
        if arg.startswith('<'):
218
            in_name = arg[1:]
219
        elif arg.startswith('>>'):
220
            out_name = arg[2:]
221
            out_mode = 'ab+'
222
        elif arg.startswith('>'):
223
            out_name = arg[1:]
224
            out_mode = 'wb+'
225
        else:
226
            remaining.append(arg)
227
    return in_name, out_name, out_mode, remaining
228
229
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
230
class ScriptRunner(object):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
231
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
232
    def __init__(self, test_case):
233
        self.test_case = test_case
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
234
        self.output_checker = doctest.OutputChecker()
235
        self.check_options = doctest.ELLIPSIS
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
236
237
    def run_script(self, text):
238
        for cmd, input, output, error in _script_to_commands(text):
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
239
            self.run_command(cmd, input, output, error)
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
240
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
241
    def _check_output(self, expected, actual):
242
        if expected is None:
4665.5.6 by Vincent Ladeuil
Implement 'bzr' command.
243
            # Specifying None means: any output is accepted
244
            return
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
245
        if actual is None:
246
            self.test_case.fail('Unexpected: %s' % actual)
247
        matching = self.output_checker.check_output(
248
            expected, actual, self.check_options)
249
        if not matching:
250
            # Note that we can't use output_checker.output_difference() here
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
251
            # because... the API is broken ('expected' must be a doctest
252
            # specific object of which a 'want' attribute will be our
253
            # 'expected' parameter. So we just fallback to our good old
254
            # assertEqualDiff since we know there *are* differences and the
255
            # output should be decently readable.
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
256
            self.test_case.assertEqualDiff(expected, actual)
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
257
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
258
    def run_command(self, cmd, input, output, error):
259
        mname = 'do_' + cmd[0]
260
        method = getattr(self, mname, None)
261
        if method is None:
262
            raise SyntaxError('Command not found "%s"' % (cmd[0],),
263
                              None, 1, ' '.join(cmd))
264
        if input is None:
265
            str_input = ''
266
        else:
267
            str_input = ''.join(input)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
268
        retcode, actual_output, actual_error = method(str_input, cmd[1:])
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
269
270
        self._check_output(output, actual_output)
271
        self._check_output(error, actual_error)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
272
        if retcode and not error and actual_error:
4665.5.13 by Vincent Ladeuil
Script execution must stop on unexpected errors.
273
            self.test_case.fail('Unexpected error: %s' % actual_error)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
274
        return retcode, actual_output, actual_error
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
275
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
276
    def _read_input(self, input, in_name):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
277
        if in_name is not None:
278
            infile = open(in_name, 'rb')
279
            try:
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
280
                # Command redirection takes precedence over provided input
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
281
                input = infile.read()
282
            finally:
283
                infile.close()
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
284
        return input
285
286
    def _write_output(self, output, out_name, out_mode):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
287
        if out_name is not None:
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
288
            outfile = open(out_name, out_mode)
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
289
            try:
290
                outfile.write(output)
291
            finally:
292
                outfile.close()
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
293
            output = None
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
294
        return output
295
296
    def do_bzr(self, input, args):
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
297
        retcode, out, err = self.test_case._run_bzr_core(
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
298
            args, retcode=None, encoding=None, stdin=input, working_dir=None)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
299
        return retcode, out, err
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
300
301
    def do_cat(self, input, args):
302
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
303
        if len(args) > 1:
304
            raise SyntaxError('Usage: cat [file1]')
305
        if args:
306
            if in_name is not None:
307
                raise SyntaxError('Specify a file OR use redirection')
308
            in_name = args[0]
309
        input = self._read_input(input, in_name)
310
        # Basically cat copy input to output
311
        output = input
312
        # Handle output redirections
313
        output = self._write_output(output, out_name, out_mode)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
314
        return 0, output, None
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
315
316
    def do_echo(self, input, args):
317
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
318
        if input and args:
319
                raise SyntaxError('Specify parameters OR use redirection')
320
        if args:
321
            input = ''.join(args)
322
        input = self._read_input(input, in_name)
323
        # Always append a \n'
324
        input += '\n'
325
        # Process output
326
        output = input
327
        # Handle output redirections
328
        output = self._write_output(output, out_name, out_mode)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
329
        return 0, output, None
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
330
331
    def _ensure_in_jail(self, path):
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
332
        jail_root = self.test_case.get_jail_root()
333
        if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
334
            raise ValueError('%s is not inside %s' % (path, jail_root))
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
335
336
    def do_cd(self, input, args):
337
        if len(args) > 1:
338
            raise SyntaxError('Usage: cd [dir]')
339
        if len(args) == 1:
340
            d = args[0]
341
            self._ensure_in_jail(d)
342
        else:
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
343
            d = self.test_case.get_jail_root()
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
344
        os.chdir(d)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
345
        return 0, None, None
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
346
347
    def do_mkdir(self, input, args):
348
        if not args or len(args) != 1:
349
            raise SyntaxError('Usage: mkdir dir')
350
        d = args[0]
351
        self._ensure_in_jail(d)
352
        os.mkdir(d)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
353
        return 0, None, None
4665.5.6 by Vincent Ladeuil
Implement 'bzr' command.
354
4597.4.6 by Vincent Ladeuil
Merge shell-like rm implementation
355
    def do_rm(self, input, args):
356
        err = None
357
358
        def error(msg, path):
359
            return  "rm: cannot remove '%s': %s\n" % (path, msg)
360
361
        force, recursive = False, False
362
        opts = None
363
        if args and args[0][0] == '-':
364
            opts = args.pop(0)[1:]
365
            if 'f' in opts:
366
                force = True
367
                opts = opts.replace('f', '', 1)
368
            if 'r' in opts:
369
                recursive = True
370
                opts = opts.replace('r', '', 1)
371
        if not args or opts:
372
            raise SyntaxError('Usage: rm [-fr] path+')
373
        for p in args:
374
            self._ensure_in_jail(p)
375
            # FIXME: Should we put that in osutils ?
376
            try:
377
                os.remove(p)
378
            except OSError, e:
379
                if e.errno == errno.EISDIR:
380
                    if recursive:
381
                        osutils.rmtree(p)
382
                    else:
383
                        err = error('Is a directory', p)
384
                        break
385
                elif e.errno == errno.ENOENT:
386
                    if not force:
387
                        err =  error('No such file or directory', p)
388
                        break
389
                else:
390
                    raise
391
        if err:
392
            retcode = 1
393
        else:
394
            retcode = 0
395
        return retcode, None, err
396
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
397
4665.5.11 by Vincent Ladeuil
Create a new test case based on TestCaseWithMemoryTransport.
398
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
399
400
    def setUp(self):
401
        super(TestCaseWithMemoryTransportAndScript, self).setUp()
402
        self.script_runner = ScriptRunner(self)
403
        # Break the circular dependency
404
        def break_dependency():
405
            self.script_runner = None
406
        self.addCleanup(break_dependency)
407
408
    def get_jail_root(self):
409
        raise NotImplementedError(self.get_jail_root)
410
411
    def run_script(self, script):
412
        return self.script_runner.run_script(script)
413
414
    def run_command(self, cmd, input, output, error):
415
        return self.script_runner.run_command(cmd, input, output, error)
416
417
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
418
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
419
420
    def setUp(self):
421
        super(TestCaseWithTransportAndScript, self).setUp()
422
        self.script_runner = ScriptRunner(self)
423
        # Break the circular dependency
424
        def break_dependency():
425
            self.script_runner = None
426
        self.addCleanup(break_dependency)
427
428
    def get_jail_root(self):
429
        return self.test_dir
430
431
    def run_script(self, script):
432
        return self.script_runner.run_script(script)
433
434
    def run_command(self, cmd, input, output, error):
435
        return self.script_runner.run_command(cmd, input, output, error)