Merge lp:~abentley/bzr-builder/launchpad into lp:~launchpad-pqm/bzr-builder/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
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.

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

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 :)

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

Subscribers

People subscribed via source and target branches