/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
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
99
import os
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
100
import shlex
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
101
from cStringIO import StringIO
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
102
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
103
from bzrlib import (
104
    osutils,
105
    tests,
106
    )
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
107
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
108
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
109
def split(s):
110
    """Split a command line respecting quotes."""
111
    scanner = shlex.shlex(s)
112
    scanner.quotes = '\'"`'
113
    scanner.whitespace_split = True
114
    for t in list(scanner):
115
        # Strip the simple and double quotes since we don't care about them.
116
        # We leave the backquotes in place though since they have a different
117
        # semantic.
118
        if t[0] in  ('"', "'") and t[0] == t[-1]:
119
            yield t[1:-1]
120
        else:
121
            yield t
122
123
124
def _script_to_commands(text, file_name=None):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
125
    """Turn a script into a list of commands with their associated IOs.
126
127
    Each command appears on a line by itself. It can be associated with an
128
    input that will feed it and an expected output.
129
    Comments starts with '#' until the end of line.
130
    Empty lines are ignored.
131
    Input and output are full lines terminated by a '\n'.
132
    Input lines start with '<'.
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
133
    Output lines start with '>'.
134
    Error lines start with '2>'.
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
135
    """
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
136
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
137
    commands = []
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
138
139
    def add_command(cmd, input, output, error):
140
        if cmd is not None:
141
            if input is not None:
142
                input = ''.join(input)
143
            if output is not None:
144
                output = ''.join(output)
145
            if error is not None:
146
                error = ''.join(error)
147
            commands.append((cmd, input, output, error))
148
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
149
    cmd_cur = None
150
    cmd_line = 1
151
    lineno = 0
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
152
    input, output, error = None, None, None
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
153
    for line in text.split('\n'):
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
154
        lineno += 1
155
        # Keep a copy for error reporting
156
        orig = line
157
        comment =  line.find('#')
158
        if comment >= 0:
159
            # Delete comments
160
            line = line[0:comment]
161
            line = line.rstrip()
162
        if line == '':
163
            # Ignore empty lines
164
            continue
165
        if line.startswith('<'):
166
            if input is None:
167
                if cmd_cur is None:
168
                    raise SyntaxError('No command for that input',
169
                                      (file_name, lineno, 1, orig))
170
                input = []
171
            input.append(line[1:] + '\n')
172
            continue
173
        elif line.startswith('>'):
174
            if output is None:
175
                if cmd_cur is None:
176
                    raise SyntaxError('No command for that output',
177
                                      (file_name, lineno, 1, orig))
178
                output = []
179
            output.append(line[1:] + '\n')
180
            continue
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
181
        elif line.startswith('2>'):
182
            if error is None:
183
                if cmd_cur is None:
184
                    raise SyntaxError('No command for that error',
185
                                      (file_name, lineno, 1, orig))
186
                error = []
187
            error.append(line[2:] + '\n')
188
            continue
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
189
        else:
190
            # Time to output the current command
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
191
            add_command(cmd_cur, input, output, error)
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
192
            # And start a new one
4665.5.2 by Vincent Ladeuil
Handle simple, double and back quotes.
193
            cmd_cur = list(split(line))
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
194
            cmd_line = lineno
4665.5.3 by Vincent Ladeuil
Separate error from normal output.
195
            input, output, error = None, None, None
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
196
    # Add the last seen command
4665.5.7 by Vincent Ladeuil
Simplify output/errors handling.
197
    add_command(cmd_cur, input, output, error)
4665.5.1 by Vincent Ladeuil
Start some shell-like capability to write tests.
198
    return commands
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
199
200
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
201
def _scan_redirection_options(args):
202
    """Recognize and process input and output redirections.
203
204
    :param args: The command line arguments
205
206
    :return: A tuple containing: 
207
        - The file name redirected from or None
208
        - The file name redirected to or None
209
        - The mode to open the output file or None
210
        - The reamining arguments
211
    """
212
    remaining = []
213
    in_name = None
214
    out_name, out_mode = None, None
215
    for arg in  args:
216
        if arg.startswith('<'):
217
            in_name = arg[1:]
218
        elif arg.startswith('>>'):
219
            out_name = arg[2:]
220
            out_mode = 'ab+'
221
        elif arg.startswith('>'):
222
            out_name = arg[1:]
223
            out_mode = 'wb+'
224
        else:
225
            remaining.append(arg)
226
    return in_name, out_name, out_mode, remaining
227
228
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
229
class ScriptRunner(object):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
230
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
231
    def __init__(self, test_case):
232
        self.test_case = test_case
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
233
        self.output_checker = doctest.OutputChecker()
234
        self.check_options = doctest.ELLIPSIS
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
235
236
    def run_script(self, text):
237
        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
238
            self.run_command(cmd, input, output, error)
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
239
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
240
    def _check_output(self, expected, actual):
241
        if expected is None:
4665.5.6 by Vincent Ladeuil
Implement 'bzr' command.
242
            # Specifying None means: any output is accepted
243
            return
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
244
        if actual is None:
245
            self.test_case.fail('Unexpected: %s' % actual)
246
        matching = self.output_checker.check_output(
247
            expected, actual, self.check_options)
248
        if not matching:
249
            # 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
250
            # because... the API is broken ('expected' must be a doctest
251
            # specific object of which a 'want' attribute will be our
252
            # 'expected' parameter. So we just fallback to our good old
253
            # assertEqualDiff since we know there *are* differences and the
254
            # output should be decently readable.
4665.5.12 by Vincent Ladeuil
Support '...' in expected strings.
255
            self.test_case.assertEqualDiff(expected, actual)
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
256
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
257
    def run_command(self, cmd, input, output, error):
258
        mname = 'do_' + cmd[0]
259
        method = getattr(self, mname, None)
260
        if method is None:
261
            raise SyntaxError('Command not found "%s"' % (cmd[0],),
262
                              None, 1, ' '.join(cmd))
263
        if input is None:
264
            str_input = ''
265
        else:
266
            str_input = ''.join(input)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
267
        retcode, actual_output, actual_error = method(str_input, cmd[1:])
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
268
269
        self._check_output(output, actual_output)
270
        self._check_output(error, actual_error)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
271
        if retcode and not error and actual_error:
4665.5.13 by Vincent Ladeuil
Script execution must stop on unexpected errors.
272
            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
273
        return retcode, actual_output, actual_error
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
274
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
275
    def _read_input(self, input, in_name):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
276
        if in_name is not None:
277
            infile = open(in_name, 'rb')
278
            try:
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
279
                # Command redirection takes precedence over provided input
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
280
                input = infile.read()
281
            finally:
282
                infile.close()
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
283
        return input
284
285
    def _write_output(self, output, out_name, out_mode):
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
286
        if out_name is not None:
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
287
            outfile = open(out_name, out_mode)
4665.5.4 by Vincent Ladeuil
Implement a 'cat' command.
288
            try:
289
                outfile.write(output)
290
            finally:
291
                outfile.close()
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
292
            output = None
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
293
        return output
294
295
    def do_bzr(self, input, args):
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
296
        retcode, out, err = self.test_case._run_bzr_core(
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
297
            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
298
        return retcode, out, err
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
299
300
    def do_cat(self, input, args):
301
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
302
        if len(args) > 1:
303
            raise SyntaxError('Usage: cat [file1]')
304
        if args:
305
            if in_name is not None:
306
                raise SyntaxError('Specify a file OR use redirection')
307
            in_name = args[0]
308
        input = self._read_input(input, in_name)
309
        # Basically cat copy input to output
310
        output = input
311
        # Handle output redirections
312
        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
313
        return 0, output, None
4665.5.8 by Vincent Ladeuil
Implement 'echo' command.
314
315
    def do_echo(self, input, args):
316
        (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
317
        if input and args:
318
                raise SyntaxError('Specify parameters OR use redirection')
319
        if args:
320
            input = ''.join(args)
321
        input = self._read_input(input, in_name)
322
        # Always append a \n'
323
        input += '\n'
324
        # Process output
325
        output = input
326
        # Handle output redirections
327
        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
328
        return 0, output, None
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
329
330
    def _ensure_in_jail(self, path):
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
331
        jail_root = self.test_case.get_jail_root()
332
        if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
333
            raise ValueError('%s is not inside %s' % (path, jail_root))
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
334
335
    def do_cd(self, input, args):
336
        if len(args) > 1:
337
            raise SyntaxError('Usage: cd [dir]')
338
        if len(args) == 1:
339
            d = args[0]
340
            self._ensure_in_jail(d)
341
        else:
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
342
            d = self.test_case.get_jail_root()
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
343
        os.chdir(d)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
344
        return 0, None, None
4665.5.5 by Vincent Ladeuil
Implement 'cd' and 'mkdir'.
345
346
    def do_mkdir(self, input, args):
347
        if not args or len(args) != 1:
348
            raise SyntaxError('Usage: mkdir dir')
349
        d = args[0]
350
        self._ensure_in_jail(d)
351
        os.mkdir(d)
4597.4.5 by Vincent Ladeuil
Merge shell-like tests patch to define errors on status
352
        return 0, None, None
4665.5.6 by Vincent Ladeuil
Implement 'bzr' command.
353
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
354
4665.5.11 by Vincent Ladeuil
Create a new test case based on TestCaseWithMemoryTransport.
355
class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
356
357
    def setUp(self):
358
        super(TestCaseWithMemoryTransportAndScript, self).setUp()
359
        self.script_runner = ScriptRunner(self)
360
        # Break the circular dependency
361
        def break_dependency():
362
            self.script_runner = None
363
        self.addCleanup(break_dependency)
364
365
    def get_jail_root(self):
366
        raise NotImplementedError(self.get_jail_root)
367
368
    def run_script(self, script):
369
        return self.script_runner.run_script(script)
370
371
    def run_command(self, cmd, input, output, error):
372
        return self.script_runner.run_command(cmd, input, output, error)
373
374
4665.5.10 by Vincent Ladeuil
Start separating the script runner from the test case.
375
class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
376
377
    def setUp(self):
378
        super(TestCaseWithTransportAndScript, self).setUp()
379
        self.script_runner = ScriptRunner(self)
380
        # Break the circular dependency
381
        def break_dependency():
382
            self.script_runner = None
383
        self.addCleanup(break_dependency)
384
385
    def get_jail_root(self):
386
        return self.test_dir
387
388
    def run_script(self, script):
389
        return self.script_runner.run_script(script)
390
391
    def run_command(self, cmd, input, output, error):
392
        return self.script_runner.run_command(cmd, input, output, error)