Merge lp:~vila/bzr/shell-like-tests into lp:bzr

Proposed by Vincent Ladeuil
Status: Rejected
Rejected by: Vincent Ladeuil
Proposed branch: lp:~vila/bzr/shell-like-tests
Merge into: lp:bzr
Diff against target: None lines
To merge this branch: bzr merge lp:~vila/bzr/shell-like-tests
Reviewer Review Type Date Requested Status
Martin Pool Needs Fixing
Review via email: mp+11204@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

This patch address some points raised during the review of the first proposal:
- document the ways the scripts can be used, including some assertions,
- stop the script execution on unexpected errors
- allow the '...' doctest notation
- separate the script runner from the test cases

It doesn't address:
- running the scripts without touching the disk

But I really like the idea that it could be done :)
Thinking a bit about that from John remark, I came to the conclusion that:
1) I didn't intend to make it happen *now*
2) It is certainly possible to get there as long as we find a way to:
   a) Use a local transport in WorkingTree and TreeTransform objects
   b) Trap and override all calls to file() open() and os. calls (osutils is a good start)
   c) Manage to trick dirstate to work in memory too :)

Given that the above is non-trivial, I'd like to land at least a usable version of this shell-like
tests feature and file bugs about all missing bits.

The initial intent was to address a UI hole, let's focus on that first and optimize the implementation later.

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (23.4 KiB)

This is looking pretty cool.

=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2009-08-28 21:05:31 +0000
+++ bzrlib/tests/__init__.py 2009-09-01 08:24:44 +0000
@@ -3618,6 +3618,7 @@
         'bzrlib.tests.test_rio',
         'bzrlib.tests.test_rules',
         'bzrlib.tests.test_sampler',
+ 'bzrlib.tests.test_script',
         'bzrlib.tests.test_selftest',
         'bzrlib.tests.test_serializer',
         'bzrlib.tests.test_setup',

=== added file 'bzrlib/tests/script.py'
--- bzrlib/tests/script.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/script.py 2009-09-04 14:55:59 +0000
@@ -0,0 +1,389 @@
+# Copyright (C) 2009 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""Shell-like test scripts.

There should be two blank lines above that.

+
+This allows users to write tests in a syntax very close to a shell session,
+using a restricted and limited set of commands that should be enough to mimic
+most of the behaviours.

This is a nice description. However, I think it's probably going to be
more discoverable and more point-to-able if it's in the developer
documentation rather than in the module docstring.

It would be kinda nice if pydoc handled this differently but
realistically I think the developer docs are much more likely to be read
before reading the code than the pydocs are.

+
+A script is a set of commands, each command is composed of:
+- one mandatory command line,
+- one optional set of input lines to feed the command,
+- one optional set of output expected lines,
+- one optional set of error expected lines.

.. in that order?

+
+The optional lines starts with a special string (mnemonic: shell redirection):
+- '<' for input,
+- '>' for output,
+- '2>' for errors,

Hm, I guess we'll see how the examples look but I think this may not be
the clearest description of it. How about instead

  $ the command
  < the input
  output by default
  2> errors

and I'm not so convinced people will find the errors bit obvious. Do we
really need to distinguish them from stdout?

+
+The execution stops as soon as an expected output or an expected error is not
+matched.
+
+When no output is specified, any ouput from the command is accepted
+and let the execution continue.

'output', 'is accepted and execution continues'.

Is this a good idea though? I feel like I'd like at least '...' so that
there's some indication that stuff will happen but we don't care.

+
+If an error occurs and no expected error is specified, the execution stops.
+
+The ...

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (7.7 KiB)

>>>>> "martin" == Martin Pool <email address hidden> writes:

    martin> Review: Needs Fixing
    martin> This is looking pretty cool.

Hey, there was so much comments I missed that introductory one
:-D

<snip/>

    martin> This is a nice description. However, I think it's
    martin> probably going to be more discoverable and more
    martin> point-to-able if it's in the developer documentation
    martin> rather than in the module docstring.

Done.

    martin> +
    martin> +A script is a set of commands, each command is composed of:
    martin> +- one mandatory command line,
    martin> +- one optional set of input lines to feed the command,
    martin> +- one optional set of output expected lines,
    martin> +- one optional set of error expected lines.

    martin> .. in that order?

No, there is no constraints on the order, in fact the lines can
even be mixed if you wish.

    martin> +
    martin> +The optional lines starts with a special string (mnemonic: shell redirection):
    martin> +- '<' for input,
    martin> +- '>' for output,
    martin> +- '2>' for errors,

    martin> Hm, I guess we'll see how the examples look but I think this may not be
    martin> the clearest description of it. How about instead

    martin> $ the command
    martin> < the input
    martin> output by default
    2> errors

Done.

    martin> and I'm not so convinced people will find the errors
    martin> bit obvious. Do we really need to distinguish them
    martin> from stdout?

Yes. I didn't at first, and then I realize that:

- most of the time you don't want to check the output and you
  don't care about it.

- most of the time you don't want to check stderr and you don't
  care about it,

- you want the script to stop if an error occurs (which is not
  related to stderr being empty or not), note that this has been
  added in a later revision that you haven't reviewed yet,

- separating output and errors allows the script to match against
  one OR the other OR both (but I've yet to use the later).

<snip/>

    martin> 'output', 'is accepted and execution continues'.

    martin> Is this a good idea though? I feel like I'd like at
    martin> least '...' so that there's some indication that
    martin> stuff will happen but we don't care.

I thought so first too, but then each time you use 'init', 'add',
'st', well, any command with output you quickly don't want to
check neither output nor errors :D

In fact expected output and expected errors are mostly used as
assertions in conventional tests, so mainly you use them at the
end of the script and sometimes you embed some. At least that's
my limited experience so far.

<snip/>

    martin> As ReST style you should put a double colon at the end of the line ::

Done.

<snip/>

    martin> +You can check the content of a file with cat:
    martin> +
    martin> + cat <file
    martin> + >expected content

    martin> Is the redirection needed?

Not anymore, it was when I first wrote the example, but then I
needed to enhance cat anyway.

<snip/>

    martin> +def split(s):
    martin> + """Split a command line respecting quotes."""

    martin> Better name?

It's the same that the c...

Read more...

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (3.8 KiB)

2009/9/18 Vincent Ladeuil <email address hidden>:

>    martin> +
>    martin> +A script is a set of commands, each command is composed of:
>    martin> +- one mandatory command line,
>    martin> +- one optional set of input lines to feed the command,
>    martin> +- one optional set of output expected lines,
>    martin> +- one optional set of error expected lines.
>
>    martin> .. in that order?
>
> No, there is no constraints on the order, in fact the lines can
> even be mixed if you wish.

I think you should say so in the docs.

>    martin> 'output', 'is accepted and execution continues'.
>
>    martin> Is this a good idea though?  I feel like I'd like at
>    martin> least '...' so that there's some indication that
>    martin> stuff will happen but we don't care.
>
> I thought so first too, but then each time you use 'init', 'add',
> 'st', well, any command with output you quickly don't want to
> check neither output nor errors :D
>
> In fact expected output and expected errors are mostly used as
> assertions in conventional tests, so mainly you use them at the
> end of the script and sometimes you embed some. At least that's
> my limited experience so far.

OK, but would it be too bad to say:

  $ bzr init
  ...
  $ bzr add foo
  ...

> <snip/>
>    martin> +class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
>
>    martin> This is a bit like what I was talking about with
>    martin> Robert on the list today: I don't like the idea of
>    martin> entangling tests with base classes so that you have
>    martin> to inherit from a particular test.  I'd like someone
>    martin> to be able to just use this style within a particular
>    martin> test without having to move it to a different class
>    martin> or to change the whole TestCase class to a different
>    martin> parent.
>
> I have the opposite position, I think we should write more test
> classes and put our tests in more specialized classes.

I don't object to that.

> Many of the refactorings I've done these last months have been in
> that direction:
>
> - bring a bunch of related tests,
> - put them in the same class,
> - add a common setUp(),
> - remove the duplication at the beginning of all the tests.

I think that's fine too, except that I would tend to put the common
code in an explicitly named method, not in setUp() (see the other
thread.)

But none of this is convincing me that you need a specific parent
class for the particular case of using scripts and a memory transport.
 Why not just let people create scripts when they need them? It
doesn't need to be a static property of the test class. Or at the
very most, this could be a mixin superclass.

> And from that I got the strong feeling that defining *more* test
> classes help to write tests with better defect localization :D
>
>    martin> Would it be feasible to just have an object that
>    martin> tests can create and pass scripts to?
>
> And create it in the setUp ?

Why not just

  def test_add(self):
    run_script("""$bzr add foo
...
""")

>    martin> +    def test_echo_more_output(self):
>    martin> +        out, err = self.run_command(['echo', 'hello', 'happy', 'world'],
>...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> "martin" == Martin Pool <email address hidden> writes:

    martin> 2009/9/18 Vincent Ladeuil <email address hidden>:
    >>    martin> +
    >>    martin> +A script is a set of commands, each command is composed of:
    >>    martin> +- one mandatory command line,
    >>    martin> +- one optional set of input lines to feed the command,
    >>    martin> +- one optional set of output expected lines,
    >>    martin> +- one optional set of error expected lines.
    >>
    >>    martin> .. in that order?
    >>
    >> No, there is no constraints on the order, in fact the lines can
    >> even be mixed if you wish.

    martin> I think you should say so in the docs.

I did. (I didn't mention the intermix though as I think it has
little use and 1) will confuse the explanation, 2) can be
discovered by trial and error if you're really desperate.

<snip/>

    martin> OK, but would it be too bad to say:

    martin> $ bzr init
    martin> ...
    martin> $ bzr add foo
    martin> ...

It would double the size of the scripts, *all* of the scripts
I've written so far used that feature.

What you are expressing above is: run this command, ignore its
output, run that command ignore its output.

What I tend to use is: run this command, run that command.

Yet another way to look at it is that you don't generally add
'2>/dev/null' to all your commands.

<snip/>

    martin> But none of this is convincing me that you need a
    martin> specific parent class for the particular case of
    martin> using scripts and a memory transport. Why not just
    martin> let people create scripts when they need them? It
    martin> doesn't need to be a static property of the test
    martin> class. Or at the very most, this could be a mixin
    martin> superclass.

You need a test object to check the jail, Either you pass it as a
parameter to all calls to run_script, or you leave it to setUp().

<snip/>

    martin> Why not just

    martin> def test_add(self):
    martin> run_script("""$bzr add foo
    martin> ...
    martin> """)

Where is the ScriptRunner instance ?

<snip/>

    >>    martin> But these tests puzzle me a bit because I don't see
    >>    martin> why they're not using scripts directly?
    >>
    >> Because the output of a script is not specified, only commands
    >> can specify their expected outputs and errors.

    martin> sorry, I don't understand.

run_command returns retcode, actual_output, actual_error
run_script returns nothing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2009-08-28 21:05:31 +0000
+++ bzrlib/tests/__init__.py 2009-09-01 08:24:44 +0000
@@ -3618,6 +3618,7 @@
3618 'bzrlib.tests.test_rio',3618 'bzrlib.tests.test_rio',
3619 'bzrlib.tests.test_rules',3619 'bzrlib.tests.test_rules',
3620 'bzrlib.tests.test_sampler',3620 'bzrlib.tests.test_sampler',
3621 'bzrlib.tests.test_script',
3621 'bzrlib.tests.test_selftest',3622 'bzrlib.tests.test_selftest',
3622 'bzrlib.tests.test_serializer',3623 'bzrlib.tests.test_serializer',
3623 'bzrlib.tests.test_setup',3624 'bzrlib.tests.test_setup',
36243625
=== added file 'bzrlib/tests/script.py'
--- bzrlib/tests/script.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/script.py 2009-09-04 14:55:59 +0000
@@ -0,0 +1,389 @@
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
16"""Shell-like test scripts.
17
18This allows users to write tests in a syntax very close to a shell session,
19using a restricted and limited set of commands that should be enough to mimic
20most of the behaviours.
21
22A 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
28The optional lines starts with a special string (mnemonic: shell redirection):
29- '<' for input,
30- '>' for output,
31- '2>' for errors,
32
33The execution stops as soon as an expected output or an expected error is not
34matched.
35
36When no output is specified, any ouput from the command is accepted
37and let the execution continue.
38
39If an error occurs and no expected error is specified, the execution stops.
40
41The matching is done on a full string comparison basis unless '...' is used, in
42which case expected output/errors can be lees precise.
43
44Examples:
45
46The following will succeeds only if 'bzr add' outputs 'adding file'.
47
48 bzr add file
49 >adding file
50
51If you want the command to succeed for any output, just use:
52
53 bzr add file
54
55The following will stop with an error:
56
57 bzr not-a-command
58
59If you want it to succeed, use:
60
61 bzr not-a-command
62 2> bzr: ERROR: unknown command "not-a-command"
63
64You can use ellipsis (...) to replace any piece of text you don't want to be
65matched exactly:
66
67 bzr branch not-a-branch
68 2>bzr: ERROR: Not a branch...not-a-branch/".
69
70
71This can be used to ignore entire lines too:
72
73cat
74<first line
75<second line
76<third line
77<fourth line
78<last line
79>first line
80>...
81>last line
82
83You can check the content of a file with cat:
84
85 cat <file
86 >expected content
87
88You can also check the existence of a file with cat, the following will fail if
89the file doesn't exist:
90
91 cat file
92
93"""
94
95import doctest
96import os
97import shlex
98from cStringIO import StringIO
99
100from bzrlib import (
101 osutils,
102 tests,
103 )
104
105
106def 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
121def _script_to_commands(text, file_name=None):
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 '<'.
130 Output lines start with '>'.
131 Error lines start with '2>'.
132 """
133
134 commands = []
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
146 cmd_cur = None
147 cmd_line = 1
148 lineno = 0
149 input, output, error = None, None, None
150 for line in text.split('\n'):
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
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
186 else:
187 # Time to output the current command
188 add_command(cmd_cur, input, output, error)
189 # And start a new one
190 cmd_cur = list(split(line))
191 cmd_line = lineno
192 input, output, error = None, None, None
193 # Add the last seen command
194 add_command(cmd_cur, input, output, error)
195 return commands
196
197
198def _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
226class ScriptRunner(object):
227
228 def __init__(self, test_case):
229 self.test_case = test_case
230 self.output_checker = doctest.OutputChecker()
231 self.check_options = doctest.ELLIPSIS
232
233 def run_script(self, text):
234 for cmd, input, output, error in _script_to_commands(text):
235 out, err = self.run_command(cmd, input, output, error)
236
237 def _check_output(self, expected, actual):
238 if expected is None:
239 # Specifying None means: any output is accepted
240 return
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
251 # decently readable.
252 self.test_case.assertEqualDiff(expected, actual)
253
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:])
265
266 self._check_output(output, actual_output)
267 self._check_output(error, actual_error)
268 if not error and actual_error:
269 self.test_case.fail('Unexpected error: %s' % actual_error)
270 return actual_output, actual_error
271
272 def _read_input(self, input, in_name):
273 if in_name is not None:
274 infile = open(in_name, 'rb')
275 try:
276 # Command redirection takes precedence over provided input
277 input = infile.read()
278 finally:
279 infile.close()
280 return input
281
282 def _write_output(self, output, out_name, out_mode):
283 if out_name is not None:
284 outfile = open(out_name, out_mode)
285 try:
286 outfile.write(output)
287 finally:
288 outfile.close()
289 output = None
290 return output
291
292 def do_bzr(self, input, args):
293 out, err = self.test_case._run_bzr_core(
294 args, retcode=None, encoding=None, stdin=input, working_dir=None)
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)
325 return output, None
326
327 def _ensure_in_jail(self, path):
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))
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:
339 d = self.test_case.get_jail_root()
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
350
351
352class 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
372class 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)
0390
=== added file 'bzrlib/tests/test_script.py'
--- bzrlib/tests/test_script.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_script.py 2009-09-04 14:55:59 +0000
@@ -0,0 +1,257 @@
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
16
17
18from bzrlib import (
19 osutils,
20 tests,
21 )
22from bzrlib.tests import script
23
24
25class TestScriptSyntax(tests.TestCase):
26
27 def test_comment_is_ignored(self):
28 self.assertEquals([], script._script_to_commands('#comment\n'))
29
30 def test_empty_line_is_ignored(self):
31 self.assertEquals([], script._script_to_commands('\n'))
32
33 def test_simple_command(self):
34 self.assertEquals([(['cd', 'trunk'], None, None, None)],
35 script._script_to_commands('cd trunk'))
36
37 def test_command_with_single_quoted_param(self):
38 story = """bzr commit -m 'two words'"""
39 self.assertEquals([(['bzr', 'commit', '-m', 'two words'],
40 None, None, None)],
41 script._script_to_commands(story))
42
43 def test_command_with_double_quoted_param(self):
44 story = """bzr commit -m "two words" """
45 self.assertEquals([(['bzr', 'commit', '-m', 'two words'],
46 None, None, None)],
47 script._script_to_commands(story))
48
49 def test_command_with_input(self):
50 self.assertEquals([(['cat', '>file'], 'content\n', None, None)],
51 script._script_to_commands('cat >file\n<content\n'))
52
53 def test_command_with_output(self):
54 story = """
55bzr add
56>adding file
57>adding file2
58"""
59 self.assertEquals([(['bzr', 'add'], None,
60 'adding file\nadding file2\n', None)],
61 script._script_to_commands(story))
62
63 def test_command_with_error(self):
64 story = """
65bzr branch foo
662>bzr: ERROR: Not a branch: "foo"
67"""
68 self.assertEquals([(['bzr', 'branch', 'foo'],
69 None, None, 'bzr: ERROR: Not a branch: "foo"\n')],
70 script._script_to_commands(story))
71 def test_input_without_command(self):
72 self.assertRaises(SyntaxError, script._script_to_commands, '<input')
73
74 def test_output_without_command(self):
75 self.assertRaises(SyntaxError, script._script_to_commands, '>input')
76
77 def test_command_with_backquotes(self):
78 story = """
79foo = `bzr file-id toto`
80"""
81 self.assertEquals([(['foo', '=', '`bzr file-id toto`'],
82 None, None, None)],
83 script._script_to_commands(story))
84
85
86class TestScriptExecution(script.TestCaseWithTransportAndScript):
87
88 def test_unknown_command(self):
89 self.assertRaises(SyntaxError, self.run_script, 'foo')
90
91 def test_stops_on_unexpected_output(self):
92 story = """
93mkdir dir
94cd dir
95>Hello, I have just cd into dir !
96"""
97 self.assertRaises(AssertionError, self.run_script, story)
98
99
100 def test_stops_on_unexpected_error(self):
101 story = """
102cat
103<Hello
104bzr not-a-command
105"""
106 self.assertRaises(AssertionError, self.run_script, story)
107
108 def test_continue_on_expected_error(self):
109 story = """
110bzr not-a-command
1112>..."not-a-command"
112"""
113 self.run_script(story)
114
115 def test_ellipsis_output(self):
116 story = """
117cat
118<first line
119<second line
120<last line
121>first line
122>...
123>last line
124"""
125 self.run_script(story)
126 story = """
127bzr not-a-command
1282>..."not-a-command"
129"""
130 self.run_script(story)
131
132 story = """
133bzr branch not-a-branch
1342>bzr: ERROR: Not a branch...not-a-branch/".
135"""
136 self.run_script(story)
137
138
139class TestCat(script.TestCaseWithTransportAndScript):
140
141 def test_cat_usage(self):
142 self.assertRaises(SyntaxError, self.run_script, 'cat foo bar baz')
143 self.assertRaises(SyntaxError, self.run_script, 'cat foo <bar')
144
145 def test_cat_input_to_output(self):
146 out, err = self.run_command(['cat'], 'content\n', 'content\n', None)
147 self.assertEquals('content\n', out)
148 self.assertEquals(None, err)
149
150 def test_cat_file_to_output(self):
151 self.build_tree_contents([('file', 'content\n')])
152 out, err = self.run_command(['cat', 'file'], None, 'content\n', None)
153 self.assertEquals('content\n', out)
154 self.assertEquals(None, err)
155
156 def test_cat_input_to_file(self):
157 out, err = self.run_command(['cat', '>file'], 'content\n', None, None)
158 self.assertFileEqual('content\n', 'file')
159 self.assertEquals(None, out)
160 self.assertEquals(None, err)
161 out, err = self.run_command(['cat', '>>file'], 'more\n', None, None)
162 self.assertFileEqual('content\nmore\n', 'file')
163 self.assertEquals(None, out)
164 self.assertEquals(None, err)
165
166 def test_cat_file_to_file(self):
167 self.build_tree_contents([('file', 'content\n')])
168 out, err = self.run_command(['cat', 'file', '>file2'], None, None, None)
169 self.assertFileEqual('content\n', 'file2')
170
171
172class TestMkdir(script.TestCaseWithTransportAndScript):
173
174 def test_mkdir_usage(self):
175 self.assertRaises(SyntaxError, self.run_script, 'mkdir')
176 self.assertRaises(SyntaxError, self.run_script, 'mkdir foo bar')
177
178 def test_mkdir_jailed(self):
179 self.assertRaises(ValueError, self.run_script, 'mkdir /out-of-jail')
180 self.assertRaises(ValueError, self.run_script, 'mkdir ../out-of-jail')
181
182 def test_mkdir_in_jail(self):
183 self.run_script("""
184mkdir dir
185cd dir
186mkdir ../dir2
187cd ..
188""")
189 self.failUnlessExists('dir')
190 self.failUnlessExists('dir2')
191
192
193class TestCd(script.TestCaseWithTransportAndScript):
194
195 def test_cd_usage(self):
196 self.assertRaises(SyntaxError, self.run_script, 'cd foo bar')
197
198 def test_cd_out_of_jail(self):
199 self.assertRaises(ValueError, self.run_script, 'cd /out-of-jail')
200 self.assertRaises(ValueError, self.run_script, 'cd ..')
201
202 def test_cd_dir_and_back_home(self):
203 self.assertEquals(self.test_dir, osutils.getcwd())
204 self.run_script("""
205mkdir dir
206cd dir
207""")
208 self.assertEquals(osutils.pathjoin(self.test_dir, 'dir'),
209 osutils.getcwd())
210
211 self.run_script('cd')
212 self.assertEquals(self.test_dir, osutils.getcwd())
213
214
215class TestBzr(script.TestCaseWithTransportAndScript):
216
217 def test_bzr_smoke(self):
218 self.run_script('bzr init branch')
219 self.failUnlessExists('branch')
220
221
222class TestEcho(script.TestCaseWithMemoryTransportAndScript):
223
224 def test_echo_usage(self):
225 story = """
226echo foo
227<bar
228"""
229 self.assertRaises(SyntaxError, self.run_script, story)
230
231 def test_echo_to_output(self):
232 out, err = self.run_command(['echo'], None, '\n', None)
233 self.assertEquals('\n', out)
234 self.assertEquals(None, err)
235
236 def test_echo_some_to_output(self):
237 out, err = self.run_command(['echo', 'hello'], None, 'hello\n', None)
238 self.assertEquals('hello\n', out)
239 self.assertEquals(None, err)
240
241 def test_echo_more_output(self):
242 out, err = self.run_command(['echo', 'hello', 'happy', 'world'],
243 None, 'hellohappyworld\n', None)
244 self.assertEquals('hellohappyworld\n', out)
245 self.assertEquals(None, err)
246
247 def test_echo_appended(self):
248 out, err = self.run_command(['echo', 'hello', '>file'],
249 None, None, None)
250 self.assertEquals(None, out)
251 self.assertEquals(None, err)
252 self.assertFileEqual('hello\n', 'file')
253 out, err = self.run_command(['echo', 'happy', '>>file'],
254 None, None, None)
255 self.assertEquals(None, out)
256 self.assertEquals(None, err)
257 self.assertFileEqual('hello\nhappy\n', 'file')