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
1=== modified file 'bzrlib/tests/__init__.py'
2--- bzrlib/tests/__init__.py 2009-08-28 21:05:31 +0000
3+++ bzrlib/tests/__init__.py 2009-09-01 08:24:44 +0000
4@@ -3618,6 +3618,7 @@
5 'bzrlib.tests.test_rio',
6 'bzrlib.tests.test_rules',
7 'bzrlib.tests.test_sampler',
8+ 'bzrlib.tests.test_script',
9 'bzrlib.tests.test_selftest',
10 'bzrlib.tests.test_serializer',
11 'bzrlib.tests.test_setup',
12
13=== added file 'bzrlib/tests/script.py'
14--- bzrlib/tests/script.py 1970-01-01 00:00:00 +0000
15+++ bzrlib/tests/script.py 2009-09-04 14:55:59 +0000
16@@ -0,0 +1,389 @@
17+# Copyright (C) 2009 Canonical Ltd
18+#
19+# This program is free software; you can redistribute it and/or modify
20+# it under the terms of the GNU General Public License as published by
21+# the Free Software Foundation; either version 2 of the License, or
22+# (at your option) any later version.
23+#
24+# This program is distributed in the hope that it will be useful,
25+# but WITHOUT ANY WARRANTY; without even the implied warranty of
26+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27+# GNU General Public License for more details.
28+#
29+# You should have received a copy of the GNU General Public License
30+# along with this program; if not, write to the Free Software
31+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
32+"""Shell-like test scripts.
33+
34+This allows users to write tests in a syntax very close to a shell session,
35+using a restricted and limited set of commands that should be enough to mimic
36+most of the behaviours.
37+
38+A script is a set of commands, each command is composed of:
39+- one mandatory command line,
40+- one optional set of input lines to feed the command,
41+- one optional set of output expected lines,
42+- one optional set of error expected lines.
43+
44+The optional lines starts with a special string (mnemonic: shell redirection):
45+- '<' for input,
46+- '>' for output,
47+- '2>' for errors,
48+
49+The execution stops as soon as an expected output or an expected error is not
50+matched.
51+
52+When no output is specified, any ouput from the command is accepted
53+and let the execution continue.
54+
55+If an error occurs and no expected error is specified, the execution stops.
56+
57+The matching is done on a full string comparison basis unless '...' is used, in
58+which case expected output/errors can be lees precise.
59+
60+Examples:
61+
62+The following will succeeds only if 'bzr add' outputs 'adding file'.
63+
64+ bzr add file
65+ >adding file
66+
67+If you want the command to succeed for any output, just use:
68+
69+ bzr add file
70+
71+The following will stop with an error:
72+
73+ bzr not-a-command
74+
75+If you want it to succeed, use:
76+
77+ bzr not-a-command
78+ 2> bzr: ERROR: unknown command "not-a-command"
79+
80+You can use ellipsis (...) to replace any piece of text you don't want to be
81+matched exactly:
82+
83+ bzr branch not-a-branch
84+ 2>bzr: ERROR: Not a branch...not-a-branch/".
85+
86+
87+This can be used to ignore entire lines too:
88+
89+cat
90+<first line
91+<second line
92+<third line
93+<fourth line
94+<last line
95+>first line
96+>...
97+>last line
98+
99+You can check the content of a file with cat:
100+
101+ cat <file
102+ >expected content
103+
104+You can also check the existence of a file with cat, the following will fail if
105+the file doesn't exist:
106+
107+ cat file
108+
109+"""
110+
111+import doctest
112+import os
113+import shlex
114+from cStringIO import StringIO
115+
116+from bzrlib import (
117+ osutils,
118+ tests,
119+ )
120+
121+
122+def split(s):
123+ """Split a command line respecting quotes."""
124+ scanner = shlex.shlex(s)
125+ scanner.quotes = '\'"`'
126+ scanner.whitespace_split = True
127+ for t in list(scanner):
128+ # Strip the simple and double quotes since we don't care about them.
129+ # We leave the backquotes in place though since they have a different
130+ # semantic.
131+ if t[0] in ('"', "'") and t[0] == t[-1]:
132+ yield t[1:-1]
133+ else:
134+ yield t
135+
136+
137+def _script_to_commands(text, file_name=None):
138+ """Turn a script into a list of commands with their associated IOs.
139+
140+ Each command appears on a line by itself. It can be associated with an
141+ input that will feed it and an expected output.
142+ Comments starts with '#' until the end of line.
143+ Empty lines are ignored.
144+ Input and output are full lines terminated by a '\n'.
145+ Input lines start with '<'.
146+ Output lines start with '>'.
147+ Error lines start with '2>'.
148+ """
149+
150+ commands = []
151+
152+ def add_command(cmd, input, output, error):
153+ if cmd is not None:
154+ if input is not None:
155+ input = ''.join(input)
156+ if output is not None:
157+ output = ''.join(output)
158+ if error is not None:
159+ error = ''.join(error)
160+ commands.append((cmd, input, output, error))
161+
162+ cmd_cur = None
163+ cmd_line = 1
164+ lineno = 0
165+ input, output, error = None, None, None
166+ for line in text.split('\n'):
167+ lineno += 1
168+ # Keep a copy for error reporting
169+ orig = line
170+ comment = line.find('#')
171+ if comment >= 0:
172+ # Delete comments
173+ line = line[0:comment]
174+ line = line.rstrip()
175+ if line == '':
176+ # Ignore empty lines
177+ continue
178+ if line.startswith('<'):
179+ if input is None:
180+ if cmd_cur is None:
181+ raise SyntaxError('No command for that input',
182+ (file_name, lineno, 1, orig))
183+ input = []
184+ input.append(line[1:] + '\n')
185+ continue
186+ elif line.startswith('>'):
187+ if output is None:
188+ if cmd_cur is None:
189+ raise SyntaxError('No command for that output',
190+ (file_name, lineno, 1, orig))
191+ output = []
192+ output.append(line[1:] + '\n')
193+ continue
194+ elif line.startswith('2>'):
195+ if error is None:
196+ if cmd_cur is None:
197+ raise SyntaxError('No command for that error',
198+ (file_name, lineno, 1, orig))
199+ error = []
200+ error.append(line[2:] + '\n')
201+ continue
202+ else:
203+ # Time to output the current command
204+ add_command(cmd_cur, input, output, error)
205+ # And start a new one
206+ cmd_cur = list(split(line))
207+ cmd_line = lineno
208+ input, output, error = None, None, None
209+ # Add the last seen command
210+ add_command(cmd_cur, input, output, error)
211+ return commands
212+
213+
214+def _scan_redirection_options(args):
215+ """Recognize and process input and output redirections.
216+
217+ :param args: The command line arguments
218+
219+ :return: A tuple containing:
220+ - The file name redirected from or None
221+ - The file name redirected to or None
222+ - The mode to open the output file or None
223+ - The reamining arguments
224+ """
225+ remaining = []
226+ in_name = None
227+ out_name, out_mode = None, None
228+ for arg in args:
229+ if arg.startswith('<'):
230+ in_name = arg[1:]
231+ elif arg.startswith('>>'):
232+ out_name = arg[2:]
233+ out_mode = 'ab+'
234+ elif arg.startswith('>'):
235+ out_name = arg[1:]
236+ out_mode = 'wb+'
237+ else:
238+ remaining.append(arg)
239+ return in_name, out_name, out_mode, remaining
240+
241+
242+class ScriptRunner(object):
243+
244+ def __init__(self, test_case):
245+ self.test_case = test_case
246+ self.output_checker = doctest.OutputChecker()
247+ self.check_options = doctest.ELLIPSIS
248+
249+ def run_script(self, text):
250+ for cmd, input, output, error in _script_to_commands(text):
251+ out, err = self.run_command(cmd, input, output, error)
252+
253+ def _check_output(self, expected, actual):
254+ if expected is None:
255+ # Specifying None means: any output is accepted
256+ return
257+ if actual is None:
258+ self.test_case.fail('Unexpected: %s' % actual)
259+ matching = self.output_checker.check_output(
260+ expected, actual, self.check_options)
261+ if not matching:
262+ # Note that we can't use output_checker.output_difference() here
263+ # because... the API is boken (expected must be a doctest specific
264+ # object of whicha 'want' attribute will be our 'expected'
265+ # parameter. So we just fallbacl to our good old assertEqualDiff
266+ # since we know there are differences and the output should be
267+ # decently readable.
268+ self.test_case.assertEqualDiff(expected, actual)
269+
270+ def run_command(self, cmd, input, output, error):
271+ mname = 'do_' + cmd[0]
272+ method = getattr(self, mname, None)
273+ if method is None:
274+ raise SyntaxError('Command not found "%s"' % (cmd[0],),
275+ None, 1, ' '.join(cmd))
276+ if input is None:
277+ str_input = ''
278+ else:
279+ str_input = ''.join(input)
280+ actual_output, actual_error = method(str_input, cmd[1:])
281+
282+ self._check_output(output, actual_output)
283+ self._check_output(error, actual_error)
284+ if not error and actual_error:
285+ self.test_case.fail('Unexpected error: %s' % actual_error)
286+ return actual_output, actual_error
287+
288+ def _read_input(self, input, in_name):
289+ if in_name is not None:
290+ infile = open(in_name, 'rb')
291+ try:
292+ # Command redirection takes precedence over provided input
293+ input = infile.read()
294+ finally:
295+ infile.close()
296+ return input
297+
298+ def _write_output(self, output, out_name, out_mode):
299+ if out_name is not None:
300+ outfile = open(out_name, out_mode)
301+ try:
302+ outfile.write(output)
303+ finally:
304+ outfile.close()
305+ output = None
306+ return output
307+
308+ def do_bzr(self, input, args):
309+ out, err = self.test_case._run_bzr_core(
310+ args, retcode=None, encoding=None, stdin=input, working_dir=None)
311+ return out, err
312+
313+ def do_cat(self, input, args):
314+ (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
315+ if len(args) > 1:
316+ raise SyntaxError('Usage: cat [file1]')
317+ if args:
318+ if in_name is not None:
319+ raise SyntaxError('Specify a file OR use redirection')
320+ in_name = args[0]
321+ input = self._read_input(input, in_name)
322+ # Basically cat copy input to output
323+ output = input
324+ # Handle output redirections
325+ output = self._write_output(output, out_name, out_mode)
326+ return output, None
327+
328+ def do_echo(self, input, args):
329+ (in_name, out_name, out_mode, args) = _scan_redirection_options(args)
330+ if input and args:
331+ raise SyntaxError('Specify parameters OR use redirection')
332+ if args:
333+ input = ''.join(args)
334+ input = self._read_input(input, in_name)
335+ # Always append a \n'
336+ input += '\n'
337+ # Process output
338+ output = input
339+ # Handle output redirections
340+ output = self._write_output(output, out_name, out_mode)
341+ return output, None
342+
343+ def _ensure_in_jail(self, path):
344+ jail_root = self.test_case.get_jail_root()
345+ if not osutils.is_inside(jail_root, osutils.normalizepath(path)):
346+ raise ValueError('%s is not inside %s' % (path, jail_root))
347+
348+ def do_cd(self, input, args):
349+ if len(args) > 1:
350+ raise SyntaxError('Usage: cd [dir]')
351+ if len(args) == 1:
352+ d = args[0]
353+ self._ensure_in_jail(d)
354+ else:
355+ d = self.test_case.get_jail_root()
356+ os.chdir(d)
357+ return None, None
358+
359+ def do_mkdir(self, input, args):
360+ if not args or len(args) != 1:
361+ raise SyntaxError('Usage: mkdir dir')
362+ d = args[0]
363+ self._ensure_in_jail(d)
364+ os.mkdir(d)
365+ return None, None
366+
367+
368+class TestCaseWithMemoryTransportAndScript(tests.TestCaseWithMemoryTransport):
369+
370+ def setUp(self):
371+ super(TestCaseWithMemoryTransportAndScript, self).setUp()
372+ self.script_runner = ScriptRunner(self)
373+ # Break the circular dependency
374+ def break_dependency():
375+ self.script_runner = None
376+ self.addCleanup(break_dependency)
377+
378+ def get_jail_root(self):
379+ raise NotImplementedError(self.get_jail_root)
380+
381+ def run_script(self, script):
382+ return self.script_runner.run_script(script)
383+
384+ def run_command(self, cmd, input, output, error):
385+ return self.script_runner.run_command(cmd, input, output, error)
386+
387+
388+class TestCaseWithTransportAndScript(tests.TestCaseWithTransport):
389+
390+ def setUp(self):
391+ super(TestCaseWithTransportAndScript, self).setUp()
392+ self.script_runner = ScriptRunner(self)
393+ # Break the circular dependency
394+ def break_dependency():
395+ self.script_runner = None
396+ self.addCleanup(break_dependency)
397+
398+ def get_jail_root(self):
399+ return self.test_dir
400+
401+ def run_script(self, script):
402+ return self.script_runner.run_script(script)
403+
404+ def run_command(self, cmd, input, output, error):
405+ return self.script_runner.run_command(cmd, input, output, error)
406
407=== added file 'bzrlib/tests/test_script.py'
408--- bzrlib/tests/test_script.py 1970-01-01 00:00:00 +0000
409+++ bzrlib/tests/test_script.py 2009-09-04 14:55:59 +0000
410@@ -0,0 +1,257 @@
411+# Copyright (C) 2009 Canonical Ltd
412+#
413+# This program is free software; you can redistribute it and/or modify
414+# it under the terms of the GNU General Public License as published by
415+# the Free Software Foundation; either version 2 of the License, or
416+# (at your option) any later version.
417+#
418+# This program is distributed in the hope that it will be useful,
419+# but WITHOUT ANY WARRANTY; without even the implied warranty of
420+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
421+# GNU General Public License for more details.
422+#
423+# You should have received a copy of the GNU General Public License
424+# along with this program; if not, write to the Free Software
425+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
426+
427+
428+from bzrlib import (
429+ osutils,
430+ tests,
431+ )
432+from bzrlib.tests import script
433+
434+
435+class TestScriptSyntax(tests.TestCase):
436+
437+ def test_comment_is_ignored(self):
438+ self.assertEquals([], script._script_to_commands('#comment\n'))
439+
440+ def test_empty_line_is_ignored(self):
441+ self.assertEquals([], script._script_to_commands('\n'))
442+
443+ def test_simple_command(self):
444+ self.assertEquals([(['cd', 'trunk'], None, None, None)],
445+ script._script_to_commands('cd trunk'))
446+
447+ def test_command_with_single_quoted_param(self):
448+ story = """bzr commit -m 'two words'"""
449+ self.assertEquals([(['bzr', 'commit', '-m', 'two words'],
450+ None, None, None)],
451+ script._script_to_commands(story))
452+
453+ def test_command_with_double_quoted_param(self):
454+ story = """bzr commit -m "two words" """
455+ self.assertEquals([(['bzr', 'commit', '-m', 'two words'],
456+ None, None, None)],
457+ script._script_to_commands(story))
458+
459+ def test_command_with_input(self):
460+ self.assertEquals([(['cat', '>file'], 'content\n', None, None)],
461+ script._script_to_commands('cat >file\n<content\n'))
462+
463+ def test_command_with_output(self):
464+ story = """
465+bzr add
466+>adding file
467+>adding file2
468+"""
469+ self.assertEquals([(['bzr', 'add'], None,
470+ 'adding file\nadding file2\n', None)],
471+ script._script_to_commands(story))
472+
473+ def test_command_with_error(self):
474+ story = """
475+bzr branch foo
476+2>bzr: ERROR: Not a branch: "foo"
477+"""
478+ self.assertEquals([(['bzr', 'branch', 'foo'],
479+ None, None, 'bzr: ERROR: Not a branch: "foo"\n')],
480+ script._script_to_commands(story))
481+ def test_input_without_command(self):
482+ self.assertRaises(SyntaxError, script._script_to_commands, '<input')
483+
484+ def test_output_without_command(self):
485+ self.assertRaises(SyntaxError, script._script_to_commands, '>input')
486+
487+ def test_command_with_backquotes(self):
488+ story = """
489+foo = `bzr file-id toto`
490+"""
491+ self.assertEquals([(['foo', '=', '`bzr file-id toto`'],
492+ None, None, None)],
493+ script._script_to_commands(story))
494+
495+
496+class TestScriptExecution(script.TestCaseWithTransportAndScript):
497+
498+ def test_unknown_command(self):
499+ self.assertRaises(SyntaxError, self.run_script, 'foo')
500+
501+ def test_stops_on_unexpected_output(self):
502+ story = """
503+mkdir dir
504+cd dir
505+>Hello, I have just cd into dir !
506+"""
507+ self.assertRaises(AssertionError, self.run_script, story)
508+
509+
510+ def test_stops_on_unexpected_error(self):
511+ story = """
512+cat
513+<Hello
514+bzr not-a-command
515+"""
516+ self.assertRaises(AssertionError, self.run_script, story)
517+
518+ def test_continue_on_expected_error(self):
519+ story = """
520+bzr not-a-command
521+2>..."not-a-command"
522+"""
523+ self.run_script(story)
524+
525+ def test_ellipsis_output(self):
526+ story = """
527+cat
528+<first line
529+<second line
530+<last line
531+>first line
532+>...
533+>last line
534+"""
535+ self.run_script(story)
536+ story = """
537+bzr not-a-command
538+2>..."not-a-command"
539+"""
540+ self.run_script(story)
541+
542+ story = """
543+bzr branch not-a-branch
544+2>bzr: ERROR: Not a branch...not-a-branch/".
545+"""
546+ self.run_script(story)
547+
548+
549+class TestCat(script.TestCaseWithTransportAndScript):
550+
551+ def test_cat_usage(self):
552+ self.assertRaises(SyntaxError, self.run_script, 'cat foo bar baz')
553+ self.assertRaises(SyntaxError, self.run_script, 'cat foo <bar')
554+
555+ def test_cat_input_to_output(self):
556+ out, err = self.run_command(['cat'], 'content\n', 'content\n', None)
557+ self.assertEquals('content\n', out)
558+ self.assertEquals(None, err)
559+
560+ def test_cat_file_to_output(self):
561+ self.build_tree_contents([('file', 'content\n')])
562+ out, err = self.run_command(['cat', 'file'], None, 'content\n', None)
563+ self.assertEquals('content\n', out)
564+ self.assertEquals(None, err)
565+
566+ def test_cat_input_to_file(self):
567+ out, err = self.run_command(['cat', '>file'], 'content\n', None, None)
568+ self.assertFileEqual('content\n', 'file')
569+ self.assertEquals(None, out)
570+ self.assertEquals(None, err)
571+ out, err = self.run_command(['cat', '>>file'], 'more\n', None, None)
572+ self.assertFileEqual('content\nmore\n', 'file')
573+ self.assertEquals(None, out)
574+ self.assertEquals(None, err)
575+
576+ def test_cat_file_to_file(self):
577+ self.build_tree_contents([('file', 'content\n')])
578+ out, err = self.run_command(['cat', 'file', '>file2'], None, None, None)
579+ self.assertFileEqual('content\n', 'file2')
580+
581+
582+class TestMkdir(script.TestCaseWithTransportAndScript):
583+
584+ def test_mkdir_usage(self):
585+ self.assertRaises(SyntaxError, self.run_script, 'mkdir')
586+ self.assertRaises(SyntaxError, self.run_script, 'mkdir foo bar')
587+
588+ def test_mkdir_jailed(self):
589+ self.assertRaises(ValueError, self.run_script, 'mkdir /out-of-jail')
590+ self.assertRaises(ValueError, self.run_script, 'mkdir ../out-of-jail')
591+
592+ def test_mkdir_in_jail(self):
593+ self.run_script("""
594+mkdir dir
595+cd dir
596+mkdir ../dir2
597+cd ..
598+""")
599+ self.failUnlessExists('dir')
600+ self.failUnlessExists('dir2')
601+
602+
603+class TestCd(script.TestCaseWithTransportAndScript):
604+
605+ def test_cd_usage(self):
606+ self.assertRaises(SyntaxError, self.run_script, 'cd foo bar')
607+
608+ def test_cd_out_of_jail(self):
609+ self.assertRaises(ValueError, self.run_script, 'cd /out-of-jail')
610+ self.assertRaises(ValueError, self.run_script, 'cd ..')
611+
612+ def test_cd_dir_and_back_home(self):
613+ self.assertEquals(self.test_dir, osutils.getcwd())
614+ self.run_script("""
615+mkdir dir
616+cd dir
617+""")
618+ self.assertEquals(osutils.pathjoin(self.test_dir, 'dir'),
619+ osutils.getcwd())
620+
621+ self.run_script('cd')
622+ self.assertEquals(self.test_dir, osutils.getcwd())
623+
624+
625+class TestBzr(script.TestCaseWithTransportAndScript):
626+
627+ def test_bzr_smoke(self):
628+ self.run_script('bzr init branch')
629+ self.failUnlessExists('branch')
630+
631+
632+class TestEcho(script.TestCaseWithMemoryTransportAndScript):
633+
634+ def test_echo_usage(self):
635+ story = """
636+echo foo
637+<bar
638+"""
639+ self.assertRaises(SyntaxError, self.run_script, story)
640+
641+ def test_echo_to_output(self):
642+ out, err = self.run_command(['echo'], None, '\n', None)
643+ self.assertEquals('\n', out)
644+ self.assertEquals(None, err)
645+
646+ def test_echo_some_to_output(self):
647+ out, err = self.run_command(['echo', 'hello'], None, 'hello\n', None)
648+ self.assertEquals('hello\n', out)
649+ self.assertEquals(None, err)
650+
651+ def test_echo_more_output(self):
652+ out, err = self.run_command(['echo', 'hello', 'happy', 'world'],
653+ None, 'hellohappyworld\n', None)
654+ self.assertEquals('hellohappyworld\n', out)
655+ self.assertEquals(None, err)
656+
657+ def test_echo_appended(self):
658+ out, err = self.run_command(['echo', 'hello', '>file'],
659+ None, None, None)
660+ self.assertEquals(None, out)
661+ self.assertEquals(None, err)
662+ self.assertFileEqual('hello\n', 'file')
663+ out, err = self.run_command(['echo', 'happy', '>>file'],
664+ None, None, None)
665+ self.assertEquals(None, out)
666+ self.assertEquals(None, err)
667+ self.assertFileEqual('hello\nhappy\n', 'file')