Merge lp:~henninge/launchpad/bug-545354-enable-sharing into lp:~launchpad/launchpad/recife
- bug-545354-enable-sharing
- Merge into recife
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Edwin Grubbs | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 9139 | ||||
Proposed branch: | lp:~henninge/launchpad/bug-545354-enable-sharing | ||||
Merge into: | lp:~launchpad/launchpad/recife | ||||
Diff against target: |
918 lines (+666/-82) 5 files modified
database/schema/security.cfg (+1/-0) lib/lp/translations/doc/potemplate.txt (+7/-3) lib/lp/translations/interfaces/potemplate.py (+23/-0) lib/lp/translations/model/potemplate.py (+161/-65) lib/lp/translations/tests/test_shared_potemplate.py (+474/-14) |
||||
To merge this branch: | bzr merge lp:~henninge/launchpad/bug-545354-enable-sharing | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Edwin Grubbs (community) | Approve | ||
Review via email: mp+27123@code.launchpad.net |
Commit message
Enable message sharing between products and source packages.
Description of the change
= Bug 545354 =
Enlarges the collection of sharing templates in a POTemplateShari
== Pre-implementation discussion ==
We discussed some of this in Recife in March but also just recently. The latest findings can be found in my comments on the bug. One design goal was to make sure the number of SQL queries remains constant and independent of the number of templates in the collection.
== Implementation details ==
The query in _queryByProduct is best understood by looking at this little picture: http://
Also in this method I used ID throughout although not all relations need to be that explicit but I found mixing those more confusing to read.
== Tests ==
A new test case was created to test getSharingPOTem
./test -vvt MessageSharingP
== Lint ==
No lint left.
Henning Eggers (henninge) wrote : | # |
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Henning,
This looks good. I just have a few comments below.
merge-conditional
-Edwin
>=== modified file 'lib/lp/
>--- lib/lp/
>+++ lib/lp/
>@@ -184,6 +184,21 @@
>
> _uses_english_
>
>+ @cachedproperty
>+ def _sharing_ids(self):
>+ """Return the IDs of all sharing templates including this one."""
>+ subset = getUtility(
>+ product=
>+ distribution=
>+ sourcepackagena
>+ # Convert to a list for caching.
>+ result = list(subset.
>+ if len(result) == 0:
>+ # Always return at least the template itself.
>+ return [self.id]
>+ else:
>+ return result
Should you also make sure that self.id is in result even if len != 0?
> def __storm_
> self.clearPOFil
> self._uses_
>@@ -1387,45 +1425,102 @@
> package = template.
> return (template.name, package)
>
>- def _iterate_
>- """Yield all templates matching the provided argument.
>-
>- This is much like a `IPOTemplateSub
>- `Product`s and `Distribution`s rather than `ProductSeries` and
>- `DistroSeries`.
>- """
>- if self.product:
>- subsets = [
>- self.potemplate
>- for series in self.product.series
>- ]
>+ def _queryByProduct
>+ """Build the query that finds POTemplates by their linked product.
>+
>+ Queries the Packaging table to find templates in linked source
>+ packages, too.
>+
>+ :param templatename_
>+ add to the where clause of the query that will select the template
>+ name.
>+ :return: A ResultSet for the query.
>+ """
>+ from lp.registry.
>+
>+ ProductSeries1 = ClassAlias(
>+ origin = LeftJoin(
>+ LeftJoin(
>+ POTemplate, ProductSeries,
>+ POTemplate.
>+ Join(
>+ Packaging, ProductSeries1,
>+ Packaging.
>+ And(
>+ Packaging.
>+ Packaging.
>+ POTemplate.
>+ )
>+ )
>+ return Store.of(
>+ POTemplate,
>+ And(
>+ Or(
>+ ProductSeries.
>+ ProductSeries1.
>+ ),
>+ ...
Henning Eggers (henninge) wrote : | # |
Am 16.06.2010 23:24, schrieb Edwin Grubbs:
> This looks good. I just have a few comments below.
Thanks for the review. I explained one away and fixed two. ;)
>> === modified file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -184,6 +184,21 @@
>>
>> _uses_english_
>>
>> + @cachedproperty
>> + def _sharing_ids(self):
>> + """Return the IDs of all sharing templates including this one."""
>> + subset = getUtility(
>> + product=
>> + distribution=
>> + sourcepackagena
>> + # Convert to a list for caching.
>> + result = list(subset.
>> + if len(result) == 0:
>> + # Always return at least the template itself.
>> + return [self.id]
>> + else:
>> + return result
>
>
> Should you also make sure that self.id is in result even if len != 0?
No. Actually, the len == 0 case is basically a "no sharing possible case" and
with a single id in the list no sharing happens. If we get any ids back, those
are the sharing ones and we can trust them to include all we need.
[...]
>> def groupEquivalent
>>
>> - for template in self._iterate_
>> + if name_pattern is None:
>> + templatename_clause = "1=1"
>> + else:
>> + templatename_clause = "potemplate.name ~ '%s'" % name_pattern
>
>
> I think you should either escape the name_pattern with sqlvalues,
> or you should convert it to a storm conditional.
Yes, thank you. I fixed that by using sqlvalues. Also I replaced "1=1" with
True since we are still in storm land here. I also pulled this out into
another variant of getSharingPOTem
>
>
>> +
>> + for template in self._queryPOTe
>> key = self._get_
>> if key not in equivalents:
>> equivalents[key] = []
>> === modified file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -177,5 +176,398 @@
>> self.assertEqua
>>
>>
>> +class TestMessageShar
>> + """Test message sharing between a product and a package.
>> +
>> + Each test uses assertStatement
>> + queries does not change. This was integrated here to avoid having
>> + a second test case just for statement counts.
>> + The current implementation is good and only needs one statement."""
>
>
> Ending triple-quotes belongs by itself on the following line, since
> this is a multiline docstring.
>
True, thanks.
...
1 | === modified file 'lib/lp/translations/interfaces/potemplate.py' |
2 | --- lib/lp/translations/interfaces/potemplate.py 2010-06-09 07:36:35 +0000 |
3 | +++ lib/lp/translations/interfaces/potemplate.py 2010-06-18 10:16:50 +0000 |
4 | @@ -695,6 +695,17 @@ |
5 | :return: A list of all potemplates of the same name from all series. |
6 | """ |
7 | |
8 | + def getSharingPOTemplatesRegex(name_pattern=None): |
9 | + """Find all sharing templates with names matching the given pattern. |
10 | + |
11 | + If name_pattern is None, match is performed on the template name. |
12 | + Use with care as it may return all templates in a distribution! |
13 | + |
14 | + :param name_pattern: A POSIX regular expression that the template |
15 | + is matched against. |
16 | + :return: A list of all potemplates matching the pattern. |
17 | + """ |
18 | + |
19 | def getSharingPOTemplateIDs(potemplate_name): |
20 | """Find database ids of all sharing templates of the given name. |
21 | |
22 | |
23 | === modified file 'lib/lp/translations/model/potemplate.py' |
24 | --- lib/lp/translations/model/potemplate.py 2010-06-16 16:31:06 +0000 |
25 | +++ lib/lp/translations/model/potemplate.py 2010-06-18 11:05:58 +0000 |
26 | @@ -1506,6 +1506,16 @@ |
27 | return self._queryPOTemplates( |
28 | POTemplate.name == potemplate_name) |
29 | |
30 | + def getSharingPOTemplatesRegex(self, name_pattern=None): |
31 | + """See IPOTemplateSharingSubset.""" |
32 | + if name_pattern is None: |
33 | + templatename_clause = True |
34 | + else: |
35 | + templatename_clause = ( |
36 | + "potemplate.name ~ %s" % sqlvalues(name_pattern)) |
37 | + |
38 | + return self._queryPOTemplates(templatename_clause) |
39 | + |
40 | def getSharingPOTemplateIDs(self, potemplate_name): |
41 | """See IPOTemplateSharingSubset.""" |
42 | return self.getSharingPOTemplates(potemplate_name).values( |
43 | @@ -1515,12 +1525,7 @@ |
44 | """See IPOTemplateSharingSubset.""" |
45 | equivalents = {} |
46 | |
47 | - if name_pattern is None: |
48 | - templatename_clause = "1=1" |
49 | - else: |
50 | - templatename_clause = "potemplate.name ~ '%s'" % name_pattern |
51 | - |
52 | - for template in self._queryPOTemplates(templatename_clause): |
53 | + for template in self.getSharingPOTemplatesRegex(name_pattern): |
54 | key = self._get_potemplate_equivalence_class(template) |
55 | if key not in equivalents: |
56 | equivalents[key] = [] |
57 | |
58 | === modified file 'lib/lp/translations/tests/test_shared_potemplate.py' |
59 | --- lib/lp/translations/tests/test_shared_potemplate.py 2010-06-09 09:16:54 +0000 |
60 | +++ lib/lp/translations/tests/test_shared_potemplate.py 2010-06-18 11:15:54 +0000 |
61 | @@ -7,6 +7,7 @@ |
62 | |
63 | import unittest |
64 | |
65 | +from storm.exceptions import DataError |
66 | from zope.component import getUtility |
67 | from zope.security.proxy import removeSecurityProxy |
68 | |
69 | @@ -176,13 +177,69 @@ |
70 | self.assertEquals(potmsgset, shared_potmsgset) |
71 | |
72 | |
73 | +class TestSharingPOTemplatesRegex(TestCaseWithFactory): |
74 | + """Isolate tests for regular expression use in SharingSubset.""" |
75 | + |
76 | + layer = ZopelessDatabaseLayer |
77 | + |
78 | + def setUp(self): |
79 | + super(TestSharingPOTemplatesRegex, self).setUp() |
80 | + self.product = self.factory.makeProduct() |
81 | + self.product.official_rosetta = True |
82 | + self.trunk = self.product.getSeries('trunk') |
83 | + self.potemplateset = getUtility(IPOTemplateSet) |
84 | + |
85 | + def _makeTemplates(self, names): |
86 | + # Create some templates with the given names. |
87 | + return [ |
88 | + self.factory.makePOTemplate(productseries=self.trunk, name=name) |
89 | + for name in names] |
90 | + |
91 | + def test_getSharingPOTemplatesRegex_baseline(self): |
92 | + # Baseline test. |
93 | + templates = self._makeTemplates(['foo', 'foo-bar', 'foo-two']) |
94 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
95 | + self.assertContentEqual( |
96 | + templates, subset.getSharingPOTemplatesRegex('foo.*')) |
97 | + |
98 | + def test_getSharingPOTemplatesRegex_not_all(self): |
99 | + # A template may not match. |
100 | + templates = self._makeTemplates(['foo', 'foo-bar', 'foo-two']) |
101 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
102 | + self.assertContentEqual( |
103 | + templates[1:], subset.getSharingPOTemplatesRegex('foo-.*')) |
104 | + |
105 | + def test_getSharingPOTemplatesRegex_all(self): |
106 | + # Not passing a pattern returns all templates. |
107 | + templates = self._makeTemplates(['foo', 'foo-bar', 'foo-two']) |
108 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
109 | + self.assertContentEqual( |
110 | + templates, subset.getSharingPOTemplatesRegex()) |
111 | + |
112 | + def test_getSharingPOTemplatesRegex_robustness_quotes(self): |
113 | + # Quotes in the pattern can be dangerous. |
114 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
115 | + self.assertContentEqual( |
116 | + [], subset.getSharingPOTemplatesRegex("'\"")) |
117 | + |
118 | + def test_getSharingPOTemplatesRegex_robustness_backslash(self): |
119 | + # A backslash at the end could escape enclosing quotes without |
120 | + # proper escaping, leading to a SyntaxError or even a successful |
121 | + # exploit. Instead, storm should complain about an invalid expression |
122 | + # by raising DataError. |
123 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
124 | + self.assertRaises( |
125 | + DataError, list, subset.getSharingPOTemplatesRegex("foo.*\\")) |
126 | + |
127 | + |
128 | class TestMessageSharingProductPackage(TestCaseWithFactory): |
129 | """Test message sharing between a product and a package. |
130 | |
131 | Each test uses assertStatementCount to make sure the number of SQL |
132 | queries does not change. This was integrated here to avoid having |
133 | a second test case just for statement counts. |
134 | - The current implementation is good and only needs one statement.""" |
135 | + The current implementation is good and only needs one statement. |
136 | + """ |
137 | |
138 | layer = ZopelessDatabaseLayer |
139 |
Jeroen T. Vermeulen (jtv) wrote : | # |
I looked over the incremental diff at Henning's request. A few notes, mostly already discussed on IRC. I think having the separate unit test for the regex matching (which was a bit of a wildcard, ha ha) worked out really well.
These came up on IRC:
* Backquote identifiers in docstrings: "See `IPOTemplateSha
* getSharingPOTem
* No need to create a Product and ProductSeries; create a series and use its product.
The unit tests mostly follow the pattern "create templates with names [...]; create a subset; select from subset by regex; verify output." Most of these would be one-liners if you had a method for "create templates with names [...] (parameter 1); create a subset; select from subset by regex (parameter 2); return list of names of the selected templates." The caller could then assertContentsEqual the result against a list of expected strings. That would also help with:
* Making the output more obvious (the relevance of "templates[1:]" is less clear).
* Testing lots of other things that could go wrong; see below.
* Eliminating implicit state. You wouldn't need any setUp at all.
Other notes:
* You may want to test that you're really getting a match, not a search: "xyz" should not match "y.*" As long as we're not taking the regex from user input but from existing template names, this is a bigger risk than unescaped quotes. (Not that I disagree with your approach of testing for those right from the start!)
* "# Quotes in the pattern can be dangerous." To me that looks like you're specifying the appropriate behaviour! How about "quotes do not confuse the regex match"?
* I'd test single and double quotes separately, not in the same string.
* When you test for quoting robustness, you might as well set up a template to establish that you don't get any unexpected matches.
* You don't exercise the case where no templates match.
Apart from those notes, the incremental diff has my unreserved blessing.
Jeroen
Preview Diff
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2010-05-20 13:59:11 +0000 |
3 | +++ database/schema/security.cfg 2010-06-22 07:46:30 +0000 |
4 | @@ -1341,6 +1341,7 @@ |
5 | public.distribution = SELECT |
6 | public.distroseries = SELECT |
7 | public.language = SELECT |
8 | +public.packaging = SELECT |
9 | public.person = SELECT |
10 | public.pofile = SELECT, INSERT, UPDATE |
11 | public.pomsgid = SELECT |
12 | |
13 | === modified file 'lib/lp/translations/doc/potemplate.txt' |
14 | --- lib/lp/translations/doc/potemplate.txt 2010-04-26 16:00:31 +0000 |
15 | +++ lib/lp/translations/doc/potemplate.txt 2010-06-22 07:46:30 +0000 |
16 | @@ -228,7 +228,7 @@ |
17 | |
18 | == getPOFileByPath == |
19 | |
20 | -We can get an IPOFile inside a templage based on its path. |
21 | +We can get an IPOFile inside a template based on its path. |
22 | |
23 | >>> pofile = potemplate.getPOFileByPath('es.po') |
24 | >>> print pofile.title |
25 | @@ -614,7 +614,10 @@ |
26 | >>> print exported_translation_file.content_type |
27 | application/x-gtar |
28 | |
29 | -Inspecting the tarball content, we have the list of entries exported. |
30 | +Inspecting the tarball content, we have the list of entries exported. This |
31 | +includes the 'pt' POFile that was created earlier on the 'evolution' |
32 | +product as this is sharing translations with the source package that this |
33 | +potemplate is from. |
34 | |
35 | >>> from canonical.launchpad.helpers import string_to_tarfile |
36 | >>> tarfile_string = exported_translation_file.read() |
37 | @@ -625,7 +628,7 @@ |
38 | >>> sorted(name.rstrip('/') for name in tarfile.getnames()) |
39 | ['evolution-2.2', 'evolution-2.2/evolution-2.2-es.po', |
40 | 'evolution-2.2/evolution-2.2-ja.po', 'evolution-2.2/evolution-2.2-xh.po', |
41 | - 'po', 'po/evolution-2.2.pot'] |
42 | + 'po', 'po/evolution-2.2-pt.po', 'po/evolution-2.2.pot'] |
43 | |
44 | The *-es.po file is indeed the Spanish translation... |
45 | |
46 | @@ -644,6 +647,7 @@ |
47 | evolution-2.2/evolution-2.2-ja.po |
48 | evolution-2.2/evolution-2.2-xh.po |
49 | po/ |
50 | + po/evolution-2.2-pt.po |
51 | po/evolution-2.2.pot |
52 | >>> pofile = simple_popen2( |
53 | ... ['tar', 'zxfO', '-', 'evolution-2.2/evolution-2.2-es.po'], |
54 | |
55 | === modified file 'lib/lp/translations/interfaces/potemplate.py' |
56 | --- lib/lp/translations/interfaces/potemplate.py 2009-11-27 12:50:16 +0000 |
57 | +++ lib/lp/translations/interfaces/potemplate.py 2010-06-22 07:46:30 +0000 |
58 | @@ -695,6 +695,29 @@ |
59 | :return: A list of all potemplates of the same name from all series. |
60 | """ |
61 | |
62 | + def getSharingPOTemplatesByRegex(name_pattern=None): |
63 | + """Find all sharing templates with names matching the given pattern. |
64 | + |
65 | + If name_pattern is None, match is performed on the template name. |
66 | + Use with care as it may return all templates in a distribution! |
67 | + |
68 | + :param name_pattern: A POSIX regular expression that the template |
69 | + is matched against. |
70 | + :return: A list of all potemplates matching the pattern. |
71 | + """ |
72 | + |
73 | + def getSharingPOTemplateIDs(potemplate_name): |
74 | + """Find database ids of all sharing templates of the given name. |
75 | + |
76 | + For distributions this method requires that sourcepackagename is set. |
77 | + This avoids serialization of full POTemplate objects. |
78 | + |
79 | + :param potemplate_name: The name of the template for which to find |
80 | + sharing equivalents. |
81 | + :return: A list of database ids of all potemplates of the same name |
82 | + from all series. |
83 | + """ |
84 | + |
85 | def groupEquivalentPOTemplates(name_pattern=None): |
86 | """Within given IProduct or IDistribution, find equivalent templates. |
87 | |
88 | |
89 | === modified file 'lib/lp/translations/model/potemplate.py' |
90 | --- lib/lp/translations/model/potemplate.py 2010-01-12 21:29:03 +0000 |
91 | +++ lib/lp/translations/model/potemplate.py 2010-06-22 07:46:30 +0000 |
92 | @@ -18,7 +18,6 @@ |
93 | import datetime |
94 | import logging |
95 | import os |
96 | -import re |
97 | |
98 | from psycopg2.extensions import TransactionRollbackError |
99 | from sqlobject import ( |
100 | @@ -26,7 +25,7 @@ |
101 | StringCol) |
102 | from storm.expr import Alias, And, Desc, In, Join, LeftJoin, Or, SQL |
103 | from storm.info import ClassAlias |
104 | -from storm.store import Store |
105 | +from storm.store import EmptyResultSet, Store |
106 | from zope.component import getAdapter, getUtility |
107 | from zope.interface import implements |
108 | from zope.security.proxy import removeSecurityProxy |
109 | @@ -43,6 +42,7 @@ |
110 | from lp.services.database.prejoin import prejoin |
111 | from lp.services.worlddata.model.language import Language |
112 | from lp.registry.interfaces.person import validate_public_person |
113 | +from lp.registry.model.packaging import Packaging |
114 | from lp.registry.model.sourcepackagename import SourcePackageName |
115 | from lp.translations.model.pofile import POFile, DummyPOFile |
116 | from lp.translations.model.pomsgid import POMsgID |
117 | @@ -184,6 +184,21 @@ |
118 | |
119 | _uses_english_msgids = None |
120 | |
121 | + @cachedproperty |
122 | + def _sharing_ids(self): |
123 | + """Return the IDs of all sharing templates including this one.""" |
124 | + subset = getUtility(IPOTemplateSet).getSharingSubset( |
125 | + product=self.product, |
126 | + distribution=self.distribution, |
127 | + sourcepackagename=self.sourcepackagename) |
128 | + # Convert to a list for caching. |
129 | + result = list(subset.getSharingPOTemplateIDs(self.name)) |
130 | + if len(result) == 0: |
131 | + # Always return at least the template itself. |
132 | + return [self.id] |
133 | + else: |
134 | + return result |
135 | + |
136 | def __storm_invalidated__(self): |
137 | self.clearPOFileCache() |
138 | self._uses_english_msgids = None |
139 | @@ -339,8 +354,7 @@ |
140 | if self.productseries is not None: |
141 | return self.productseries |
142 | elif self.distroseries is not None: |
143 | - from lp.registry.model.sourcepackage import \ |
144 | - SourcePackage |
145 | + from lp.registry.model.sourcepackage import SourcePackage |
146 | return SourcePackage(distroseries=self.distroseries, |
147 | sourcepackagename=self.sourcepackagename) |
148 | raise AssertionError('Unknown POTemplate translation target') |
149 | @@ -560,31 +574,10 @@ |
150 | ] |
151 | clause_tables = ['TranslationTemplateItem'] |
152 | if sharing_templates: |
153 | - clauses.extend([ |
154 | - 'TranslationTemplateItem.potemplate = POTemplate.id', |
155 | - 'TranslationTemplateItem.potmsgset = POTMsgSet.id', |
156 | - 'POTemplate.name = %s' % sqlvalues(self.name), |
157 | - ]) |
158 | - product = None |
159 | - distribution = None |
160 | - clause_tables.append('POTemplate') |
161 | - if self.productseries is not None: |
162 | - product = self.productseries.product |
163 | - clauses.extend([ |
164 | - 'POTemplate.productseries=ProductSeries.id', |
165 | - 'ProductSeries.product %s' % self._null_quote(product) |
166 | - ]) |
167 | - clause_tables.append('ProductSeries') |
168 | - elif self.distroseries is not None: |
169 | - distribution = self.distroseries.distribution |
170 | - clauses.extend([ |
171 | - 'POTemplate.distroseries=DistroSeries.id', |
172 | - 'DistroSeries.distribution %s' % ( |
173 | - self._null_quote(distribution)), |
174 | - 'POTemplate.sourcepackagename %s' % self._null_quote( |
175 | - self.sourcepackagename), |
176 | - ]) |
177 | - clause_tables.append('DistroSeries') |
178 | + clauses.append( |
179 | + 'TranslationTemplateItem.potemplate in %s' % sqlvalues( |
180 | + self._sharing_ids) |
181 | + ) |
182 | else: |
183 | clauses.append( |
184 | 'TranslationTemplateItem.potemplate = %s' % sqlvalues(self)) |
185 | @@ -1374,9 +1367,54 @@ |
186 | assert distribution or not sourcepackagename, ( |
187 | "Picking a source package only makes sense with a distribution.") |
188 | self.potemplateset = potemplateset |
189 | - self.distribution = distribution |
190 | - self.sourcepackagename = sourcepackagename |
191 | - self.product = product |
192 | + self.share_by_name = False |
193 | + self.subset = None |
194 | + |
195 | + if distribution is not None: |
196 | + self.distribution = distribution |
197 | + self.sourcepackagename = sourcepackagename |
198 | + if sourcepackagename is not None: |
199 | + product = self._findUpstreamProduct( |
200 | + distribution, sourcepackagename) |
201 | + if product is None: |
202 | + self.share_by_name = self._canShareByName( |
203 | + distribution, sourcepackagename) |
204 | + if product is not None: |
205 | + self.product = product |
206 | + |
207 | + def _findUpstreamProduct(self, distribution, sourcepackagename): |
208 | + """Find the upstream product by looking at the translation focus. |
209 | + |
210 | + The translation focus is used to pick a distroseries, so a source |
211 | + package instance can be created. If no translation focus is set, |
212 | + the distribution's current series is used.""" |
213 | + |
214 | + from lp.registry.model.sourcepackage import SourcePackage |
215 | + distroseries = distribution.translation_focus |
216 | + if distroseries is None: |
217 | + distroseries = distribution.currentseries |
218 | + sourcepackage = SourcePackage(sourcepackagename, distroseries) |
219 | + if sourcepackage.productseries is None: |
220 | + return None |
221 | + return sourcepackage.productseries.product |
222 | + |
223 | + def _canShareByName(self, distribution, sourcepackagename): |
224 | + """Determine if sharing by sourcepackagename is a wise thing. |
225 | + |
226 | + Without a product, the linkage between sharing packages can only be |
227 | + determined by their name. This is only (fairly) safe if none of these |
228 | + is packaged elsewhere. |
229 | + """ |
230 | + from lp.registry.model.distroseries import DistroSeries |
231 | + origin = Join( |
232 | + Packaging, DistroSeries, |
233 | + Packaging.distroseries == DistroSeries.id) |
234 | + result = Store.of(distribution).using(origin).find( |
235 | + Packaging, |
236 | + And(DistroSeries.distribution == distribution.id, |
237 | + Packaging.sourcepackagename == sourcepackagename.id) |
238 | + ).count() |
239 | + return result == 0 |
240 | |
241 | |
242 | def _get_potemplate_equivalence_class(self, template): |
243 | @@ -1387,45 +1425,107 @@ |
244 | package = template.sourcepackagename.name |
245 | return (template.name, package) |
246 | |
247 | - def _iterate_potemplates(self, name_pattern=None): |
248 | - """Yield all templates matching the provided argument. |
249 | - |
250 | - This is much like a `IPOTemplateSubset`, except it operates on |
251 | - `Product`s and `Distribution`s rather than `ProductSeries` and |
252 | - `DistroSeries`. |
253 | - """ |
254 | - if self.product: |
255 | - subsets = [ |
256 | - self.potemplateset.getSubset(productseries=series) |
257 | - for series in self.product.series |
258 | - ] |
259 | + def _queryByProduct(self, templatename_clause): |
260 | + """Build the query that finds POTemplates by their linked product. |
261 | + |
262 | + Queries the Packaging table to find templates in linked source |
263 | + packages, too. |
264 | + |
265 | + :param templatename_clause: A string or a storm expression to |
266 | + add to the where clause of the query that will select the template |
267 | + name. |
268 | + :return: A ResultSet for the query. |
269 | + """ |
270 | + from lp.registry.model.productseries import ProductSeries |
271 | + |
272 | + ProductSeries1 = ClassAlias(ProductSeries) |
273 | + origin = LeftJoin( |
274 | + LeftJoin( |
275 | + POTemplate, ProductSeries, |
276 | + POTemplate.productseriesID == ProductSeries.id), |
277 | + Join( |
278 | + Packaging, ProductSeries1, |
279 | + Packaging.productseriesID == ProductSeries1.id), |
280 | + And( |
281 | + Packaging.distroseriesID == POTemplate.distroseriesID, |
282 | + Packaging.sourcepackagenameID == ( |
283 | + POTemplate.sourcepackagenameID) |
284 | + ) |
285 | + ) |
286 | + return Store.of(self.product).using(origin).find( |
287 | + POTemplate, |
288 | + And( |
289 | + Or( |
290 | + ProductSeries.productID == self.product.id, |
291 | + ProductSeries1.productID == self.product.id |
292 | + ), |
293 | + templatename_clause |
294 | + )) |
295 | + |
296 | + def _queryBySourcepackagename(self, templatename_clause): |
297 | + """Build the query that finds POTemplates by their names. |
298 | + |
299 | + :param templatename_clause: A string or a storm expression to |
300 | + add to the where clause of the query that will select the template |
301 | + name. |
302 | + :return: A ResultSet for the query. |
303 | + """ |
304 | + from lp.registry.model.distroseries import DistroSeries |
305 | + origin = Join( |
306 | + POTemplate, DistroSeries, |
307 | + POTemplate.distroseries == DistroSeries.id) |
308 | + return Store.of(self.distribution).using(origin).find( |
309 | + POTemplate, |
310 | + And( |
311 | + DistroSeries.distributionID == self.distribution.id, |
312 | + POTemplate.sourcepackagename == self.sourcepackagename, |
313 | + templatename_clause |
314 | + )) |
315 | + |
316 | + def _queryByDistribution(self, templatename_clause): |
317 | + """Special case when templates are searched across a distribution.""" |
318 | + return Store.of(self.distribution).find( |
319 | + POTemplate, templatename_clause) |
320 | + |
321 | + def _queryPOTemplates(self, templatename_clause): |
322 | + """Select the right query to be used.""" |
323 | + if self.product is not None: |
324 | + return self._queryByProduct(templatename_clause) |
325 | + elif self.share_by_name: |
326 | + return self._queryBySourcepackagename(templatename_clause) |
327 | + elif self.distribution is not None and self.sourcepackagename is None: |
328 | + return self._queryByDistribution(templatename_clause) |
329 | else: |
330 | - subsets = [ |
331 | - self.potemplateset.getSubset( |
332 | - distroseries=series, |
333 | - sourcepackagename=self.sourcepackagename) |
334 | - for series in self.distribution.series |
335 | - ] |
336 | - for subset in subsets: |
337 | - for template in subset: |
338 | - if name_pattern is None or re.match(name_pattern, |
339 | - template.name): |
340 | - yield template |
341 | + return EmptyResultSet() |
342 | |
343 | def getSharingPOTemplates(self, potemplate_name): |
344 | - """See IPOTemplateSharingSubset.""" |
345 | + """See `IPOTemplateSharingSubset`.""" |
346 | if self.distribution is not None: |
347 | assert self.sourcepackagename is not None, ( |
348 | "Need sourcepackagename to select from distribution.") |
349 | - |
350 | - escaped_potemplate_name = re.escape(potemplate_name) |
351 | - return self._iterate_potemplates("^%s$" % escaped_potemplate_name) |
352 | + return self._queryPOTemplates( |
353 | + POTemplate.name == potemplate_name) |
354 | + |
355 | + def getSharingPOTemplatesByRegex(self, name_pattern=None): |
356 | + """See `IPOTemplateSharingSubset`.""" |
357 | + if name_pattern is None: |
358 | + templatename_clause = True |
359 | + else: |
360 | + templatename_clause = ( |
361 | + "potemplate.name ~ %s" % sqlvalues(name_pattern)) |
362 | + |
363 | + return self._queryPOTemplates(templatename_clause) |
364 | + |
365 | + def getSharingPOTemplateIDs(self, potemplate_name): |
366 | + """See `IPOTemplateSharingSubset`.""" |
367 | + return self.getSharingPOTemplates(potemplate_name).values( |
368 | + POTemplate.id) |
369 | |
370 | def groupEquivalentPOTemplates(self, name_pattern=None): |
371 | - """See IPOTemplateSharingSubset.""" |
372 | + """See `IPOTemplateSharingSubset`.""" |
373 | equivalents = {} |
374 | |
375 | - for template in self._iterate_potemplates(name_pattern): |
376 | + for template in self.getSharingPOTemplatesByRegex(name_pattern): |
377 | key = self._get_potemplate_equivalence_class(template) |
378 | if key not in equivalents: |
379 | equivalents[key] = [] |
380 | @@ -1510,10 +1610,6 @@ |
381 | if flag |
382 | ]) |
383 | |
384 | - # Store sequences so we can detect later whether we changed the |
385 | - # message. |
386 | - sequence = row.sequence |
387 | - |
388 | # Store the message. |
389 | messages.append(msgset) |
390 | |
391 | |
392 | === modified file 'lib/lp/translations/tests/test_shared_potemplate.py' |
393 | --- lib/lp/translations/tests/test_shared_potemplate.py 2010-05-18 14:00:10 +0000 |
394 | +++ lib/lp/translations/tests/test_shared_potemplate.py 2010-06-22 07:46:30 +0000 |
395 | @@ -7,13 +7,16 @@ |
396 | |
397 | import unittest |
398 | |
399 | +from storm.exceptions import DataError |
400 | +from zope.component import getUtility |
401 | from zope.security.proxy import removeSecurityProxy |
402 | |
403 | -from lp.testing.factory import LaunchpadObjectFactory |
404 | +from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
405 | from canonical.testing import ZopelessDatabaseLayer |
406 | - |
407 | - |
408 | -class TestTranslationSharingPOTemplate(unittest.TestCase): |
409 | +from lp.testing import TestCaseWithFactory |
410 | +from lp.translations.interfaces.potemplate import IPOTemplateSet |
411 | + |
412 | +class TestTranslationSharingPOTemplate(TestCaseWithFactory): |
413 | """Test behaviour of "sharing" PO templates.""" |
414 | |
415 | layer = ZopelessDatabaseLayer |
416 | @@ -22,20 +25,19 @@ |
417 | """Set up context to test in.""" |
418 | # Create a product with two series and sharing POTemplates |
419 | # in different series ('devel' and 'stable'). |
420 | - factory = LaunchpadObjectFactory() |
421 | - self.factory = factory |
422 | - self.foo = factory.makeProduct() |
423 | - self.foo_devel = factory.makeProductSeries( |
424 | + super(TestTranslationSharingPOTemplate, self).setUp() |
425 | + self.foo = self.factory.makeProduct() |
426 | + self.foo_devel = self.factory.makeProductSeries( |
427 | name='devel', product=self.foo) |
428 | - self.foo_stable = factory.makeProductSeries( |
429 | + self.foo_stable = self.factory.makeProductSeries( |
430 | name='stable', product=self.foo) |
431 | self.foo.official_rosetta = True |
432 | |
433 | # POTemplate is a 'sharing' one if it has the same name ('messages'). |
434 | - self.devel_potemplate = factory.makePOTemplate( |
435 | + self.devel_potemplate = self.factory.makePOTemplate( |
436 | productseries=self.foo_devel, name="messages") |
437 | - self.stable_potemplate = factory.makePOTemplate(self.foo_stable, |
438 | - name="messages") |
439 | + self.stable_potemplate = self.factory.makePOTemplate( |
440 | + self.foo_stable, name="messages") |
441 | |
442 | # Create a single POTMsgSet that is used across all tests, |
443 | # and add it to only one of the POTemplates. |
444 | @@ -106,8 +108,6 @@ |
445 | self.assertEquals(has_message_id, True) |
446 | |
447 | def test_hasPluralMessage(self): |
448 | - naked_potemplate = removeSecurityProxy(self.devel_potemplate) |
449 | - |
450 | # At the moment, a POTemplate has no plural form messages. |
451 | self.assertEquals(self.devel_potemplate.hasPluralMessage(), False) |
452 | |
453 | @@ -177,5 +177,465 @@ |
454 | self.assertEquals(potmsgset, shared_potmsgset) |
455 | |
456 | |
457 | +class TestSharingPOTemplatesByRegex(TestCaseWithFactory): |
458 | + """Isolate tests for regular expression use in SharingSubset.""" |
459 | + |
460 | + layer = ZopelessDatabaseLayer |
461 | + |
462 | + def setUp(self): |
463 | + super(TestSharingPOTemplatesByRegex, self).setUp() |
464 | + |
465 | + def _makeAndFind(self, names, name_pattern=None): |
466 | + product = self.factory.makeProduct() |
467 | + product.official_rosetta = True |
468 | + trunk = product.getSeries('trunk') |
469 | + for name in names: |
470 | + self.factory.makePOTemplate(productseries=trunk, name=name) |
471 | + subset = getUtility(IPOTemplateSet).getSharingSubset(product=product) |
472 | + return [ |
473 | + template.name |
474 | + for template in subset.getSharingPOTemplatesByRegex(name_pattern)] |
475 | + |
476 | + def test_getSharingPOTemplatesByRegex_baseline(self): |
477 | + # Baseline test. |
478 | + self.assertContentEqual( |
479 | + ['foo', 'foo-bar', 'foo-two'], |
480 | + self._makeAndFind(['foo', 'foo-bar', 'foo-two'], 'foo.*')) |
481 | + |
482 | + def test_getSharingPOTemplatesByRegex_not_all(self): |
483 | + # A template may not match. |
484 | + self.assertContentEqual( |
485 | + ['foo-bar', 'foo-two'], |
486 | + self._makeAndFind(['foo', 'foo-bar', 'foo-two'], 'foo-.*')) |
487 | + |
488 | + def test_getSharingPOTemplatesByRegex_all(self): |
489 | + # Not passing a pattern returns all templates. |
490 | + self.assertContentEqual( |
491 | + ['foo', 'foo-bar', 'foo-two'], |
492 | + self._makeAndFind(['foo', 'foo-bar', 'foo-two'])) |
493 | + |
494 | + def test_getSharingPOTemplatesByRegex_no_match(self): |
495 | + # A not matching pattern returns no templates. |
496 | + self.assertContentEqual( |
497 | + [], |
498 | + self._makeAndFind(['foo', 'foo-bar', 'foo-two'], "doo.+dle")) |
499 | + |
500 | + def test_getSharingPOTemplatesByRegex_robustness_single_quotes(self): |
501 | + # Single quotes do not confuse the regex match. |
502 | + self.assertContentEqual( |
503 | + [], |
504 | + self._makeAndFind(['foo', 'foo-bar', 'foo-two'], "'")) |
505 | + |
506 | + def test_getSharingPOTemplatesByRegex_robustness_double_quotes(self): |
507 | + # Double quotes do not confuse the regex match. |
508 | + self.assertContentEqual( |
509 | + [], |
510 | + self._makeAndFind(['foo', 'foo-bar', 'foo-two'], '"')) |
511 | + |
512 | + def test_getSharingPOTemplatesByRegex_robustness_backslash(self): |
513 | + # A backslash at the end could escape enclosing quotes without |
514 | + # proper escaping, leading to a SyntaxError or even a successful |
515 | + # exploit. Instead, storm should complain about an invalid expression |
516 | + # by raising DataError. |
517 | + product = self.factory.makeProduct() |
518 | + subset = getUtility(IPOTemplateSet).getSharingSubset(product=product) |
519 | + self.assertRaises( |
520 | + DataError, list, subset.getSharingPOTemplatesByRegex("foo.*\\")) |
521 | + |
522 | + |
523 | +class TestMessageSharingProductPackage(TestCaseWithFactory): |
524 | + """Test message sharing between a product and a package. |
525 | + |
526 | + Each test uses assertStatementCount to make sure the number of SQL |
527 | + queries does not change. This was integrated here to avoid having |
528 | + a second test case just for statement counts. |
529 | + The current implementation is good and only needs one statement. |
530 | + """ |
531 | + |
532 | + layer = ZopelessDatabaseLayer |
533 | + |
534 | + def setUp(self): |
535 | + super(TestMessageSharingProductPackage, self).setUp() |
536 | + |
537 | + self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
538 | + self.hoary = self.ubuntu['hoary'] |
539 | + self.warty = self.ubuntu['warty'] |
540 | + self.ubuntu.translation_focus = self.hoary |
541 | + self.packagename = self.factory.makeSourcePackageName() |
542 | + |
543 | + self.product = self.factory.makeProduct() |
544 | + self.trunk = self.product.getSeries('trunk') |
545 | + self.stable = self.factory.makeProductSeries( |
546 | + product=self.product) |
547 | + |
548 | + self.templatename = self.factory.getUniqueString() |
549 | + self.trunk_template = self.factory.makePOTemplate( |
550 | + productseries=self.trunk, name=self.templatename) |
551 | + self.hoary_template = self.factory.makePOTemplate( |
552 | + distroseries=self.hoary, sourcepackagename=self.packagename, |
553 | + name=self.templatename) |
554 | + |
555 | + self.owner = self.factory.makePerson() |
556 | + self.potemplateset = getUtility(IPOTemplateSet) |
557 | + |
558 | + def _assertStatements(self, no_of_statements, resultset): |
559 | + """Assert constant number of SQL statements when iterating result set. |
560 | + |
561 | + This encapsulates using the 'list' function to feed the iterator to |
562 | + the assert method. This iterates the resultset, triggering SQL |
563 | + statement execution.""" |
564 | + return self.assertStatementCount(no_of_statements, list, resultset) |
565 | + |
566 | + def test_getSharingPOTemplates_product(self): |
567 | + # Sharing templates for a product include the same templates from |
568 | + # a linked source package. |
569 | + self.factory.makeSourcePackagePublishingHistory( |
570 | + sourcepackagename=self.packagename, |
571 | + distroseries=self.hoary) |
572 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
573 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
574 | + templates = self._assertStatements( |
575 | + 1, subset.getSharingPOTemplates(self.templatename)) |
576 | + |
577 | + self.assertContentEqual( |
578 | + [self.trunk_template, self.hoary_template], templates) |
579 | + |
580 | + def test_getSharingPOTemplates_package(self): |
581 | + # Sharing templates for a source package include the same templates |
582 | + # from a linked product. |
583 | + sourcepackage = self.factory.makeSourcePackage( |
584 | + self.packagename, self.hoary) |
585 | + sourcepackage.setPackaging(self.trunk, self.owner) |
586 | + subset = self.potemplateset.getSharingSubset( |
587 | + distribution=self.ubuntu, |
588 | + sourcepackagename=self.packagename) |
589 | + templates = self._assertStatements( |
590 | + 1, subset.getSharingPOTemplates(self.templatename)) |
591 | + |
592 | + self.assertContentEqual( |
593 | + [self.trunk_template, self.hoary_template], templates) |
594 | + |
595 | + def test_getSharingPOTemplates_product_multiple_series(self): |
596 | + # Sharing templates for a product include the same templates from |
597 | + # a linked source package, even from multiple product series. |
598 | + # But templates for the same sourcepackagename are not returned |
599 | + # if they are not linked. |
600 | + stable_template = self.factory.makePOTemplate( |
601 | + productseries=self.stable, name=self.templatename) |
602 | + # This will not be returned. |
603 | + self.factory.makePOTemplate( |
604 | + distroseries=self.warty, sourcepackagename=self.packagename, |
605 | + name=self.templatename) |
606 | + self.factory.makeSourcePackagePublishingHistory( |
607 | + sourcepackagename=self.packagename, |
608 | + distroseries=self.hoary) |
609 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
610 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
611 | + templates = self._assertStatements( |
612 | + 1, subset.getSharingPOTemplates(self.templatename)) |
613 | + |
614 | + self.assertContentEqual( |
615 | + [self.trunk_template, self.hoary_template, stable_template], |
616 | + templates) |
617 | + |
618 | + def test_getSharingPOTemplates_package_multiple_series(self): |
619 | + # Sharing templates for a source package include the same templates |
620 | + # from a linked product, even with multiple product series. |
621 | + # To include templates from other source packages, the product must |
622 | + # be linked to that one, too. |
623 | + stable_template = self.factory.makePOTemplate( |
624 | + productseries=self.stable, name=self.templatename) |
625 | + warty_template = self.factory.makePOTemplate( |
626 | + distroseries=self.warty, sourcepackagename=self.packagename, |
627 | + name=self.templatename) |
628 | + hoary_sourcepackage = self.factory.makeSourcePackage( |
629 | + self.packagename, self.hoary) |
630 | + hoary_sourcepackage.setPackaging(self.trunk, self.owner) |
631 | + warty_sourcepackage = self.factory.makeSourcePackage( |
632 | + self.packagename, self.warty) |
633 | + warty_sourcepackage.setPackaging(self.stable, self.owner) |
634 | + subset = self.potemplateset.getSharingSubset( |
635 | + distribution=self.ubuntu, |
636 | + sourcepackagename=self.packagename) |
637 | + templates = self._assertStatements( |
638 | + 1, subset.getSharingPOTemplates(self.templatename)) |
639 | + |
640 | + self.assertContentEqual( |
641 | + [self.trunk_template, self.hoary_template, |
642 | + stable_template, warty_template], |
643 | + templates) |
644 | + |
645 | + def test_getSharingPOTemplates_package_name_changed(self): |
646 | + # When the name of a package changes (but not the name of the |
647 | + # template), it will still share translations if it is linked |
648 | + # to the same product. |
649 | + changed_name = self.factory.makeSourcePackageName() |
650 | + warty_template = self.factory.makePOTemplate( |
651 | + distroseries=self.warty, sourcepackagename=changed_name, |
652 | + name=self.templatename) |
653 | + hoary_sourcepackage = self.factory.makeSourcePackage( |
654 | + self.packagename, self.hoary) |
655 | + hoary_sourcepackage.setPackaging(self.trunk, self.owner) |
656 | + warty_sourcepackage = self.factory.makeSourcePackage( |
657 | + changed_name, self.warty) |
658 | + warty_sourcepackage.setPackaging(self.stable, self.owner) |
659 | + subset = self.potemplateset.getSharingSubset( |
660 | + distribution=self.ubuntu, |
661 | + sourcepackagename=self.packagename) |
662 | + templates = self._assertStatements( |
663 | + 1, subset.getSharingPOTemplates(self.templatename)) |
664 | + |
665 | + self.assertContentEqual( |
666 | + [self.trunk_template, self.hoary_template, warty_template], |
667 | + templates) |
668 | + |
669 | + def test_getSharingPOTemplates_many_series(self): |
670 | + # The number of queries for a call to getSharingPOTemplates must |
671 | + # remain constant. |
672 | + |
673 | + all_templates = [self.trunk_template, self.hoary_template] |
674 | + hoary_sourcepackage = self.factory.makeSourcePackage( |
675 | + self.packagename, self.hoary) |
676 | + hoary_sourcepackage.setPackaging(self.trunk, self.owner) |
677 | + # Add a greater number of series and sharing templates on either side. |
678 | + seriesnames = ( |
679 | + ('0.1', 'feisty'), |
680 | + ('0.2', 'gutsy'), |
681 | + ('0.3', 'hardy'), |
682 | + ('0.4', 'intrepid'), |
683 | + ('0.5', 'jaunty'), |
684 | + ('0.6', 'karmic'), |
685 | + ) |
686 | + for pseries_name, dseries_name in seriesnames: |
687 | + productseries = self.factory.makeProductSeries( |
688 | + self.product, pseries_name) |
689 | + all_templates.append(self.factory.makePOTemplate( |
690 | + productseries=productseries, name=self.templatename)) |
691 | + distroseries = self.factory.makeDistroSeries( |
692 | + self.ubuntu, name=dseries_name) |
693 | + all_templates.append(self.factory.makePOTemplate( |
694 | + distroseries=distroseries, sourcepackagename=self.packagename, |
695 | + name=self.templatename)) |
696 | + sourcepackage = self.factory.makeSourcePackage( |
697 | + self.packagename, distroseries) |
698 | + sourcepackage.setPackaging(productseries, self.owner) |
699 | + # Don't forget warty and stable. |
700 | + all_templates.append(self.factory.makePOTemplate( |
701 | + productseries=self.stable, name=self.templatename)) |
702 | + all_templates.append(self.factory.makePOTemplate( |
703 | + distroseries=self.warty, sourcepackagename=self.packagename, |
704 | + name=self.templatename)) |
705 | + warty_sourcepackage = self.factory.makeSourcePackage( |
706 | + self.packagename, self.warty) |
707 | + warty_sourcepackage.setPackaging(self.stable, self.owner) |
708 | + |
709 | + # Looking from the product side. |
710 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
711 | + templates = self._assertStatements( |
712 | + 1, subset.getSharingPOTemplates(self.templatename)) |
713 | + self.assertContentEqual(all_templates, templates) |
714 | + |
715 | + # Looking from the sourcepackage side. |
716 | + subset = self.potemplateset.getSharingSubset( |
717 | + distribution=self.ubuntu, |
718 | + sourcepackagename=self.packagename) |
719 | + templates = self._assertStatements( |
720 | + 1, subset.getSharingPOTemplates(self.templatename)) |
721 | + self.assertContentEqual(all_templates, templates) |
722 | + |
723 | + def test_getSharingPOTemplates_product_unrelated_templates(self): |
724 | + # Sharing templates for a product must not include other templates |
725 | + # from a linked source package. |
726 | + self.factory.makePOTemplate( |
727 | + distroseries=self.hoary, sourcepackagename=self.packagename, |
728 | + name=self.factory.getUniqueString()) |
729 | + self.factory.makePOTemplate( |
730 | + distroseries=self.warty, sourcepackagename=self.packagename, |
731 | + name=self.factory.getUniqueString()) |
732 | + self.factory.makeSourcePackagePublishingHistory( |
733 | + sourcepackagename=self.packagename, |
734 | + distroseries=self.hoary) |
735 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
736 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
737 | + templates = self._assertStatements( |
738 | + 1, subset.getSharingPOTemplates(self.templatename)) |
739 | + |
740 | + self.assertContentEqual( |
741 | + [self.trunk_template, self.hoary_template], |
742 | + templates) |
743 | + |
744 | + def test_getSharingPOTemplates_product_different_names_and_series(self): |
745 | + # A product may be packaged into differently named packages in |
746 | + # different distroseries. |
747 | + warty_packagename = self.factory.makeSourcePackageName() |
748 | + warty_template = self.factory.makePOTemplate( |
749 | + distroseries=self.warty, sourcepackagename=warty_packagename, |
750 | + name=self.templatename) |
751 | + self.factory.makeSourcePackagePublishingHistory( |
752 | + sourcepackagename=self.packagename, |
753 | + distroseries=self.hoary) |
754 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
755 | + self.factory.makeSourcePackagePublishingHistory( |
756 | + sourcepackagename=warty_packagename, |
757 | + distroseries=self.warty) |
758 | + self.trunk.setPackaging(self.warty, warty_packagename, self.owner) |
759 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
760 | + templates = self._assertStatements( |
761 | + 1, subset.getSharingPOTemplates(self.templatename)) |
762 | + |
763 | + self.assertContentEqual( |
764 | + [self.trunk_template, self.hoary_template, warty_template], |
765 | + templates) |
766 | + |
767 | + def test_getSharingPOTemplates_product_different_names_same_series(self): |
768 | + # A product may be packaged into differently named packages even in |
769 | + # the same distroseries. Must use different product series, though. |
770 | + other_packagename = self.factory.makeSourcePackageName() |
771 | + other_template = self.factory.makePOTemplate( |
772 | + distroseries=self.hoary, sourcepackagename=other_packagename, |
773 | + name=self.templatename) |
774 | + self.factory.makeSourcePackagePublishingHistory( |
775 | + sourcepackagename=self.packagename, |
776 | + distroseries=self.hoary) |
777 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
778 | + self.factory.makeSourcePackagePublishingHistory( |
779 | + sourcepackagename=other_packagename, |
780 | + distroseries=self.hoary) |
781 | + self.stable.setPackaging(self.hoary, other_packagename, self.owner) |
782 | + subset = self.potemplateset.getSharingSubset(product=self.product) |
783 | + templates = self._assertStatements( |
784 | + 1, subset.getSharingPOTemplates(self.templatename)) |
785 | + |
786 | + self.assertContentEqual( |
787 | + [self.trunk_template, self.hoary_template, other_template], |
788 | + templates) |
789 | + |
790 | + def test_getSharingPOTemplates_package_unrelated_template(self): |
791 | + # Sharing templates for a source package must not include other |
792 | + # templates from a linked product. |
793 | + self.factory.makePOTemplate( |
794 | + productseries=self.trunk, name=self.factory.getUniqueString()) |
795 | + self.factory.makePOTemplate( |
796 | + productseries=self.stable, name=self.factory.getUniqueString()) |
797 | + sourcepackage = self.factory.makeSourcePackage( |
798 | + self.packagename, self.hoary) |
799 | + sourcepackage.setPackaging(self.trunk, self.owner) |
800 | + subset = self.potemplateset.getSharingSubset( |
801 | + distribution=self.ubuntu, |
802 | + sourcepackagename=self.packagename) |
803 | + templates = self._assertStatements( |
804 | + 1, subset.getSharingPOTemplates(self.templatename)) |
805 | + |
806 | + self.assertContentEqual( |
807 | + [self.trunk_template, self.hoary_template], |
808 | + templates) |
809 | + |
810 | + def test_getSharingPOTemplates_package_unrelated_template_linked(self): |
811 | + # Sharing templates for a source package must not include templates |
812 | + # from sourcepackages of the same name that are linked to a different |
813 | + # product. |
814 | + other_productseries = self.factory.makeProductSeries() |
815 | + other_sourcepackage = self.factory.makeSourcePackage( |
816 | + self.packagename, self.warty) |
817 | + other_sourcepackage.setPackaging(other_productseries, self.owner) |
818 | + other_template = self.factory.makePOTemplate( |
819 | + productseries=other_productseries, name=self.templatename) |
820 | + |
821 | + sourcepackage = self.factory.makeSourcePackage( |
822 | + self.packagename, self.hoary) |
823 | + sourcepackage.setPackaging(self.trunk, self.owner) |
824 | + subset = self.potemplateset.getSharingSubset( |
825 | + distribution=self.ubuntu, |
826 | + sourcepackagename=self.packagename) |
827 | + templates = self._assertStatements( |
828 | + 1, subset.getSharingPOTemplates(self.templatename)) |
829 | + |
830 | + self.assertContentEqual( |
831 | + [self.trunk_template, self.hoary_template], templates) |
832 | + |
833 | + # The behavior is controlled by the translation focus of the |
834 | + # distribution. The series in focus will be selected. |
835 | + self.ubuntu.translation_focus = self.warty |
836 | + subset = self.potemplateset.getSharingSubset( |
837 | + distribution=self.ubuntu, |
838 | + sourcepackagename=self.packagename) |
839 | + templates = self._assertStatements( |
840 | + 1, subset.getSharingPOTemplates(self.templatename)) |
841 | + |
842 | + self.assertContentEqual([other_template], templates) |
843 | + |
844 | + def test_getSharingPOTemplates_package_only(self): |
845 | + # Sharing templates for a source package only, is done by the |
846 | + # sourcepackagename. |
847 | + warty_template = self.factory.makePOTemplate( |
848 | + distroseries=self.warty, sourcepackagename=self.packagename, |
849 | + name=self.templatename) |
850 | + other_series = self.factory.makeDistroSeries(self.ubuntu) |
851 | + other_template = self.factory.makePOTemplate( |
852 | + distroseries=other_series, sourcepackagename=self.packagename, |
853 | + name=self.templatename) |
854 | + subset = self.potemplateset.getSharingSubset( |
855 | + distribution=self.ubuntu, |
856 | + sourcepackagename=self.packagename) |
857 | + templates = self._assertStatements( |
858 | + 1, subset.getSharingPOTemplates(self.templatename)) |
859 | + |
860 | + self.assertContentEqual( |
861 | + [self.hoary_template, other_template, warty_template], templates) |
862 | + |
863 | + def test_getSharingPOTemplates_package_one_linked(self): |
864 | + # Once one a sourcepackage in a distroseries that is neither the |
865 | + # translation focus nor the current series is linked to a product, |
866 | + # no sharing by name is possible anymore. |
867 | + self.factory.makePOTemplate( |
868 | + distroseries=self.warty, sourcepackagename=self.packagename, |
869 | + name=self.templatename) |
870 | + other_series = self.factory.makeDistroSeries(self.ubuntu) |
871 | + self.factory.makePOTemplate( |
872 | + distroseries=other_series, sourcepackagename=self.packagename, |
873 | + name=self.templatename) |
874 | + other_sourcepackage = self.factory.makeSourcePackage( |
875 | + self.packagename, other_series) |
876 | + other_sourcepackage.setPackaging(self.trunk, self.owner) |
877 | + |
878 | + subset = self.potemplateset.getSharingSubset( |
879 | + distribution=self.ubuntu, |
880 | + sourcepackagename=self.packagename) |
881 | + templates = self._assertStatements( |
882 | + 0, subset.getSharingPOTemplates(self.templatename)) |
883 | + |
884 | + self.assertEqual([], templates) |
885 | + |
886 | + def test_getOrCreateSharedPOTMsgSet_product(self): |
887 | + # Trying to create an identical POTMsgSet in a product as exists |
888 | + # in a linked sourcepackage will return the existing POTMsgset. |
889 | + self.factory.makeSourcePackagePublishingHistory( |
890 | + sourcepackagename=self.packagename, |
891 | + distroseries=self.hoary) |
892 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
893 | + hoary_potmsgset = self.factory.makePOTMsgSet( |
894 | + potemplate=self.hoary_template, sequence=1) |
895 | + |
896 | + trunk_potmsgset = self.trunk_template.getOrCreateSharedPOTMsgSet( |
897 | + singular_text=hoary_potmsgset.singular_text, |
898 | + plural_text=hoary_potmsgset.plural_text) |
899 | + self.assertEqual(hoary_potmsgset, trunk_potmsgset) |
900 | + |
901 | + def test_getOrCreateSharedPOTMsgSet_package(self): |
902 | + # Trying to create an identical POTMsgSet in a product as exists |
903 | + # in a linked sourcepackage will return the existing POTMsgset. |
904 | + self.factory.makeSourcePackagePublishingHistory( |
905 | + sourcepackagename=self.packagename, |
906 | + distroseries=self.hoary) |
907 | + self.trunk.setPackaging(self.hoary, self.packagename, self.owner) |
908 | + hoary_potmsgset = self.factory.makePOTMsgSet( |
909 | + potemplate=self.hoary_template, sequence=1) |
910 | + |
911 | + trunk_potmsgset = self.trunk_template.getOrCreateSharedPOTMsgSet( |
912 | + singular_text=hoary_potmsgset.singular_text, |
913 | + plural_text=hoary_potmsgset.plural_text) |
914 | + self.assertEqual(hoary_potmsgset, trunk_potmsgset) |
915 | + |
916 | + |
917 | def test_suite(): |
918 | return unittest.TestLoader().loadTestsFromName(__name__) |
Seeing how many tests were affected, I'd suggest the following test command which tests all translation tests:
bin/test -vvt lp.translations