Merge lp:~vila/bzr/shell-like-tests into lp:bzr
- shell-like-tests
- Merge into bzr.dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pool | Needs Fixing | ||
Review via email: mp+11204@code.launchpad.net |
Commit message
Description of the change
Vincent Ladeuil (vila) wrote : | # |
Martin Pool (mbp) wrote : | # |
This is looking pretty cool.
=== modified file 'bzrlib/
--- bzrlib/
+++ bzrlib/
@@ -3618,6 +3618,7 @@
+ 'bzrlib.
=== added file 'bzrlib/
--- bzrlib/
+++ bzrlib/
@@ -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 ...
Vincent Ladeuil (vila) wrote : | # |
>>>>> "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...
Martin Pool (mbp) wrote : | # |
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 TestCaseWithMem
>
> Â Â 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_
...
""")
> Â Â martin> + Â Â def test_echo_
> Â Â martin> + Â Â Â Â out, err = self.run_
>...
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
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') |
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.