Merge lp:~jtv/launchpad/bug-580337 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Edwin Grubbs
Approved revision: no longer in the source branch.
Merged at revision: 11083
Proposed branch: lp:~jtv/launchpad/bug-580337
Merge into: lp:launchpad
Diff against target: 378 lines (+184/-45)
4 files modified
lib/canonical/buildd/debian/changelog (+6/-0)
lib/canonical/buildd/pottery/intltool.py (+33/-8)
lib/canonical/buildd/translationtemplates.py (+3/-1)
lib/lp/translations/tests/test_pottery_detect_intltool.py (+142/-36)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-580337
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) Approve
Canonical Launchpad Engineering code Pending
Review via email: mp+28880@code.launchpad.net

Commit message

Make pottery deal with quotes.

Description of the change

= Bug 580337 =

The pottery code, which runs on build-farm slaves in order to generate translation templates from branches, was failing in some cases. The reason was that it attempts to extract a project name from the configure/autoconf/make setup found in the branch, but fails to deal with quotes around these names.

Here I add a bit of quote-stripping logic. It's pretty naïve, since project names are expected to be simple identifiers. After discussion with Henning, I applied it to both function parameters and variables, and removed the old special-case trick for removing M4-style bracket quotes.

A lot of the diff is actually a cleanup of the unit tests. The existing unit test created a single sample configuration file as part of its setup, for use as implicit state in the tests. Each test then verified one expected result from querying the file. This is poorly maintainable: a lot of what is being tested remains implicit ("the weird bit in this line doesn't affect how we deal with that line"). Such tests are easily broken by future maintenance of the test, or you find you can test one thing or the other but not both because they both need something different to be in the first/last place in a list etc. So I broke it up into individual explicit tests without reference to hidden state.

To exercise the tests:
{{{
./bin/test -vv -m lp.translations -t pottery
}}}

To Q/A, roll out this patch to the dogfood build slaves and generate some templates from branches. This is to verify that the existing functionality is still there. Then, do the same with lp:zeitgeist to see if that one has started working.

No lint.

Jeroen

To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Jeroen,

Looks good. Just one comment below.

-Edwin

>=== modified file 'lib/canonical/buildd/pottery/intltool.py'
>--- lib/canonical/buildd/pottery/intltool.py 2010-05-07 09:54:50 +0000
>+++ lib/canonical/buildd/pottery/intltool.py 2010-06-30 14:44:01 +0000
>@@ -86,10 +86,9 @@
> if params is None or len(params) < 2:
> return None
> if len(params) < 4:
>- value = params[0]
>+ return params[0]
> else:
>- value = params[3]
>- return value.strip("[]")
>+ return params[3]
>

The docstring for _get_AC_PACKAGE_NAME still says:
"They may be enclosed in []."

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/buildd/debian/changelog'
2--- lib/canonical/buildd/debian/changelog 2010-06-12 03:48:34 +0000
3+++ lib/canonical/buildd/debian/changelog 2010-06-30 15:48:33 +0000
4@@ -1,3 +1,9 @@
5+launchpad-buildd (64) hardy-cat; urgency=low
6+
7+ * Pottery now strips quotes from variables.
8+
9+ -- Jeroen Vermeulen <jtv@canonical.com> Wed, 30 Jun 2010 12:50:59 +0200
10+
11 launchpad-buildd (63) hardy-cat; urgency=low
12
13 * Drop apply-ogre-model, since override-sources-list replaced it three years
14
15=== modified file 'lib/canonical/buildd/pottery/intltool.py'
16--- lib/canonical/buildd/pottery/intltool.py 2010-05-07 09:54:50 +0000
17+++ lib/canonical/buildd/pottery/intltool.py 2010-06-30 15:48:33 +0000
18@@ -80,16 +80,15 @@
19
20 The value of AC_PACKAGE_NAME is either the first or the fourth
21 parameter of the AC_INIT call if it is called with at least two
22- parameters. They may be enclosed in [].
23+ parameters.
24 """
25 params = config_file.getFunctionParams("AC_INIT")
26 if params is None or len(params) < 2:
27 return None
28 if len(params) < 4:
29- value = params[0]
30+ return params[0]
31 else:
32- value = params[3]
33- return value.strip("[]")
34+ return params[3]
35
36
37 def _try_substitution(config_files, varname, substitution):
38@@ -151,14 +150,14 @@
39 if value == "AC_PACKAGE_NAME":
40 value = _get_AC_PACKAGE_NAME(config_files[-1])
41 else:
42- # Check if the value need a substitution.
43+ # Check if the value needs a substitution.
44 substitution = Substitution.get(value)
45 if substitution is not None:
46 # Try to substitute with value.
47 value = _try_substitution(
48 config_files, varname, substitution)
49 if value is None:
50- # No substitution found, the setup is broken.
51+ # No substitution found; the setup is broken.
52 break
53 if value is not None and not keep_trying:
54 # A value has been found.
55@@ -213,6 +212,28 @@
56 conf_file = file_or_name
57 self.content = conf_file.read()
58
59+ def _stripQuotes(self, identifier):
60+ """Strip surrounding quotes from `identifier`, if present.
61+
62+ :param identifier: a string, possibly surrounded by matching
63+ 'single,' "double," or [bracket] quotes.
64+ :return: `identifier` but with the outer pair of matching quotes
65+ removed, if they were there.
66+ """
67+ if len(identifier) < 2:
68+ return identifier
69+
70+ quote_pairs = [
71+ ('"', '"'),
72+ ("'", "'"),
73+ ("[", "]"),
74+ ]
75+ for (left, right) in quote_pairs:
76+ if identifier.startswith(left) and identifier.endswith(right):
77+ return identifier[1:-1]
78+
79+ return identifier
80+
81 def getVariable(self, name):
82 """Search the file for a variable definition with this name."""
83 pattern = re.compile(
84@@ -220,7 +241,7 @@
85 result = pattern.search(self.content)
86 if result is None:
87 return None
88- return result.group(1)
89+ return self._stripQuotes(result.group(1))
90
91 def getFunctionParams(self, name):
92 """Search file for a function call with this name, return parameters.
93@@ -229,7 +250,11 @@
94 result = pattern.search(self.content)
95 if result is None:
96 return None
97- return [param.strip() for param in result.group(1).split(',')]
98+ else:
99+ return [
100+ self._stripQuotes(param.strip())
101+ for param in result.group(1).split(',')
102+ ]
103
104
105 class Substitution(object):
106
107=== modified file 'lib/canonical/buildd/translationtemplates.py'
108--- lib/canonical/buildd/translationtemplates.py 2010-04-01 09:48:13 +0000
109+++ lib/canonical/buildd/translationtemplates.py 2010-06-30 15:48:33 +0000
110@@ -7,6 +7,7 @@
111
112 from canonical.buildd.debian import DebianBuildManager, DebianBuildState
113
114+
115 class TranslationTemplatesBuildState(DebianBuildState):
116 INSTALL = "INSTALL"
117 GENERATE = "GENERATE"
118@@ -65,7 +66,8 @@
119 # user. Should be safe to assume the home dirs are named identically.
120 assert self.home.startswith('/'), "home directory must be absolute."
121
122- path = os.path.join(self._chroot_path, self.home[1:], self._resultname)
123+ path = os.path.join(
124+ self._chroot_path, self.home[1:], self._resultname)
125 if os.access(path, os.F_OK):
126 self._slave.addWaitingFile(path)
127
128
129=== modified file 'lib/lp/translations/tests/test_pottery_detect_intltool.py'
130--- lib/lp/translations/tests/test_pottery_detect_intltool.py 2010-05-06 15:48:35 +0000
131+++ lib/lp/translations/tests/test_pottery_detect_intltool.py 2010-06-30 15:48:33 +0000
132@@ -1,4 +1,4 @@
133-# Copyright 2009 Canonical Ltd. This software is licensed under the
134+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
135 # GNU Affero General Public License version 3 (see the file LICENSE).
136
137 from __future__ import with_statement
138@@ -17,6 +17,7 @@
139 ConfigFile, check_potfiles_in, find_intltool_dirs, find_potfiles_in,
140 generate_pot, generate_pots, get_translation_domain)
141 from lp.testing import TestCase
142+from lp.testing.fakemethod import FakeMethod
143
144
145 class SetupTestPackageMixin(object):
146@@ -78,7 +79,7 @@
147 def test_check_potfiles_in_content_ok(self):
148 # Ideally all files listed in POTFILES.in exist in the source package.
149 self.prepare_package("intltool_single_ok")
150- self.assertTrue(check_potfiles_in("./po"))
151+ self.assertTrue(check_potfiles_in("./po"))
152
153 def test_check_potfiles_in_content_ok_file_added(self):
154 # If a file is not listed in POTFILES.in, the file is still good for
155@@ -87,7 +88,7 @@
156 added_file = file("./src/sourcefile_new.c", "w")
157 added_file.write("/* Test file. */")
158 added_file.close()
159- self.assertTrue(check_potfiles_in("./po"))
160+ self.assertTrue(check_potfiles_in("./po"))
161
162 def test_check_potfiles_in_content_not_ok_file_removed(self):
163 # If a file is missing that is listed in POTFILES.in, the file
164@@ -95,13 +96,13 @@
165 # our purposes.
166 self.prepare_package("intltool_single_ok")
167 os.remove("./src/sourcefile1.c")
168- self.assertFalse(check_potfiles_in("./po"))
169+ self.assertFalse(check_potfiles_in("./po"))
170
171 def test_check_potfiles_in_wrong_directory(self):
172 # Passing in the wrong directory will cause the check to fail
173 # gracefully and return False.
174 self.prepare_package("intltool_single_ok")
175- self.assertFalse(check_potfiles_in("./foo"))
176+ self.assertFalse(check_potfiles_in("./foo"))
177
178 def test_find_intltool_dirs(self):
179 # Complete run: find all directories with intltool structure.
180@@ -378,62 +379,167 @@
181
182 class TestConfigFile(TestCase):
183
184- def setUp(self):
185- super(TestConfigFile, self).setUp()
186- self.configfile = ConfigFile(StringIO(dedent("""\
187- # Demo config file
188- CCC
189- AAA=
190- FUNC_1(param1)
191- BBB =
192- FUNC_2(param1, param2,param3 )
193- CCC = ccc # comment
194- ML_FUNC_1(param1,
195- param2, param3)
196- DDD=dd.d
197- ML_FUNC_2(
198- param1,
199- param2)
200- EEE
201- = eee
202- """)))
203+ def _makeConfigFile(self, text):
204+ """Create a `ConfigFile` containing `text`."""
205+ return ConfigFile(StringIO(dedent(text)))
206+
207+ def test_getVariable_smoke(self):
208+ configfile = self._makeConfigFile("""
209+ A = 1
210+ B = 2
211+ C = 3
212+ """)
213+ self.assertEqual('1', configfile.getVariable('A'))
214+ self.assertEqual('2', configfile.getVariable('B'))
215+ self.assertEqual('3', configfile.getVariable('C'))
216
217 def test_getVariable_exists(self):
218- self.assertEqual('dd.d', self.configfile.getVariable('DDD'))
219+ configfile = self._makeConfigFile("DDD=dd.d")
220+ self.assertEqual('dd.d', configfile.getVariable('DDD'))
221+
222+ def test_getVariable_ignores_mere_mention(self):
223+ configfile = self._makeConfigFile("""
224+ CCC
225+ CCC = ccc # (this is the real definition)
226+ CCC
227+ """)
228+ self.assertEqual('ccc', configfile.getVariable('CCC'))
229+
230+ def test_getVariable_ignores_irrelevancies(self):
231+ configfile = self._makeConfigFile("""
232+ A = a
233+ ===
234+ blah
235+ FOO(n, m)
236+ a = case-insensitive
237+
238+ Z = z
239+ """)
240+ self.assertEqual('a', configfile.getVariable('A'))
241+ self.assertEqual('z', configfile.getVariable('Z'))
242
243 def test_getVariable_exists_spaces_comment(self):
244- self.assertEqual('ccc', self.configfile.getVariable('CCC'))
245+ configfile = self._makeConfigFile("CCC = ccc # comment")
246+ self.assertEqual('ccc', configfile.getVariable('CCC'))
247
248 def test_getVariable_empty(self):
249- self.assertEqual('', self.configfile.getVariable('AAA'))
250+ configfile = self._makeConfigFile("AAA=")
251+ self.assertEqual('', configfile.getVariable('AAA'))
252
253 def test_getVariable_empty_spaces(self):
254- self.assertEqual('', self.configfile.getVariable('BBB'))
255+ configfile = self._makeConfigFile("BBB = ")
256+ self.assertEqual('', configfile.getVariable('BBB'))
257
258 def test_getVariable_nonexistent(self):
259- self.assertIs(None, self.configfile.getVariable('FFF'))
260+ configfile = self._makeConfigFile("X = y")
261+ self.assertIs(None, configfile.getVariable('FFF'))
262
263 def test_getVariable_broken(self):
264- self.assertIs(None, self.configfile.getVariable('EEE'))
265+ configfile = self._makeConfigFile("EEE \n= eee")
266+ self.assertIs(None, configfile.getVariable('EEE'))
267+
268+ def test_getVariable_strips_quotes(self):
269+ # Quotes get stripped off variables.
270+ configfile = self._makeConfigFile("QQQ = 'qqq'")
271+ self.assertEqual('qqq', configfile.getVariable('QQQ'))
272+
273+ # This is done by invoking _stripQuotes (tested separately).
274+ configfile._stripQuotes = FakeMethod(result='foo')
275+ self.assertEqual('foo', configfile.getVariable('QQQ'))
276+ self.assertNotEqual(0, configfile._stripQuotes.call_count)
277
278 def test_getFunctionParams_single(self):
279- self.assertEqual(
280- ['param1'], self.configfile.getFunctionParams('FUNC_1'))
281+ configfile = self._makeConfigFile("FUNC_1(param1)")
282+ self.assertEqual(['param1'], configfile.getFunctionParams('FUNC_1'))
283
284 def test_getFunctionParams_multiple(self):
285+ configfile = self._makeConfigFile("FUNC_2(param1, param2, param3 )")
286 self.assertEqual(
287 ['param1', 'param2', 'param3'],
288- self.configfile.getFunctionParams('FUNC_2'))
289+ configfile.getFunctionParams('FUNC_2'))
290
291 def test_getFunctionParams_multiline_indented(self):
292+ configfile = self._makeConfigFile("""
293+ ML_FUNC_1(param1,
294+ param2, param3)
295+ """)
296 self.assertEqual(
297 ['param1', 'param2', 'param3'],
298- self.configfile.getFunctionParams('ML_FUNC_1'))
299+ configfile.getFunctionParams('ML_FUNC_1'))
300
301 def test_getFunctionParams_multiline_not_indented(self):
302+ configfile = self._makeConfigFile("""
303+ ML_FUNC_2(
304+ param1,
305+ param2)
306+ """)
307 self.assertEqual(
308- ['param1', 'param2'],
309- self.configfile.getFunctionParams('ML_FUNC_2'))
310+ ['param1', 'param2'], configfile.getFunctionParams('ML_FUNC_2'))
311+
312+ def test_getFunctionParams_strips_quotes(self):
313+ # Quotes get stripped off function parameters.
314+ configfile = self._makeConfigFile('FUNC("param")')
315+ self.assertEqual(['param'], configfile.getFunctionParams('FUNC'))
316+
317+ # This is done by invoking _stripQuotes (tested separately).
318+ configfile._stripQuotes = FakeMethod(result='arg')
319+ self.assertEqual(['arg'], configfile.getFunctionParams('FUNC'))
320+ self.assertNotEqual(0, configfile._stripQuotes.call_count)
321+
322+ def test_stripQuotes_unquoted(self):
323+ # _stripQuotes leaves unquoted identifiers intact.
324+ configfile = self._makeConfigFile('')
325+ self.assertEqual('hello', configfile._stripQuotes('hello'))
326+
327+ def test_stripQuotes_empty(self):
328+ configfile = self._makeConfigFile('')
329+ self.assertEqual('', configfile._stripQuotes(''))
330+
331+ def test_stripQuotes_single_quotes(self):
332+ # Single quotes are stripped.
333+ configfile = self._makeConfigFile('')
334+ self.assertEqual('x', configfile._stripQuotes("'x'"))
335+
336+ def test_stripQuotes_double_quotes(self):
337+ # Double quotes are stripped.
338+ configfile = self._makeConfigFile('')
339+ self.assertEqual('y', configfile._stripQuotes('"y"'))
340+
341+ def test_stripQuotes_bracket_quotes(self):
342+ # Brackets are stripped.
343+ configfile = self._makeConfigFile('')
344+ self.assertEqual('z', configfile._stripQuotes('[z]'))
345+
346+ def test_stripQuotes_opening_brackets(self):
347+ # An opening bracket must be matched by a closing one.
348+ configfile = self._makeConfigFile('')
349+ self.assertEqual('[x[', configfile._stripQuotes('[x['))
350+
351+ def test_stripQuotes_closing_brackets(self):
352+ # A closing bracket is not accepted as an opening quote.
353+ configfile = self._makeConfigFile('')
354+ self.assertEqual(']x]', configfile._stripQuotes(']x]'))
355+
356+ def test_stripQuotes_multiple(self):
357+ # Only a single layer of quotes is stripped.
358+ configfile = self._makeConfigFile('')
359+ self.assertEqual('"n"', configfile._stripQuotes("'\"n\"'"))
360+
361+ def test_stripQuotes_single_quote(self):
362+ # A string consisting of just one quote is not stripped.
363+ configfile = self._makeConfigFile('')
364+ self.assertEqual("'", configfile._stripQuotes("'"))
365+
366+ def test_stripQuotes_mismatched(self):
367+ # Mismatched quotes are not stripped.
368+ configfile = self._makeConfigFile('')
369+ self.assertEqual("'foo\"", configfile._stripQuotes("'foo\""))
370+
371+ def test_stripQuotes_unilateral(self):
372+ # A quote that's only on one end doesn't get stripped.
373+ configfile = self._makeConfigFile('')
374+ self.assertEqual('"foo', configfile._stripQuotes('"foo'))
375+
376
377
378 def test_suite():