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