Merge lp:~abentley/bzr-builder/launchpad into lp:~launchpad-pqm/bzr-builder/trunk
- launchpad
- Merge into trunk
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Approved by: | Aaron Bentley |
Approved revision: | 68 |
Merged at revision: | 68 |
Proposed branch: | lp:~abentley/bzr-builder/launchpad |
Merge into: | lp:~launchpad-pqm/bzr-builder/trunk |
Diff against target: |
459 lines (+171/-66) 2 files modified
recipe.py (+70/-29) tests/test_recipe.py (+101/-37) |
To merge this branch: | bzr merge lp:~abentley/bzr-builder/launchpad |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Aaron Bentley (community) | Approve | ||
Review via email: mp+41635@code.launchpad.net |
This proposal supersedes a proposal from 2010-11-23.
Commit message
Description of the change
This updates to bzr-builder tip so that we can use its improved error messages.
To post a comment you must log in.
Revision history for this message
Māris Fogels (mars) wrote : Posted in a previous version of this proposal | # |
review:
Approve
Revision history for this message
Aaron Bentley (abentley) wrote : | # |
Well, in fact, I submitted against the wrong branch, and the diff didn't show the changes I described, so, there was room for review.
Revision history for this message
Aaron Bentley (abentley) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'recipe.py' |
2 | --- recipe.py 2010-09-29 22:35:56 +0000 |
3 | +++ recipe.py 2010-11-23 18:09:11 +0000 |
4 | @@ -55,6 +55,13 @@ |
5 | NEST_PART_INSTRUCTION = "nest-part" |
6 | NEST_INSTRUCTION = "nest" |
7 | RUN_INSTRUCTION = "run" |
8 | +USAGE = { |
9 | + MERGE_INSTRUCTION: 'merge NAME BRANCH [REVISION]', |
10 | + NEST_INSTRUCTION: 'nest NAME BRANCH TARGET-DIR [REVISION]', |
11 | + NEST_PART_INSTRUCTION: |
12 | + 'nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]', |
13 | + RUN_INSTRUCTION: 'run COMMAND', |
14 | + } |
15 | |
16 | SAFE_INSTRUCTIONS = [ |
17 | MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION] |
18 | @@ -520,11 +527,16 @@ |
19 | |
20 | def as_text(self): |
21 | revid_part = self._get_revid_part() |
22 | - if self.target_subdir is not None: |
23 | + if revid_part: |
24 | + target_subdir = self.target_subdir |
25 | + if target_subdir is None: |
26 | + target_subdir = self.subpath |
27 | target_revid_part = " %s%s" % ( |
28 | - self.target_subdir, revid_part) |
29 | + target_subdir, revid_part) |
30 | + elif self.target_subdir is not None: |
31 | + target_revid_part = " %s" % self.target_subdir |
32 | else: |
33 | - target_revid_part = revid_part |
34 | + target_revid_part = "" |
35 | return "%s %s %s %s%s" % ( |
36 | NEST_PART_INSTRUCTION, self.recipe_branch.name, |
37 | self.recipe_branch.url, self.subpath, target_revid_part) |
38 | @@ -762,6 +774,14 @@ |
39 | problem=problem) |
40 | |
41 | |
42 | +class InstructionParseError(RecipeParseError): |
43 | + _fmt = RecipeParseError._fmt + "\nUsage: %(usage)s" |
44 | + |
45 | + def __init__(self, filename, line, char, problem, instruction): |
46 | + RecipeParseError.__init__(self, filename, line, char, problem) |
47 | + self.usage = USAGE[instruction] |
48 | + |
49 | + |
50 | class ForbiddenInstructionError(RecipeParseError): |
51 | |
52 | def __init__(self, filename, line, char, problem, instruction_name=None): |
53 | @@ -848,17 +868,18 @@ |
54 | instruction = self.parse_instruction( |
55 | permitted_instructions=permitted_instructions) |
56 | if instruction == RUN_INSTRUCTION: |
57 | - self.parse_whitespace("the command") |
58 | + self.parse_whitespace("the command", |
59 | + instruction=instruction) |
60 | command = self.take_to_newline().strip() |
61 | self.new_line() |
62 | active_branches[-1].run_command(command) |
63 | else: |
64 | - branch_id = self.parse_branch_id() |
65 | - url = self.parse_branch_url() |
66 | + branch_id = self.parse_branch_id(instruction) |
67 | + url = self.parse_branch_url(instruction) |
68 | if instruction == NEST_INSTRUCTION: |
69 | - location = self.parse_branch_location() |
70 | + location = self.parse_branch_location(instruction) |
71 | if instruction == NEST_PART_INSTRUCTION: |
72 | - path = self.parse_subpath() |
73 | + path = self.parse_subpath(instruction) |
74 | target_subdir = self.parse_optional_path() |
75 | if target_subdir == '': |
76 | target_subdir = None |
77 | @@ -867,7 +888,7 @@ |
78 | revspec = self.parse_optional_revspec() |
79 | else: |
80 | revspec = self.parse_optional_revspec() |
81 | - self.new_line() |
82 | + self.new_line(instruction) |
83 | last_branch = RecipeBranch(branch_id, url, revspec=revspec) |
84 | if instruction == NEST_INSTRUCTION: |
85 | active_branches[-1].nest_branch(location, last_branch) |
86 | @@ -922,12 +943,13 @@ |
87 | self.throw_parse_error("Expecting %s, got '%s'" |
88 | % (options_str, instruction)) |
89 | |
90 | - def parse_branch_id(self): |
91 | - self.parse_whitespace("the branch id") |
92 | + def parse_branch_id(self, instruction): |
93 | + self.parse_whitespace("the branch id", instruction=instruction) |
94 | branch_id = self.peek_to_whitespace() |
95 | if branch_id is None: |
96 | self.throw_parse_error("End of line while looking for the " |
97 | - "branch id") |
98 | + "branch id", cls=InstructionParseError, |
99 | + instruction=instruction) |
100 | if branch_id in self.seen_nicks: |
101 | self.throw_parse_error("'%s' was already used to identify " |
102 | "a branch." % branch_id) |
103 | @@ -935,36 +957,39 @@ |
104 | self.seen_nicks.add(branch_id) |
105 | return branch_id |
106 | |
107 | - def parse_branch_url(self): |
108 | - self.parse_whitespace("the branch url") |
109 | - branch_url = self.take_to_whitespace("the branch url") |
110 | + def parse_branch_url(self, instruction): |
111 | + self.parse_whitespace("the branch url", instruction=instruction) |
112 | + branch_url = self.take_to_whitespace("the branch url", instruction) |
113 | return branch_url |
114 | |
115 | - def parse_branch_location(self): |
116 | + def parse_branch_location(self, instruction): |
117 | # FIXME: Needs a better term |
118 | self.parse_whitespace("the location to nest") |
119 | location = self.peek_to_whitespace() |
120 | if location is None: |
121 | self.throw_parse_error("End of line while looking for the " |
122 | - "location to nest") |
123 | + "location to nest", cls=InstructionParseError, |
124 | + instruction=instruction) |
125 | norm_location = os.path.normpath(location) |
126 | if norm_location in self.seen_paths: |
127 | self.throw_parse_error("The path '%s' is a duplicate of " |
128 | "the one used on line %d." % (location, |
129 | - self.seen_paths[norm_location])) |
130 | + self.seen_paths[norm_location]), |
131 | + InstructionParseError, instruction=instruction) |
132 | if os.path.isabs(norm_location): |
133 | self.throw_parse_error("Absolute paths are not allowed: %s" |
134 | - % location) |
135 | + % location, InstructionParseError, instruction=instruction) |
136 | if norm_location.startswith(".."): |
137 | self.throw_parse_error("Paths outside the current directory " |
138 | - "are not allowed: %s" % location) |
139 | + "are not allowed: %s" % location, |
140 | + cls=InstructionParseError, instruction=instruction) |
141 | self.take_chars(len(location)) |
142 | self.seen_paths[norm_location] = self.line_index + 1 |
143 | return location |
144 | |
145 | - def parse_subpath(self): |
146 | - self.parse_whitespace("the subpath to merge") |
147 | - location = self.take_to_whitespace("the subpath to merge") |
148 | + def parse_subpath(self, instruction): |
149 | + self.parse_whitespace("the subpath to merge", instruction=instruction) |
150 | + location = self.take_to_whitespace("the subpath to merge", instruction) |
151 | return location |
152 | |
153 | def parse_revspec(self): |
154 | @@ -999,13 +1024,18 @@ |
155 | def throw_eol(self, expected): |
156 | self.throw_parse_error("End of line while looking for '%s'" % expected) |
157 | |
158 | - def new_line(self): |
159 | + def new_line(self, instruction=None): |
160 | # Jump over any whitespace |
161 | self.parse_whitespace(None, require=False) |
162 | remaining = self.peek_to_whitespace() |
163 | if remaining != None: |
164 | + kwargs = {} |
165 | + if instruction is not None: |
166 | + kwargs = { |
167 | + 'cls': InstructionParseError, |
168 | + 'instruction': instruction} |
169 | self.throw_parse_error("Expecting the end of the line, got '%s'" |
170 | - % remaining) |
171 | + % remaining, **kwargs) |
172 | self.index = 0 |
173 | self.line_index += 1 |
174 | if self.line_index >= len(self.lines): |
175 | @@ -1071,12 +1101,18 @@ |
176 | return old_indent_level |
177 | return None |
178 | |
179 | - def parse_whitespace(self, looking_for, require=True): |
180 | + def parse_whitespace(self, looking_for, require=True, instruction=None): |
181 | if require: |
182 | actual = self.peek_char() |
183 | if actual is None: |
184 | + kwargs = {} |
185 | + if instruction is not None: |
186 | + kwargs = { |
187 | + 'cls': InstructionParseError, |
188 | + 'instruction': instruction, |
189 | + } |
190 | self.throw_parse_error("End of line while looking for " |
191 | - "%s" % looking_for) |
192 | + "%s" % looking_for, **kwargs) |
193 | if actual not in self.whitespace_chars: |
194 | self.throw_parse_error("Expecting whitespace before %s, " |
195 | "got '%s'." % (looking_for, actual)) |
196 | @@ -1123,11 +1159,16 @@ |
197 | char = self.peek_char(skip=count) |
198 | return ret |
199 | |
200 | - def take_to_whitespace(self, looking_for): |
201 | + def take_to_whitespace(self, looking_for, instruction=None): |
202 | text = self.peek_to_whitespace() |
203 | if text is None: |
204 | + kwargs = {} |
205 | + if instruction is not None: |
206 | + kwargs = { |
207 | + 'cls': InstructionParseError, |
208 | + 'instruction': instruction} |
209 | self.throw_parse_error("End of line while looking for %s" |
210 | - % looking_for) |
211 | + % looking_for, **kwargs) |
212 | self.take_chars(len(text)) |
213 | return text |
214 | |
215 | |
216 | === modified file 'tests/test_recipe.py' |
217 | --- tests/test_recipe.py 2010-09-29 22:35:56 +0000 |
218 | +++ tests/test_recipe.py 2010-11-23 18:09:11 +0000 |
219 | @@ -29,13 +29,19 @@ |
220 | BaseRecipeBranch, |
221 | build_tree, |
222 | ensure_basedir, |
223 | + InstructionParseError, |
224 | ForbiddenInstructionError, |
225 | + MERGE_INSTRUCTION, |
226 | + NEST_INSTRUCTION, |
227 | + NEST_PART_INSTRUCTION, |
228 | pull_or_branch, |
229 | RecipeParser, |
230 | RecipeBranch, |
231 | RecipeParseError, |
232 | resolve_revisions, |
233 | + RUN_INSTRUCTION, |
234 | SAFE_INSTRUCTIONS, |
235 | + USAGE, |
236 | ) |
237 | |
238 | |
239 | @@ -59,6 +65,13 @@ |
240 | self.assertEqual("recipe", exc.filename) |
241 | return exc |
242 | |
243 | + def assertInstructionParseError(self, line, char, problem, instruction, |
244 | + callable, *args, **kwargs): |
245 | + exc = self.assertParseError(line, char, problem, callable, *args, |
246 | + **kwargs) |
247 | + self.assertIsInstance(exc, InstructionParseError), |
248 | + self.assertEqual(USAGE[instruction], exc.usage) |
249 | + |
250 | def check_recipe_branch(self, branch, name, url, revspec=None, |
251 | num_child_branches=0, revid=None): |
252 | self.assertEqual(name, branch.name) |
253 | @@ -138,53 +151,69 @@ |
254 | self.basic_header + "http://foo.org/\n" + "cat") |
255 | |
256 | def test_rejects_merge_no_name(self): |
257 | - self.assertParseError(3, 7, "End of line while looking for " |
258 | - "the branch id", self.get_recipe, |
259 | + self.assertInstructionParseError(3, 7, "End of line while looking for " |
260 | + "the branch id", MERGE_INSTRUCTION, self.get_recipe, |
261 | self.basic_header_and_branch + "merge ") |
262 | |
263 | + def test_rejects_merge_no_name_no_space(self): |
264 | + self.assertInstructionParseError(3, 6, "End of line while looking for " |
265 | + "the branch id", MERGE_INSTRUCTION, self.get_recipe, |
266 | + self.basic_header_and_branch + "merge") |
267 | + |
268 | def test_rejects_merge_no_url(self): |
269 | - self.assertParseError(3, 11, "End of line while looking for " |
270 | - "the branch url", self.get_recipe, |
271 | + self.assertInstructionParseError(3, 11, "End of line while looking for" |
272 | + " the branch url", MERGE_INSTRUCTION, self.get_recipe, |
273 | self.basic_header_and_branch + "merge foo ") |
274 | |
275 | def test_rejects_text_at_end_of_merge_line(self): |
276 | - self.assertParseError(3, 17, "Expecting the end of the line, " |
277 | - "got 'bar'", self.get_recipe, |
278 | + self.assertInstructionParseError(3, 17, |
279 | + "Expecting the end of the line, got 'bar'", |
280 | + MERGE_INSTRUCTION, self.get_recipe, |
281 | self.basic_header_and_branch + "merge foo url 2 bar") |
282 | |
283 | def test_rejects_nest_part_no_name(self): |
284 | - self.assertParseError(3, 11, "End of line while looking for " |
285 | - "the branch id", self.get_recipe, |
286 | + self.assertInstructionParseError(3, 11, "End of line while looking for" |
287 | + " the branch id", NEST_PART_INSTRUCTION, self.get_recipe, |
288 | self.basic_header_and_branch + "nest-part ") |
289 | |
290 | def test_rejects_nest_part_no_url(self): |
291 | - self.assertParseError(3, 15, "End of line while looking for " |
292 | - "the branch url", self.get_recipe, |
293 | + self.assertInstructionParseError(3, 15, "End of line while looking for" |
294 | + " the branch url", NEST_PART_INSTRUCTION, self.get_recipe, |
295 | self.basic_header_and_branch + "nest-part foo ") |
296 | |
297 | def test_rejects_nest_part_no_subpath(self): |
298 | - self.assertParseError(3, 22, "End of line while looking for " |
299 | - "the subpath to merge", self.get_recipe, |
300 | + self.assertInstructionParseError(3, 22, "End of line while looking for" |
301 | + " the subpath to merge", NEST_PART_INSTRUCTION, |
302 | + self.get_recipe, |
303 | self.basic_header_and_branch + "nest-part foo url:// ") |
304 | |
305 | + def test_rejects_nest_part_no_subpath_no_space(self): |
306 | + self.assertInstructionParseError(3, 21, "End of line while looking for" |
307 | + " the subpath to merge", NEST_PART_INSTRUCTION, |
308 | + self.get_recipe, |
309 | + self.basic_header_and_branch + "nest-part foo url://") |
310 | + |
311 | def test_rejects_nest_no_name(self): |
312 | - self.assertParseError(3, 6, "End of line while looking for " |
313 | - "the branch id", self.get_recipe, |
314 | + self.assertInstructionParseError(3, 6, "End of line while looking for" |
315 | + " the branch id", NEST_INSTRUCTION, self.get_recipe, |
316 | self.basic_header_and_branch + "nest ") |
317 | |
318 | def test_rejects_nest_no_url(self): |
319 | - self.assertParseError(3, 10, "End of line while looking for " |
320 | - "the branch url", self.get_recipe, |
321 | - self.basic_header_and_branch + "nest foo ") |
322 | + self.assertInstructionParseError(3, 10, |
323 | + "End of line while looking for the branch url", |
324 | + NEST_INSTRUCTION, self.get_recipe, |
325 | + self.basic_header_and_branch + "nest foo ") |
326 | |
327 | def test_rejects_nest_no_location(self): |
328 | - self.assertParseError(3, 14, "End of line while looking for " |
329 | - "the location to nest", self.get_recipe, |
330 | + self.assertInstructionParseError(3, 14, |
331 | + "End of line while looking for the location to nest", |
332 | + NEST_INSTRUCTION, self.get_recipe, |
333 | self.basic_header_and_branch + "nest foo url ") |
334 | |
335 | def test_rejects_text_at_end_of_nest_line(self): |
336 | - self.assertParseError(3, 20, "Expecting the end of the line, " |
337 | - "got 'baz'", self.get_recipe, |
338 | + self.assertInstructionParseError(3, 20, |
339 | + "Expecting the end of the line, got 'baz'", |
340 | + NEST_INSTRUCTION, self.get_recipe, |
341 | self.basic_header_and_branch + "nest foo url bar 2 baz") |
342 | |
343 | def test_rejects_indent_after_first_branch(self): |
344 | @@ -226,6 +255,12 @@ |
345 | self.basic_header_and_branch + "merge foo url\n" |
346 | + "merge foo other-url\n") |
347 | |
348 | + def test_rejects_run_with_no_instruction(self): |
349 | + self.assertInstructionParseError(3, 4, |
350 | + "End of line while looking for the command", |
351 | + RUN_INSTRUCTION, self.get_recipe, |
352 | + self.basic_header_and_branch + "run\n") |
353 | + |
354 | def test_builds_simplest_recipe(self): |
355 | base_branch = self.get_recipe(self.basic_header_and_branch) |
356 | self.check_base_recipe_branch(base_branch, "http://foo.org/") |
357 | @@ -443,34 +478,40 @@ |
358 | self.assertEqual("run", exc.instruction_name) |
359 | |
360 | def test_error_on_duplicate_path(self): |
361 | - exc = self.assertParseError(3, 15, "The path '.' is a duplicate " |
362 | - "of the one used on line 1.", self.get_recipe, |
363 | + exc = self.assertInstructionParseError(3, 15, |
364 | + "The path '.' is a duplicate of the one used on line 1.", |
365 | + NEST_INSTRUCTION, self.get_recipe, |
366 | self.basic_header_and_branch + "nest nest url .\n") |
367 | |
368 | def test_error_on_duplicate_path_with_another_nest(self): |
369 | - exc = self.assertParseError(4, 16, "The path 'foo' is a duplicate " |
370 | - "of the one used on line 3.", self.get_recipe, |
371 | + exc = self.assertInstructionParseError(4, 16, |
372 | + "The path 'foo' is a duplicate of the one used on line 3.", |
373 | + NEST_INSTRUCTION, self.get_recipe, |
374 | self.basic_header_and_branch + "nest nest url foo\n" |
375 | + "nest nest2 url foo\n") |
376 | |
377 | def test_duplicate_path_check_uses_normpath(self): |
378 | - exc = self.assertParseError(3, 15, "The path 'foo/..' is a duplicate " |
379 | - "of the one used on line 1.", self.get_recipe, |
380 | + exc = self.assertInstructionParseError(3, 15, |
381 | + "The path 'foo/..' is a duplicate of the one used on line 1.", |
382 | + NEST_INSTRUCTION, self.get_recipe, |
383 | self.basic_header_and_branch + "nest nest url foo/..\n") |
384 | |
385 | def test_error_absolute_path(self): |
386 | - exc = self.assertParseError(3, 15, "Absolute paths are not allowed: " |
387 | - "/etc/passwd", self.get_recipe, self.basic_header_and_branch |
388 | - + "nest nest url /etc/passwd\n") |
389 | + exc = self.assertInstructionParseError(3, 15, |
390 | + "Absolute paths are not allowed: /etc/passwd", |
391 | + NEST_INSTRUCTION, self.get_recipe, |
392 | + self.basic_header_and_branch + "nest nest url /etc/passwd\n") |
393 | |
394 | def test_error_simple_parent_dir(self): |
395 | - exc = self.assertParseError(3, 15, "Paths outside the current " |
396 | - "directory are not allowed: ../foo", self.get_recipe, |
397 | + exc = self.assertInstructionParseError(3, 15, |
398 | + "Paths outside the current directory are not allowed: ../foo", |
399 | + NEST_INSTRUCTION, self.get_recipe, |
400 | self.basic_header_and_branch + "nest nest url ../foo\n") |
401 | |
402 | def test_error_complex_parent_dir(self): |
403 | - exc = self.assertParseError(3, 15, "Paths outside the current " |
404 | - "directory are not allowed: ./foo/../..", self.get_recipe, |
405 | + exc = self.assertInstructionParseError(3, 15, |
406 | + "Paths outside the current directory are not allowed:" |
407 | + " ./foo/../..", NEST_INSTRUCTION, self.get_recipe, |
408 | self.basic_header_and_branch + "nest nest url ./foo/../..\n") |
409 | |
410 | |
411 | @@ -1064,8 +1105,8 @@ |
412 | nested_branch1 = RecipeBranch("nested1", "nested1_url", |
413 | revspec="tag:foo") |
414 | base_branch.nest_branch(".", nested_branch1) |
415 | - self.assertRaises(RecipeParseError, base_branch.get_recipe_text, |
416 | - validate=True) |
417 | + self.assertRaises(InstructionParseError, |
418 | + base_branch.get_recipe_text, validate=True) |
419 | |
420 | def test_str_validates(self): |
421 | base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
422 | @@ -1073,7 +1114,7 @@ |
423 | nested_branch1 = RecipeBranch("nested1", "nested1_url", |
424 | revspec="tag:foo") |
425 | base_branch.nest_branch(".", nested_branch1) |
426 | - self.assertRaises(RecipeParseError, str, base_branch) |
427 | + self.assertRaises(InstructionParseError, str, base_branch) |
428 | |
429 | def test_with_nest_part(self): |
430 | base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
431 | @@ -1087,6 +1128,29 @@ |
432 | "nest-part nested1 nested1_url foo bar tag:foo\n", |
433 | manifest) |
434 | |
435 | + def test_with_nest_part_with_no_target_dir(self): |
436 | + base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
437 | + base_branch.revid = "base_revid" |
438 | + nested_branch1 = RecipeBranch("nested1", "nested1_url", |
439 | + revspec="tag:foo") |
440 | + base_branch.nest_part_branch(nested_branch1, "foo", None) |
441 | + manifest = base_branch.get_recipe_text() |
442 | + self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" |
443 | + "base_url revid:base_revid\n" |
444 | + "nest-part nested1 nested1_url foo foo tag:foo\n", |
445 | + manifest) |
446 | + |
447 | + def test_with_nest_part_with_no_target_dir_no_revspec(self): |
448 | + base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
449 | + base_branch.revid = "base_revid" |
450 | + nested_branch1 = RecipeBranch("nested1", "nested1_url") |
451 | + base_branch.nest_part_branch(nested_branch1, "foo", None) |
452 | + manifest = base_branch.get_recipe_text() |
453 | + self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" |
454 | + "base_url revid:base_revid\n" |
455 | + "nest-part nested1 nested1_url foo\n", |
456 | + manifest) |
457 | + |
458 | |
459 | class RecipeBranchTests(TestCaseInTempDir): |
460 |
Hi Aaron, this looks good. r=mars
You may want to self-review merges like this in future - I am not sure I contributed much :)