~adeuring/charmworld/spurious-failures

169.2.3 by Aaron Bentley
Implement indexing in terms of elasticsearch.
1
# Copyright 2013 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
__metaclass__ = type
5
6
197.1.6 by Aaron Bentley
Sort by date_created works.
7
from datetime import datetime
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
8
from textwrap import dedent
197.1.6 by Aaron Bentley
Sort by date_created works.
9
258.3.8 by Aaron Bentley
search can raise SearchServiceNotAvailable.
10
from pyelasticsearch import ElasticSearch
251.2.1 by Abel Deuring
sanitize full text search strings to avoid parsing errors
11
from pyelasticsearch.exceptions import (
12
    ElasticHttpError,
13
    ElasticHttpNotFoundError,
14
)
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
15
import yaml
169.2.3 by Aaron Bentley
Implement indexing in terms of elasticsearch.
16
201.1.1 by Aaron Bentley
Extract store url generation.
17
from charmworld.charmstore import (
18
    get_address,
19
    make_store_url,
20
)
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
21
from charmworld.models import (
325.1.4 by Brad Crittenden
Fix lint
22
    Bundle,
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
23
    Charm,
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
24
    store_bundles,
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
25
)
177.1.3 by Aaron Bentley
Implement boosting.
26
from charmworld.search import (
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
27
    BUNDLE,
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
28
    CHARM,
221.2.1 by Aaron Bentley
Add ability to manipulate aliases.
29
    ElasticSearchClient,
344.3.1 by Brad Crittenden
Search bundle's data field.
30
    charm_exact_fields,
31
    charm_free_text_fields,
221.2.3 by Aaron Bentley
Ensure unique failure for incompatible mapping.
32
    IncompatibleMapping,
221.3.3 by Aaron Bentley
put_mapping raises IndexMissing as needed.
33
    IndexMissing,
258.3.1 by Aaron Bentley
Web site search raises IndexNotReady if document info is unavailable.
34
    IndexNotReady,
182.2.18 by Aaron Bentley
Get api_search fully under test.
35
    InvalidCharmType,
221.2.12 by Aaron Bentley
Rename migrate to update.
36
    update,
182.2.18 by Aaron Bentley
Get api_search fully under test.
37
    NegativeLimit,
221.2.5 by Aaron Bentley
Implement reindex and migrate.
38
    reindex,
258.3.8 by Aaron Bentley
search can raise SearchServiceNotAvailable.
39
    SearchServiceNotAvailable,
177.1.6 by Aaron Bentley
Cleanup.
40
)
179.3.1 by Aaron Bentley
Remove all mention of Xapian.
41
from charmworld.testing import (
42
    factory,
43
    temp_index_client,
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
44
    TestCase,
179.3.1 by Aaron Bentley
Remove all mention of Xapian.
45
)
221.2.5 by Aaron Bentley
Implement reindex and migrate.
46
from charmworld.utils import get_ini
169.2.3 by Aaron Bentley
Implement indexing in terms of elasticsearch.
47
48
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
49
class TestTempIndexClient(TestCase):
169.2.3 by Aaron Bentley
Implement indexing in terms of elasticsearch.
50
51
    def test_temp_index_client(self):
52
        with temp_index_client() as client:
53
            self.assertNotEqual('charms', client.index_name)
54
            client._client.update_settings(client.index_name, {})
55
        with self.assertRaises(ElasticHttpNotFoundError):
56
            client._client.update_settings(client.index_name, {})
57
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
58
59
class TestElasticSearchClient(TestCase):
60
61
    def setUp(self):
62
        super(TestElasticSearchClient, self).setUp()
63
        self.use_index_client()
64
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
65
    def exists_in_index(self, id_, kind=CHARM):
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
66
        client = self.index_client
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
67
        if kind == CHARM:
68
            return client.get(id_) is not None
169.2.3 by Aaron Bentley
Implement indexing in terms of elasticsearch.
69
        try:
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
70
            result = client._client.get(client.index_name, kind, id_)
169.2.3 by Aaron Bentley
Implement indexing in terms of elasticsearch.
71
        except ElasticHttpNotFoundError:
72
            return False
73
        return result['exists']
74
75
    def test_index_charm(self):
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
76
        foo_charm = factory.get_charm_json()
77
        self.assertFalse(self.exists_in_index(foo_charm['_id']))
78
        self.index_client.index_charm(foo_charm)
79
        self.assertTrue(self.exists_in_index(foo_charm['_id']))
80
258.3.14 by Aaron Bentley
Handle max retries exceeded in cstat.
81
    def test_index_charm_max_retries_exceeded(self):
82
        # The Gopher port should be inactive.
83
        client = ElasticSearchClient(
84
            ElasticSearch(['http://localhost:70']), 'foo')
85
        with self.assertRaises(SearchServiceNotAvailable):
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
86
            client.index_charm({'_id': 'id', 'owner': 'foo', 'series': 'bar',
87
                                'name': 'baz'})
258.3.14 by Aaron Bentley
Handle max retries exceeded in cstat.
88
230.1.1 by Aaron Bentley
Optimize reindexing.
89
    def test_index_charms(self):
90
        foo_charm = factory.get_charm_json()
91
        bar_charm = factory.get_charm_json()
92
        self.assertFalse(self.exists_in_index(foo_charm['_id']))
93
        self.assertFalse(self.exists_in_index(bar_charm['_id']))
94
        self.index_client.index_charms([foo_charm, bar_charm])
95
        self.assertTrue(self.exists_in_index(foo_charm['_id']))
96
        self.assertTrue(self.exists_in_index(bar_charm['_id']))
97
98
    def test_index_charms_no_charms(self):
99
        # Assert that no exception is raised.
100
        with self.assertRaises(AssertionError):
101
            with self.assertRaises(BaseException):
102
                self.index_client.index_charms([])
103
302.4.8 by Aaron Bentley
Add tests for delete_charm.
104
    def test_delete_charm(self):
105
        charm_data = factory.get_charm_json()
106
        self.index_client.index_charm(charm_data)
107
        self.assertTrue(self.exists_in_index(charm_data['_id']))
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
108
        self.index_client.delete_charm(charm_data)
302.4.8 by Aaron Bentley
Add tests for delete_charm.
109
        self.assertFalse(self.exists_in_index(charm_data['_id']))
110
288.6.7 by Aaron Bentley
Stop leaving pending aliases behind.
111
    def test_update_aliased_None(self):
112
        alias = ElasticSearchClient.from_settings(get_ini(), 'foo')
113
        alias.update_aliased(self.index_client.index_name, [])
114
        alias.update_aliased(None, [])
115
        self.assertEqual([self.index_client.index_name], alias.get_aliased())
116
        alias.update_aliased(None, [self.index_client.index_name])
117
        self.assertEqual([], alias.get_aliased())
118
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
119
    def makeCharm(self, *args, **kwargs):
182.2.18 by Aaron Bentley
Get api_search fully under test.
120
        charm = factory.get_charm_json(*args, **kwargs)
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
121
        self.index_client.index_charm(charm)
122
        return charm
169.2.7 by Aaron Bentley
Basic test of search functionality.
123
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
124
    def makeBundle(self, *args, **kwargs):
125
        bundle = factory.get_bundle_data(*args, **kwargs)
126
        bundle['_id'] = Bundle(bundle).id
127
        self.index_client.index_bundle(bundle)
128
        return bundle
129
169.2.7 by Aaron Bentley
Basic test of search functionality.
130
    def test_search(self):
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
131
        result_keys = ['results', 'search_time', 'result_total', 'matches',
169.2.16 by Aaron Bentley
Cleanup and test fixes.
132
                       'matches_human_readable_estimate']
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
133
        client = self.index_client
283.1.3 by Abel Deuring
ElasticSearchClient.search(): Return Charm objects instead of simple dictionaries.
134
        foo_charm = Charm(self.makeCharm())
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
135
        results = client.search('foo')
136
        self.assertItemsEqual(result_keys, results.keys())
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
137
        self.assertEqual([], results['results']['charm'])
138
        results = client.search(foo_charm.name)['results']['charm']
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
139
        self.assertEqual(1, len(results))
140
        self.assertEqual(foo_charm, results[0]['data'])
141
        # The absolute value of weight is not defined, so we just ensure
142
        # it's present.
143
        self.assertItemsEqual(['data', 'weight'], results[0].keys())
177.1.1 by Aaron Bentley
Ensure only specific fields are searched.
144
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
145
    def test_search_bundle(self):
146
        bundle = Bundle(self.makeBundle())
147
        client = self.index_client
344.3.1 by Brad Crittenden
Search bundle's data field.
148
        results = client.search(bundle.name)['results']
149
        self.assertEqual(1, len(results['bundle']))
150
        self.assertEqual(bundle, results['bundle'][0]['data'])
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
151
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
152
    def _setup_store_bundles(self):
337.1.4 by Brad Crittenden
Broken version where basket is stored with owner as a prefix.
153
        _id = '~abentley/wordpress-basket/5/wordpress-stage'
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
154
        deployer_config = dedent("""\
155
            wordpress-stage:
156
                series: precise
157
                services:
158
                    blog:
159
                        charm: cs:precise/wordpress
160
                        constraints: mem=2
161
                        options:
162
                            tuning: optimized
163
                            engine: apache
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
164
                    db:
165
                        charm: cs:precise/mysql
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
166
        """)
167
        parsed = yaml.safe_load(deployer_config)
337.1.4 by Brad Crittenden
Broken version where basket is stored with owner as a prefix.
168
        store_bundles(None, parsed, 'abentley', 'wordpress-basket/5',
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
169
                      index_client=self.index_client)
170
        return _id
171
172
    def test_store_bundles_bundle_name_indexed(self):
173
        # Ensure that bundles stored into the index via the `store_bundles`
174
        # function are searchable via the index.
368.1.3 by Brad Crittenden
De-lint
175
        _id = self._setup_store_bundles()
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
176
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
177
        # Bundle name is indexed.
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
178
        results = self.index_client.search(
179
            'wordpress-stage')['results']['bundle']
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
180
        self.assertEqual(1, len(results))
181
        self.assertEqual(_id, results[0]['data'].id)
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
182
183
    def test_store_bundles_series_indexed(self):
368.1.3 by Brad Crittenden
De-lint
184
        _id = self._setup_store_bundles()
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
185
        # Series is indexed.
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
186
        results = self.index_client.search('precise')['results']['bundle']
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
187
        self.assertEqual(1, len(results))
188
        self.assertEqual(_id, results[0]['data'].id)
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
189
368.1.3 by Brad Crittenden
De-lint
190
    def test_store_bundles_owner_indexed(self):
191
        _id = self._setup_store_bundles()
337.1.2 by Brad Crittenden
Update store bundles to include required data for indexing.
192
        # Owner is indexed.
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
193
        results = self.index_client.search('abentley')['results']['bundle']
194
        self.assertEqual(1, len(results))
195
        self.assertEqual(_id, results[0]['data'].id)
196
368.1.3 by Brad Crittenden
De-lint
197
    def test_store_bundles_charm_names_indexed(self):
198
        _id = self._setup_store_bundles()
367.1.1 by Brad Crittenden
Search for charm names within the services dict.
199
        # Charm names within services are index.
200
        results = self.index_client.search('mysql')['results']['bundle']
201
        self.assertEqual(1, len(results))
202
        self.assertEqual(_id, results[0]['data'].id)
203
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
204
    def test_search_charms_and_bundles_same_name(self):
205
        charm = Charm(self.makeCharm(name='mozilla'))
206
        bundle = Bundle(self.makeBundle(name='mozilla'))
207
        client = self.index_client
208
        search_results = client.search('mozilla')
209
        results = search_results['results']
210
        self.assertEqual(2, len(results))
211
        self.assertEqual(2, search_results['result_total'])
212
        self.assertEqual(2, search_results['matches'])
213
        self.assertEqual(1, len(results['charm']))
214
        self.assertEqual(1, len(results['bundle']))
215
        self.assertEqual(charm, results['charm'][0]['data'])
216
        self.assertEqual(bundle, results['bundle'][0]['data'])
217
218
    def test_search_charms_and_bundles_description(self):
219
        charm = Charm(self.makeCharm(description='a mozilla charm'))
220
        bundle = Bundle(self.makeBundle(description='a mozilla bundle'))
221
        client = self.index_client
222
        search_results = client.search('mozilla')
223
        results = search_results['results']
224
        self.assertEqual(2, len(results))
225
        self.assertEqual(2, search_results['result_total'])
226
        self.assertEqual(2, search_results['matches'])
227
        self.assertEqual(1, len(results['charm']))
228
        self.assertEqual(1, len(results['bundle']))
229
        self.assertEqual(charm, results['charm'][0]['data'])
230
        self.assertEqual(bundle, results['bundle'][0]['data'])
231
258.3.1 by Aaron Bentley
Web site search raises IndexNotReady if document info is unavailable.
232
    def test_search_not_ready(self):
233
234
        class FakeElasticSearch:
235
236
            @staticmethod
237
            def status(index):
238
                return {'indices': {'foo': {}}}
239
240
            @staticmethod
241
            def aliases():
242
                return {}
243
244
        client = ElasticSearchClient(FakeElasticSearch, 'foo')
245
        with self.assertRaises(IndexNotReady):
246
            client.search('bar')
247
258.3.8 by Aaron Bentley
search can raise SearchServiceNotAvailable.
248
    def test_max_retries_exceeded_search(self):
249
        # The Gopher port should be inactive.
250
        client = ElasticSearchClient(
251
            ElasticSearch(['http://localhost:70']), 'foo')
252
        with self.assertRaises(SearchServiceNotAvailable):
253
            client.search('bar')
254
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
255
    def search_ids(self, terms):
256
        client = self.index_client
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
257
        hits = client.search(terms)['results']['charm']
258
        return [hit['data']._id for hit in hits]
177.1.1 by Aaron Bentley
Ensure only specific fields are searched.
259
260
    def test_search_matches_on_search_terms(self):
344.3.1 by Brad Crittenden
Search bundle's data field.
261
        fields = charm_free_text_fields.keys() + charm_exact_fields
177.1.4 by Aaron Bentley
All desired fields are searchable and tested.
262
        fields = [field.split('.')[0] for field in fields]
221.4.5 by Aaron Bentley
Fix failing test.
263
        charm = factory.get_charm_json(categories=['unknown'])
177.1.4 by Aaron Bentley
All desired fields are searchable and tested.
264
        # Ensure all fields have unique values.
340.1.1 by Abel Deuring
do not try to use non-existent value charm['store_data']['address'] in templates; property Charm.address added.
265
        charm['address'] = 'cs:bogus-address'
177.1.1 by Aaron Bentley
Ensure only specific fields are searched.
266
        charm['owner'] = 'steve'
267
        charm['series'] = 'grumpy'
268
        charm['name'] = 'arthur'
269
        charm['label'] = 'organic'
270
        charm['bname'] = 'my-dev-branch'
177.1.3 by Aaron Bentley
Implement boosting.
271
        charm['i_provides'] = ['provisions']
177.1.4 by Aaron Bentley
All desired fields are searchable and tested.
272
        charm['provides']['website']['interface'] = 'gopher'
273
        charm['requires']['database']['interface'] = 'reiserfs'
177.1.1 by Aaron Bentley
Ensure only specific fields are searched.
274
        charm['_id'] = 'asdf'
275
        # Best-effort ensure there's no overlap between search terms
276
        items = charm.items()
277
        for num, (key, value) in enumerate(items):
278
            if not isinstance(value, basestring):
279
                continue
280
            for key2, value2 in items[num + 1:]:
281
                if not isinstance(value2, basestring):
282
                    continue
283
                self.assertNotIn(value, value2, '%s in %s' % (key, key2))
284
                self.assertNotIn(value2, value, '%s in %s' % (key2, key))
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
285
        self.index_client.index_charm(charm)
286
        for key, query in items:
225.2.1 by Abel Deuring
JenkinsIngestJob.run(): Better check if a charm is promulgated.
287
            # Skip ints and lists.
288.2.1 by Rick Harding
It's alive and tests pass
288
            if key in ('revision', 'downloads_in_past_30_days', 'downloads'):
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
289
                continue
221.4.5 by Aaron Bentley
Fix failing test.
290
            elif key in ('hooks', 'i_requires', 'i_provides', 'categories'):
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
291
                query = query[0]
292
            elif key == 'changes':
293
                query = query[0]['message']
294
            elif key in ('provides', 'requires'):
295
                query = query.values()[0].values()[0]
296
            elif key == 'config':
284.1.3 by Aaron Bentley
Start using new internal format for options.
297
                query = query.values()[0][0]['description']
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
298
            elif key == 'store_data':
299
                query = query['digest']
300
            elif key == 'files':
301
                query = query['readme']['filename']
302
            elif key in ('first_change', 'last_change'):
303
                query = query['message']
304
            elif key == 'proof':
305
                query = query['w'][0]
197.1.14 by Aaron Bentley
Fix failing tests.
306
            self.assertIsInstance(query, (basestring, bool))
307
            if isinstance(query, basestring):
308
                query = '"%s"' % self.index_client.escape_query(query)
309
            else:
310
                self.assertIsInstance(query, bool)
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
311
            ids = self.search_ids(query)
312
            if key in fields:
313
                self.assertEqual(1, len(ids), 'No hit for %s' % key)
314
            else:
315
                self.assertEqual(0, len(ids), 'Undesired hit on %s' % key)
179.1.1 by Aaron Bentley
Boost official charms.
316
317
    def test_official_charms_boost(self):
318
        """An official version of a charm rates 10x higher."""
225.2.18 by Abel Deuring
ElasticSearch queries: Do not assuma that charmers are the only owners of promulgated charms.
319
        charm = factory.get_charm_json(promulgated=True, owner='foo')
179.1.1 by Aaron Bentley
Boost official charms.
320
        official_id = charm['_id']
321
        unofficial_id = official_id + '-1'
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
322
        client = self.index_client
323
        client.index_charm(charm)
324
        charm['owner'] = 'jrandom'
325
        charm['_id'] = unofficial_id
225.2.18 by Abel Deuring
ElasticSearch queries: Do not assuma that charmers are the only owners of promulgated charms.
326
        charm['store_url'] = get_address(charm, short=False)
182.2.17 by Aaron Bentley
Revamp TestElasticSearchClient to use use_index_client()
327
        client.index_charm(charm)
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
328
        results = client.search('terminal')['results']['charm']
283.1.3 by Abel Deuring
ElasticSearchClient.search(): Return Charm objects instead of simple dictionaries.
329
        by_id = dict((result['data']._id, result) for result in results)
179.1.1 by Aaron Bentley
Boost official charms.
330
        self.assertAlmostEqual(by_id[official_id]['weight'],
331
                               by_id[unofficial_id]['weight'] * 10)
182.2.18 by Aaron Bentley
Get api_search fully under test.
332
201.1.3 by Aaron Bentley
Unpromulgated charms are not boosted.
333
    def test_unreviewed_charmers_not_boosted(self):
334
        """Unpromulgated charms are not boosted even if owned by charmers."""
335
        charm = factory.get_charm_json(promulgated=False)
336
        charmers_id = charm['_id']
337
        jrandom_id = charmers_id + '-1'
338
        client = self.index_client
339
        client.index_charm(charm)
340
        charm['owner'] = 'jrandom'
341
        charm['_id'] = jrandom_id
342
        client.index_charm(charm)
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
343
        results = client.search('terminal')['results']['charm']
283.1.3 by Abel Deuring
ElasticSearchClient.search(): Return Charm objects instead of simple dictionaries.
344
        by_id = dict((result['data']._id, result) for result in results)
201.1.3 by Aaron Bentley
Unpromulgated charms are not boosted.
345
        self.assertAlmostEqual(by_id[charmers_id]['weight'],
346
                               by_id[jrandom_id]['weight'])
347
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
348
    def get_charm_bundle_test_data(self):
225.2.2 by Abel Deuring
factory.get_charm_json(): Proper handling of the promulgated parameter.
349
        one = self.makeCharm(name='name1', promulgated=True)
182.2.18 by Aaron Bentley
Get api_search fully under test.
350
        maintainer2 = 'jrandom@example.com (J. Random Hacker)'
351
        provides2 = {'cookie-factory': {
352
            'interface': 'imap',
353
        }}
354
        requires2 = {'postal-service': {
355
            'interface': 'envelope',
356
        }}
357
        options2 = {
358
            'foobar': {
359
                'type': 'str',
360
                'default': 'baz',
361
                'description': 'quxx',
362
            },
363
        }
364
        files2 = {
365
            'readme': {
366
                'filename': 'README.md',
367
                'subdir': '',
368
            },
369
            'config-changed': {
370
                'filename': 'config-changed',
371
                'subdir': 'hooks',
372
            },
373
        }
374
        two = self.makeCharm(owner='jrandom', revision=3,
375
                             series='warty', summary='Charm two',
376
                             name='name2', revno=13,
377
                             maintainer=maintainer2,
378
                             commit_message='message2',
379
                             provides=provides2,
380
                             requires=requires2, options=options2,
381
                             files=files2, subordinate=True)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
382
        bundle = self.makeBundle(name='name3')
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
383
        return one, two, bundle
384
385
    def test_api_search_all(self):
362.1.2 by Curtis Hovey
Always include the doctype with api_search results when the doctype is not specified.
386
        # With no parameters, all charms and bundles are returned.
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
387
        one, two, bundle = self.get_charm_bundle_test_data()
362.1.6 by Curtis Hovey
DRY tests.
388
        result = self.get_charms_and_bundles(doctype=None)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
389
        self.assertEqual(3, len(result))
201.1.1 by Aaron Bentley
Extract store url generation.
390
        one_url = make_store_url(1, get_address(one, short=True))
182.2.19 by Aaron Bentley
Cleanup.
391
        self.assertEqual(one, result[0])
201.1.1 by Aaron Bentley
Extract store url generation.
392
        two_url = make_store_url(1, get_address(two, short=False))
182.2.19 by Aaron Bentley
Cleanup.
393
        self.assertEqual(two, result[1])
182.2.18 by Aaron Bentley
Get api_search fully under test.
394
        self.assertNotEqual(one['_id'], two['_id'])
395
        self.assertNotEqual(one_url, two_url)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
396
        self.assertEqual(bundle, result[2])
182.2.18 by Aaron Bentley
Get api_search fully under test.
397
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
398
    def test_api_search_charms_only(self):
399
        """With no parameters, all charms are returned."""
400
        one, two, bundle = self.get_charm_bundle_test_data()
401
        result = self.get_charms_and_bundles(doctype=CHARM)
402
        self.assertEqual(2, len(result))
362.1.6 by Curtis Hovey
DRY tests.
403
        self.assertEqual(one, result[0])
404
        self.assertEqual(two, result[1])
362.1.7 by Curtis Hovey
DRY tests.
405
        self.assertNotEqual(one['_id'], two['_id'])
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
406
407
    def test_api_search_bundles_only(self):
332.1.2 by Brad Crittenden
Fix doctype cut-n-paste error.
408
        """Passing doctype of 'bundles' retrieves only bundles."""
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
409
        one, two, bundle = self.get_charm_bundle_test_data()
410
        result = self.get_charms_and_bundles(doctype=BUNDLE)
411
        self.assertEqual(1, len(result))
362.1.6 by Curtis Hovey
DRY tests.
412
        self.assertEqual(bundle, result[0])
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
413
298.1.1 by Aaron Bentley
Autocomplete flag switches to matching on names only.
414
    def test_autocomplete_matches_on_names(self):
415
        matching_charm = self.makeCharm(name='foo')
416
        differing_charm = self.makeCharm(name='bar')
344.3.1 by Brad Crittenden
Search bundle's data field.
417
        for field in charm_free_text_fields:
298.1.1 by Aaron Bentley
Autocomplete flag switches to matching on names only.
418
            if '.' in field or field == 'name':
419
                continue
420
            differing_charm[field] = 'foo'
344.3.1 by Brad Crittenden
Search bundle's data field.
421
        for field in charm_exact_fields:
298.1.1 by Aaron Bentley
Autocomplete flag switches to matching on names only.
422
            differing_charm[field] = 'foo'
423
        self.index_client.index_charm(differing_charm)
362.1.7 by Curtis Hovey
DRY tests.
424
        result = self.index_client.api_search('foo', autocomplete=True)
425
        ids = [r['data']['_id'] for r in result]
298.1.1 by Aaron Bentley
Autocomplete flag switches to matching on names only.
426
        self.assertEqual([matching_charm['_id']], ids)
427
298.1.2 by Aaron Bentley
Autocomplete does prefix matching.
428
    def test_autocomplete_matches_on_prefix(self):
429
        charm1 = self.makeCharm(name='foo')
430
        charm2 = self.makeCharm(name='foobar')
304.3.1 by Aaron Bentley
Fix lint.
431
        self.makeCharm(name='barfoo')
362.1.4 by Curtis Hovey
Always return the doctype with api_search.
432
        result = self.index_client.api_search('foo', autocomplete=True)
433
        ids = [r['data']['_id'] for r in result]
298.1.2 by Aaron Bentley
Autocomplete does prefix matching.
434
        self.assertItemsEqual([charm1['_id'], charm2['_id']], ids)
435
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
436
    def get_charms_and_bundles(self, text='', type_=None, doctype=CHARM,
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
437
                               *args, **kwargs):
182.2.18 by Aaron Bentley
Get api_search fully under test.
438
        filters = dict((key, [value]) for key, value in kwargs.items())
439
        if type_ is not None and isinstance(type_, basestring):
440
            type_ = [type_]
362.1.6 by Curtis Hovey
DRY tests.
441
        result = self.index_client.api_search(
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
442
            text, filters, type_, doctype=doctype)
362.1.6 by Curtis Hovey
DRY tests.
443
        charms_and_bundles = [r['data'] for r in result]
444
        return sorted(charms_and_bundles, key=lambda x: x['name'])
182.2.18 by Aaron Bentley
Get api_search fully under test.
445
446
    def _test_filter(self, filter_str, make_charm=None):
447
        if make_charm is None:
448
            def make_charm(value, warty=False):
449
                kwargs = {filter_str: value}
450
                if filter_str != 'series' and warty:
451
                    kwargs['series'] = 'warty'
452
                return self.makeCharm(**kwargs)
453
        one = make_charm('common')
454
        self.index_client.index_charm(one)
455
        two = make_charm('common', warty=True)
456
        self.index_client.index_charm(two)
457
        three = make_charm('different')
458
        self.index_client.index_charm(three)
459
        four = make_charm('different-again')
460
        self.index_client.index_charm(four)
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
461
        result = self.get_charms_and_bundles(**{filter_str: 'common'})
182.2.18 by Aaron Bentley
Get api_search fully under test.
462
        self.assertEqual(2, len(result))
463
        self.assertEqual(set([one['store_url'], two['store_url']]),
464
                         set(charm['store_url'] for charm in result))
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
465
        result = self.get_charms_and_bundles(**{filter_str: 'different'})
182.2.18 by Aaron Bentley
Get api_search fully under test.
466
        self.assertEqual(set([three['store_url']]),
467
                         set(charm['store_url'] for charm in result))
468
        self.assertEqual(1, len(result))
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
469
        result = self.get_charms_and_bundles(
470
            **{filter_str: ['different', 'common']})
182.2.18 by Aaron Bentley
Get api_search fully under test.
471
        self.assertEqual(3, len(result))
472
473
    def test_charms_name_filter(self):
474
        """Charms can be filtered by name."""
475
        self._test_filter('name')
476
477
    def test_charms_series_filter(self):
478
        """Charms can be filtered by series."""
479
        self._test_filter('series')
480
481
    def test_charms_owner_filter(self):
482
        """Charms can be filtered by owner."""
483
        self._test_filter('owner')
484
485
    def test_charms_provides_filter(self):
486
        """Charms can be filtered by provided interfaces."""
487
        def make_charm(value, warty=False):
488
            provides = {'foo': {'interface': value}}
489
            return self.makeCharm(provides=provides)
490
        self._test_filter('i_provides', make_charm)
491
492
    def test_charms_requires_filter(self):
493
        """Charms can be filtered by required interfaces."""
494
        def make_charm(value, warty=False):
495
            requires = {'foo': {'interface': value}}
496
            return self.makeCharm(requires=requires)
497
        self._test_filter('i_requires', make_charm)
498
499
    def test_charms_type_filter(self):
500
        """Filtering by type works as well as possible.
501
315.1.1 by Brad Crittenden
Add doctype to charms and bundles. Introduce API3, but don't use it.
502
        Charms are considered 'approved' if they are promulgated,
225.2.18 by Abel Deuring
ElasticSearch queries: Do not assuma that charmers are the only owners of promulgated charms.
503
        'community' otherwise.  'environment' causes no charms
201.1.5 by Aaron Bentley
Update API to consider whether charms are promulgated.
504
        to be returned.
182.2.18 by Aaron Bentley
Get api_search fully under test.
505
        """
362.1.6 by Curtis Hovey
DRY tests.
506
        self.makeCharm(owner='alice', name='ant', promulgated=True)
507
        self.makeCharm(owner='steve', name='bat')
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
508
        result = self.get_charms_and_bundles(type_='community')
182.2.18 by Aaron Bentley
Get api_search fully under test.
509
        self.assertEqual(['steve'], [charm['owner'] for charm in result])
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
510
        result = self.get_charms_and_bundles(type_='approved')
225.2.18 by Abel Deuring
ElasticSearch queries: Do not assuma that charmers are the only owners of promulgated charms.
511
        self.assertEqual(['alice'], [charm['owner'] for charm in result])
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
512
        result = self.get_charms_and_bundles(type_='environment')
182.2.18 by Aaron Bentley
Get api_search fully under test.
513
        self.assertEqual([], [charm['owner'] for charm in result])
514
        with self.assertRaises(InvalidCharmType):
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
515
            self.get_charms_and_bundles(type_='foo')
516
        result = self.get_charms_and_bundles(type_=['approved', 'community'])
225.2.18 by Abel Deuring
ElasticSearch queries: Do not assuma that charmers are the only owners of promulgated charms.
517
        self.assertEqual(['alice', 'steve'],
182.2.18 by Aaron Bentley
Get api_search fully under test.
518
                         [charm['owner'] for charm in result])
519
201.1.4 by Aaron Bentley
Search filters consider promulgation for charm type.
520
    def test_type_search_considers_promulgated(self):
521
        reviewed = self.makeCharm(promulgated=True)['_id']
203.1.4 by Aaron Bentley
Invoke update_date_created directly.
522
        self.makeCharm(promulgated=False)
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
523
        results = self.get_charms_and_bundles(type_=['approved'])
201.1.4 by Aaron Bentley
Search filters consider promulgation for charm type.
524
        self.assertEqual([reviewed], [result['_id'] for result in results])
525
182.2.18 by Aaron Bentley
Get api_search fully under test.
526
    def test_charms_text_search(self):
527
        abc_charm = self.makeCharm(summary='abc def')
182.2.19 by Aaron Bentley
Cleanup.
528
        self.makeCharm(summary='ghi jkl')
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
529
        charms = self.get_charms_and_bundles(text='def')
182.2.18 by Aaron Bentley
Get api_search fully under test.
530
        self.assertEqual(1, len(charms))
362.1.6 by Curtis Hovey
DRY tests.
531
        self.assertEqual(abc_charm['_id'], charms[0]['_id'])
182.2.18 by Aaron Bentley
Get api_search fully under test.
532
533
    def test_charms_text_search_respects_filters(self):
225.2.2 by Abel Deuring
factory.get_charm_json(): Proper handling of the promulgated parameter.
534
        official = self.makeCharm(summary='abc def', promulgated=True)
535
        self.makeCharm(promulgated=True)
182.2.19 by Aaron Bentley
Cleanup.
536
        self.makeCharm(summary='abc def', owner='steve')
537
        self.makeCharm(owner='steve')
325.1.7 by Brad Crittenden
Fixed valid charm search. Tests working but not complete.
538
        result = self.get_charms_and_bundles(type_=['approved'], text='def')
182.2.18 by Aaron Bentley
Get api_search fully under test.
539
        self.assertEqual([official['_id']],
362.1.6 by Curtis Hovey
DRY tests.
540
                         [charm['_id'] for charm in result])
182.2.18 by Aaron Bentley
Get api_search fully under test.
541
542
    def test_charms_limit(self):
543
        """Limit provides a maximum for charm numbers."""
544
        result = self.index_client.api_search('', {}, None, 6)
545
        self.assertEqual(0, len(result))
546
        self.makeCharm()
547
        self.makeCharm(owner='jrandom')
548
        result = self.index_client.api_search('', {}, None, 6)
549
        self.assertEqual(2, len(result))
550
        result = self.index_client.api_search('', {}, None, 1)
551
        self.assertEqual(1, len(result))
552
        result = self.index_client.api_search('', {}, None, 0)
553
        self.assertEqual(0, len(result))
554
        with self.assertRaises(NegativeLimit):
555
            result = self.index_client.api_search('', {}, None, -1)
188.1.1 by Abel Deuring
return only charms without errors in api_search() by default.
556
362.1.7 by Curtis Hovey
DRY tests.
557
    def test_api_search_with_store_error(self):
188.1.2 by Abel Deuring
return only charms without errors in search() by default.
558
        """api_search() does not return charms withs errors by default."""
188.1.1 by Abel Deuring
return only charms without errors in api_search() by default.
559
        self.makeCharm(name='evil-charm', charm_error=True)
560
        self.makeCharm(name='good-charm')
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
561
        self.makeBundle(name='great-bundle')
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
562
        result = self.index_client.api_search('', {}, None, doctype=None)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
563
        self.assertEqual(2, len(result))
362.1.2 by Curtis Hovey
Always include the doctype with api_search results when the doctype is not specified.
564
        names = set(charm['data']['name'] for charm in result)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
565
        self.assertEqual(set(('good-charm', 'great-bundle')), names)
188.1.1 by Abel Deuring
return only charms without errors in api_search() by default.
566
        # Charms with error are included in the search result
325.1.9 by Brad Crittenden
Reverted the changes to search_json to restore backwards compatability. Made the other readability and style changes from review.
567
        # if the parameter valid_only is set to False.  Bundles are
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
568
        # always included.
188.1.1 by Abel Deuring
return only charms without errors in api_search() by default.
569
        result = self.index_client.api_search(
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
570
            '', {}, None, valid_only=False, doctype=None)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
571
        self.assertEqual(3, len(result))
362.1.2 by Curtis Hovey
Always include the doctype with api_search results when the doctype is not specified.
572
        names = set(charm['data']['name'] for charm in result)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
573
        self.assertEqual(
574
            set(('good-charm', 'evil-charm', 'great-bundle')), names)
188.1.2 by Abel Deuring
return only charms without errors in search() by default.
575
362.1.7 by Curtis Hovey
DRY tests.
576
    def test_search_with_store_error(self):
188.1.2 by Abel Deuring
return only charms without errors in search() by default.
577
        """search() does not return charms withs errors by default."""
578
        self.makeCharm(name='evil-charm', owner='foo', charm_error=True)
579
        self.makeCharm(name='good-charm', owner='foo')
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
580
        self.makeBundle(name='great-bundle', owner='foo')
188.1.2 by Abel Deuring
return only charms without errors in search() by default.
581
        result = self.index_client.search('foo')
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
582
        self.assertEqual(2, result['matches'])
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
583
        data = result['results']['charm'][0]['data']
584
        self.assertEqual('good-charm', data.name)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
585
        data = result['results']['bundle'][0]['data']
586
        self.assertEqual('great-bundle', data.name)
188.1.2 by Abel Deuring
return only charms without errors in search() by default.
587
        # Charms with error are included in the search result
325.1.9 by Brad Crittenden
Reverted the changes to search_json to restore backwards compatability. Made the other readability and style changes from review.
588
        # if the parameter valid_only is set to False.
589
        result = self.index_client.search('foo', valid_only=False)
325.1.8 by Brad Crittenden
Update tests for bundle searching with valid_items_only
590
        self.assertEqual(3, result['matches'])
591
        names = set(e['data'].name for e in result['results']['charm'])
592
        names.update(set(e['data'].name for e in result['results']['bundle']))
593
        self.assertEqual(
594
            set(('good-charm', 'evil-charm', 'great-bundle')), names)
197.1.4 by Aaron Bentley
Implement searching by popularity.
595
213.1.3 by Aaron Bentley
Ensure search excludes error charms.
596
    def test_search_charm_with_error(self):
286.2.3 by Abel Deuring
fix obvious failure of test_search_charm_with_error()
597
        """searches include charms with processing errors."""
283.1.3 by Abel Deuring
ElasticSearchClient.search(): Return Charm objects instead of simple dictionaries.
598
        charm = Charm(self.makeCharm())
599
        result = self.index_client.search(charm.name)
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
600
        self.assertEqual(charm, result['results']['charm'][0]['data'])
283.1.3 by Abel Deuring
ElasticSearchClient.search(): Return Charm objects instead of simple dictionaries.
601
        charm._representation['error'] = {'error': 'error text'}
602
        self.index_client.index_charm(charm._representation)
603
        result = self.index_client.search(charm.name)
325.1.1 by Brad Crittenden
Checkpoint. Tests broken.
604
        self.assertEqual(charm, result['results']['charm'][0]['data'])
213.1.3 by Aaron Bentley
Ensure search excludes error charms.
605
213.1.5 by Aaron Bentley
Test API search.
606
    def test_api_search_charm_with_error(self):
286.2.2 by Abel Deuring
test that charms with processing errors are included by find_charms() and search()
607
        """Search results include charms with processing errors."""
213.1.5 by Aaron Bentley
Test API search.
608
        charm = self.makeCharm()
286.2.2 by Abel Deuring
test that charms with processing errors are included by find_charms() and search()
609
        charm['error'] = {'error': 'error text'}
213.1.5 by Aaron Bentley
Test API search.
610
        self.index_client.index_charm(charm)
611
        result = self.index_client.api_search(charm['name'])
362.1.8 by Curtis Hovey
Remove extra lines.
612
        self.assertEqual([charm], [r['data'] for r in result])
213.1.5 by Aaron Bentley
Test API search.
613
197.1.4 by Aaron Bentley
Implement searching by popularity.
614
    def test_search_charm_sort_most_downloaded(self):
615
        """Specifying 'downloaded' sorts results appropriately."""
288.2.1 by Rick Harding
It's alive and tests pass
616
        self.makeCharm(name='popular1', downloads=3)
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
617
        self.makeCharm(name='popular2', downloads=1)
618
        self.makeCharm(name='popular3', downloads=2)
619
        order132 = ['popular1', 'popular3', 'popular2']
197.1.4 by Aaron Bentley
Implement searching by popularity.
620
        result = self.index_client.api_search()
362.1.8 by Curtis Hovey
Remove extra lines.
621
        self.assertNotEqual(order132, [r['data']['name'] for r in result])
197.1.4 by Aaron Bentley
Implement searching by popularity.
622
        result = self.index_client.api_search(sort='downloaded')
362.1.8 by Curtis Hovey
Remove extra lines.
623
        self.assertEqual(order132, [r['data']['name'] for r in result])
197.1.6 by Aaron Bentley
Sort by date_created works.
624
625
    def test_search_charm_sort_newest(self):
626
        """Specifying 'new' sorts results appropriately."""
627
        self.makeCharm(name='new1', date_created=datetime(2013, 3, 1))
628
        self.makeCharm(name='new2', date_created=datetime(2013, 1, 2))
629
        self.makeCharm(name='new3', date_created=datetime(2013, 1, 1))
630
        order123 = ['new1', 'new2', 'new3']
631
        result = self.index_client.api_search()
362.1.8 by Curtis Hovey
Remove extra lines.
632
        self.assertNotEqual(order123, [r['data']['name'] for r in result])
197.1.6 by Aaron Bentley
Sort by date_created works.
633
        result = self.index_client.api_search(sort='new')
362.1.8 by Curtis Hovey
Remove extra lines.
634
        self.assertEqual(order123, [r['data']['name'] for r in result])
197.1.8 by Aaron Bentley
Exclude unknown sorts.
635
636
    def test_search_unknown_sort(self):
637
        """Specifying an unknown sort raises an exception."""
638
        with self.assertRaises(ValueError) as e:
197.1.9 by Aaron Bentley
Cleanup.
639
            self.index_client.api_search(sort='foo')
197.1.8 by Aaron Bentley
Exclude unknown sorts.
640
        self.assertEqual(str(e.exception), 'Invalid sort: foo.')
221.2.1 by Aaron Bentley
Add ability to manipulate aliases.
641
642
    def test_update_alias(self):
643
        alias = ElasticSearchClient(self.index_client._client,
644
                                    'temp_index_alias')
645
        self.assertEqual([], alias.get_aliased())
646
        alias.update_aliased(self.index_client.index_name, [])
647
        self.assertEqual([self.index_client.index_name], alias.get_aliased())
648
        self.use_context(temp_index_client('foo'))
649
        alias.update_aliased('foo', [])
650
        self.assertItemsEqual([self.index_client.index_name, 'foo'],
651
                              alias.get_aliased())
652
        self.use_context(temp_index_client('bar'))
653
        alias.update_aliased('bar', alias.get_aliased())
654
        self.assertEqual(['bar'], alias.get_aliased())
221.2.2 by Aaron Bentley
Add ability to copy indices.
655
344.2.2 by Brad Crittenden
Changed test names for clarity as suggested in review.
656
    def test_create_replacement_charm_indexed_in_place(self):
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
657
        charm_data = {'_id': 'a', 'name': 'foo', 'owner': 'bar',
658
                      'series': 'baz'}
659
        self.index_client.index_charm(charm_data)
241.1.4 by Aaron Bentley
reindex permits specifying charms.
660
        copy = self.index_client.create_replacement('index-copy')
221.2.2 by Aaron Bentley
Add ability to copy indices.
661
        self.addCleanup(copy.delete_index)
662
        copy.wait_for_startup()
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
663
        mapping = copy.get_mapping()[CHARM]
664
        self.assertIn('series', mapping['properties']['data']['properties'])
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
665
        self.assertEqual(charm_data, copy.get('a'))
221.2.3 by Aaron Bentley
Ensure unique failure for incompatible mapping.
666
344.2.2 by Brad Crittenden
Changed test names for clarity as suggested in review.
667
    def test_create_replacement_bundle_indexed_in_place(self):
344.2.1 by Brad Crittenden
Make create_replacement be bundle aware.
668
        bundle_data = {'_id': 'a', 'name': 'foo', 'owner': 'bar',
358.1.10 by Brad Crittenden
Fix tests from changing bundle revision use.
669
                       'basket_name': 'basket', 'data': {'series': 'baz'}}
344.2.1 by Brad Crittenden
Make create_replacement be bundle aware.
670
        self.index_client.index_bundle(bundle_data)
671
        copy = self.index_client.create_replacement('index-copy')
672
        self.addCleanup(copy.delete_index)
673
        copy.wait_for_startup()
351.1.2 by Benji York
finish up the branch
674
        mapping = copy.get_mapping(doctype=BUNDLE)[BUNDLE]
675
        self.assertIn('series', mapping['properties']['data']['properties'])
676
        self.assertEqual(
677
            {u'_id': u'~bar/basket/foo', u'data': bundle_data},
678
            copy.get(Bundle(bundle_data).id, BUNDLE))
344.2.1 by Brad Crittenden
Make create_replacement be bundle aware.
679
344.2.2 by Brad Crittenden
Changed test names for clarity as suggested in review.
680
    def test_create_replacement_charms_replacing_old(self):
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
681
        charm_data = {'_id': 'a', 'name': 'foo', 'owner': 'bar',
682
                      'series': 'baz'}
683
        charm_data2 = {'_id': 'a', 'name': 'bar', 'owner': 'bar',
684
                       'series': 'baz'}
685
        self.index_client.index_charm(charm_data)
241.1.4 by Aaron Bentley
reindex permits specifying charms.
686
        copy = self.index_client.create_replacement(
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
687
            charms=[charm_data2])
241.1.4 by Aaron Bentley
reindex permits specifying charms.
688
        self.addCleanup(copy.delete_index)
689
        copy.wait_for_startup()
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
690
        mapping = copy.get_mapping()[CHARM]
691
        self.assertIn('series', mapping['properties']['data']['properties'])
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
692
        self.assertEqual(charm_data2, copy.get('a'))
241.1.4 by Aaron Bentley
reindex permits specifying charms.
693
344.2.2 by Brad Crittenden
Changed test names for clarity as suggested in review.
694
    def test_create_replacement_bundles_replacing_old(self):
344.2.1 by Brad Crittenden
Make create_replacement be bundle aware.
695
        bundle_data = {'_id': 'a', 'name': 'foo', 'owner': 'bar',
358.1.10 by Brad Crittenden
Fix tests from changing bundle revision use.
696
                       'series': 'first', 'basket_name': 'basket'}
351.1.2 by Benji York
finish up the branch
697
        bundle_data2 = {'_id': 'a', 'name': 'foo', 'owner': 'bar',
358.1.10 by Brad Crittenden
Fix tests from changing bundle revision use.
698
                        'series': 'second', 'basket_name': 'basket'}
344.2.1 by Brad Crittenden
Make create_replacement be bundle aware.
699
        self.index_client.index_bundle(bundle_data)
700
        copy = self.index_client.create_replacement(
701
            bundles=[bundle_data2])
702
        self.addCleanup(copy.delete_index)
703
        copy.wait_for_startup()
351.1.2 by Benji York
finish up the branch
704
        mapping = copy.get_mapping(doctype=BUNDLE)[BUNDLE]
705
        self.assertIn('series', mapping['properties']['data']['properties'])
706
        self.assertEqual(
707
            {u'_id': u'~bar/basket/foo', u'data': bundle_data2},
708
            copy.get(Bundle(bundle_data).id, BUNDLE))
709
344.2.1 by Brad Crittenden
Make create_replacement be bundle aware.
710
    def test_create_replacement_missing(self):
241.1.5 by Aaron Bentley
Handle reindexing when the index does not exist.
711
        client = ElasticSearchClient.from_settings(get_ini(), 'temp-index')
241.1.9 by Aaron Bentley
Comment tests that prove no exception raised.
712
        # This should not raise an exception, even though the index has not
713
        # been created.
241.1.5 by Aaron Bentley
Handle reindexing when the index does not exist.
714
        copy = client.create_replacement()
715
        self.addCleanup(copy.delete_index)
716
221.2.3 by Aaron Bentley
Ensure unique failure for incompatible mapping.
717
    def test_put_mapping_incompatible_mapping_error(self):
718
        # The error we get from an incompatible mapping is IncompatibleMapping
719
        context = temp_index_client(name='foo', put_mapping=False)
720
        client = self.use_context(context)
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
721
        put_incompatible_mapping(client)
221.2.3 by Aaron Bentley
Ensure unique failure for incompatible mapping.
722
        with self.assertRaises(IncompatibleMapping):
723
            client.put_mapping()
221.2.5 by Aaron Bentley
Implement reindex and migrate.
724
221.3.3 by Aaron Bentley
put_mapping raises IndexMissing as needed.
725
    def test_put_mapping_missing_index(self):
726
        # The error we get from a missing index is IndexMissing
727
        client = ElasticSearchClient.from_settings(get_ini(), 'temp-index')
728
        with self.assertRaises(IndexMissing):
729
            client.put_mapping()
221.2.13 by Aaron Bentley
Merged migrate-elasticsearch into migrate.
730
324.1.2 by Brad Crittenden
Add support for bundles in search
731
    def test_get_mapping_default(self):
332.1.1 by Brad Crittenden
Change api_search and related charms to ignore bundles for now.
732
        # Calling get_mapping with no doctype returns charms.
324.1.2 by Brad Crittenden
Add support for bundles in search
733
        client = self.index_client
734
        client.put_mapping()
735
        mapping = client.get_mapping()
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
736
        self.assertEqual([CHARM], mapping.keys())
324.1.2 by Brad Crittenden
Add support for bundles in search
737
738
    def test_get_mapping_charms(self):
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
739
        # Calling get_mapping with CHARM returns charms.
324.1.2 by Brad Crittenden
Add support for bundles in search
740
        client = self.index_client
741
        client.put_mapping()
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
742
        mapping = client.get_mapping(CHARM)
743
        self.assertEqual([CHARM], mapping.keys())
324.1.2 by Brad Crittenden
Add support for bundles in search
744
745
    def test_get_mapping_bundles(self):
746
        # Calling get_mapping with 'bundles' returns bundles.
747
        client = self.index_client
748
        client.put_mapping()
749
        mapping = client.get_mapping('bundle')
750
        self.assertEqual(['bundle'], mapping.keys())
751
752
    def test_get_mapping_all(self):
753
        # Calling get_mapping with None returns full mapping.
754
        client = self.index_client
755
        client.put_mapping()
756
        mapping = client.get_mapping(None)
757
        self.assertEqual(['temp_index'], mapping.keys())
758
        self.assertEqual(
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
759
            ['bundle', CHARM],
324.1.2 by Brad Crittenden
Add support for bundles in search
760
            sorted(mapping['temp_index'].keys()))
761
367.2.16 by Curtis Hovey
Added tests to verify get_items() returns charms and bundles data.
762
    def test_get_items(self):
763
        # The method returns the typed documents that match the typed_ids.
764
        charm_data = {'name': 'charm', 'owner': 'owner1', 'series': 'series'}
765
        self.index_client.index_charm(charm_data)
766
        bundle_data = {
767
            'name': 'bundle', 'owner': 'owner2', 'basket_name': 'basket'}
768
        self.index_client.index_bundle(bundle_data)
769
        typed_ids = [
770
            {'_id': '~owner1/series/charm', '_type': CHARM},
771
            {'_id': '~owner2/basket/bundle', '_type': BUNDLE}
772
        ]
773
        docs = self.index_client.get_items(typed_ids)
774
        expected = [
775
            {'data': charm_data, 'doctype': CHARM},
776
            {'data': bundle_data, 'doctype': BUNDLE}
777
        ]
778
        self.assertEqual(expected, docs)
779
367.2.15 by Curtis Hovey
Added a test to verify that passing an empty list to get_items() retuns an empty list.
780
    def test_get_items_with_empty_list(self):
367.2.16 by Curtis Hovey
Added tests to verify get_items() returns charms and bundles data.
781
        # When test_ids is an empty list, the list of docs is also empty.
367.2.15 by Curtis Hovey
Added a test to verify that passing an empty list to get_items() retuns an empty list.
782
        self.assertEqual([], self.index_client.get_items([]))
783
217.1.9 by Aaron Bentley
Implement related-interface search.
784
    def test_related_charms(self):
785
        client = self.index_client
786
        charm = factory.get_charm_json(provides={'abc': {'interface': 'def'}})
787
        client.index_charm(charm)
217.1.17 by Aaron Bentley
Fix test failure due to signature change.
788
        requires, provides = client.related_charms(
789
            set(charm['i_provides']), set(charm['i_requires']))
217.1.9 by Aaron Bentley
Implement related-interface search.
790
        self.assertEqual({}, provides)
791
        self.assertEqual({}, requires)
217.1.17 by Aaron Bentley
Fix test failure due to signature change.
792
        requires, provides = client.related_charms(set(['def']), set(['def']))
217.1.18 by Aaron Bentley
Update related_charms to include weight.
793
        self.assertEqual(['def'], provides.keys())
217.1.23 by Aaron Bentley
Change related_charms() to match search()
794
        self.assertEqual(charm, provides['def'][0]['data'])
795
        self.assertIs(float, type(provides['def'][0]['weight']))
217.1.18 by Aaron Bentley
Update related_charms to include weight.
796
        self.assertEqual(2, len(provides['def'][0]))
217.1.9 by Aaron Bentley
Implement related-interface search.
797
        self.assertEqual({}, requires)
798
        charm2 = factory.get_charm_json(
217.1.21 by Aaron Bentley
Fix lint.
799
            provides={
800
                'abc': {'interface': 'def'},
801
                'mno': {'interface': 'pqr'}},
217.1.9 by Aaron Bentley
Implement related-interface search.
802
            requires={'ghi': {'interface': 'jkl'}})
803
        client.index_charm(charm2)
217.1.17 by Aaron Bentley
Fix test failure due to signature change.
804
        requires, provides = client.related_charms(set(['jkl']), set())
217.1.9 by Aaron Bentley
Implement related-interface search.
805
        self.assertEqual({}, provides)
217.1.18 by Aaron Bentley
Update related_charms to include weight.
806
        self.assertEqual(['jkl'], requires.keys())
217.1.23 by Aaron Bentley
Change related_charms() to match search()
807
        self.assertEqual(charm2, requires['jkl'][0]['data'])
217.1.17 by Aaron Bentley
Fix test failure due to signature change.
808
        requires, provides = client.related_charms(set(['jkl']), set(['def']))
217.1.9 by Aaron Bentley
Implement related-interface search.
809
        self.assertEqual(['def'], provides.keys())
217.1.18 by Aaron Bentley
Update related_charms to include weight.
810
        self.assertItemsEqual([charm, charm2],
217.1.23 by Aaron Bentley
Change related_charms() to match search()
811
                              [payload['data'] for payload in provides['def']])
217.1.18 by Aaron Bentley
Update related_charms to include weight.
812
        self.assertEqual([charm2],
217.1.23 by Aaron Bentley
Change related_charms() to match search()
813
                         [payload['data'] for payload in requires['jkl']])
217.1.17 by Aaron Bentley
Fix test failure due to signature change.
814
        requires, provides = client.related_charms(set(),
815
                                                   set(['def', 'pqr', 'xyz']))
217.1.9 by Aaron Bentley
Implement related-interface search.
816
        self.assertItemsEqual(['def', 'pqr'], provides.keys())
217.1.23 by Aaron Bentley
Change related_charms() to match search()
817
        self.assertEqual([charm2], [payload['data']
818
                         for payload in provides['pqr']])
217.1.18 by Aaron Bentley
Update related_charms to include weight.
819
        self.assertItemsEqual([charm, charm2],
217.1.23 by Aaron Bentley
Change related_charms() to match search()
820
                              [payload['data'] for payload in provides['def']])
217.1.9 by Aaron Bentley
Implement related-interface search.
821
217.1.22 by Aaron Bentley
Boosting applies to related_charms()
822
    def test_related_charms_official_boost(self):
823
        """An official version of a charm rates 10x higher."""
824
        charm = factory.get_charm_json(
225.2.10 by Abel Deuring
trunk merged; conflicts and new test failures fixed.
825
            provides={'foo': {'interface': 'telnet'}}, promulgated=True)
217.1.22 by Aaron Bentley
Boosting applies to related_charms()
826
        official_id = charm['_id']
827
        unofficial_id = official_id + '-1'
828
        client = self.index_client
829
        client.index_charm(charm)
830
        charm['owner'] = 'jrandom'
831
        charm['_id'] = unofficial_id
225.2.18 by Abel Deuring
ElasticSearch queries: Do not assuma that charmers are the only owners of promulgated charms.
832
        charm['store_url'] = get_address(charm, short=False)
217.1.22 by Aaron Bentley
Boosting applies to related_charms()
833
        client.index_charm(charm)
834
        requires, provides = client.related_charms(set(), set(['telnet']))
217.1.23 by Aaron Bentley
Change related_charms() to match search()
835
        by_id = dict((result['data']['_id'], result['weight'])
217.1.22 by Aaron Bentley
Boosting applies to related_charms()
836
                     for result in provides['telnet'])
837
        self.assertAlmostEqual(by_id[official_id], by_id[unofficial_id] * 10)
838
240.5.16 by Aaron Bentley
Related charms is limited to the same series.
839
    def test_related_charms_series_filter(self):
840
        def filter_data(result):
841
            return dict((key, [entry['data'] for entry in value])
842
                        for key, value in result.items())
843
        alpha_charm = factory.get_charm_json(
844
            series='alpha', provides={'asdf': {'interface': 'foo'}})
845
        beta_charm = factory.get_charm_json(
846
            series='beta', requires={'asdf': {'interface': 'bar'}})
847
        self.index_client.index_charms([alpha_charm, beta_charm])
848
        requires, provides = self.index_client.related_charms({'bar'}, {'foo'})
849
        self.assertEqual({'bar': [beta_charm]}, filter_data(requires))
850
        self.assertEqual({'foo': [alpha_charm]}, filter_data(provides))
851
        requires, provides = self.index_client.related_charms(
852
            {'bar'}, {'foo'}, series='alpha')
853
        self.assertEqual({}, filter_data(requires))
854
        self.assertEqual({'foo': [alpha_charm]}, filter_data(provides))
855
        requires, provides = self.index_client.related_charms(
856
            {'bar'}, {'foo'}, series='beta')
857
        self.assertEqual({'bar': [beta_charm]}, filter_data(requires))
858
        self.assertEqual({}, filter_data(provides))
859
        requires, provides = self.index_client.related_charms(
860
            {'bar'}, {'foo'}, series='gamma')
861
        self.assertEqual({}, filter_data(requires))
862
        self.assertEqual({}, filter_data(provides))
863
251.1.1 by Aaron Bentley
Support exclude_name in ESC.related_charms.
864
    def test_related_charms_exclude_name(self):
865
        charm = factory.get_charm_json(
866
            requires={'asdf': {'interface': 'bar'}})
867
        self.index_client.index_charm(charm)
868
        requires, provides = self.index_client.related_charms({'bar'}, set())
869
        self.assertEqual(charm, requires['bar'][0]['data'])
870
        requires, provides = self.index_client.related_charms(
871
            {'bar'}, set(), exclude_name=charm['name'])
872
        self.assertEqual({}, requires)
873
251.2.1 by Abel Deuring
sanitize full text search strings to avoid parsing errors
874
    def test_special_characters_in_full_text_term(self):
875
        # Some characters have a special meaning for the text parser
876
        # used by ElasticSearch/Lucene. Since these characters can
877
        # cause exceptions when used unexpectedly, like unbalanced
878
        # parentheses, they are replaced with spaces.
879
        for char in range(0, 128):
880
            char = chr(char)
881
            try:
882
                text = u'foo %s bar' % char
883
                self.index_client.search(text)
884
            except ElasticHttpError:
885
                self.fail(
886
                    "Character '%s' inside a search term causes "
887
                    "ElasticHttpError" % char)
888
            try:
889
                text = 'foo%s' % char
890
                self.index_client.search(text)
891
            except ElasticHttpError:
892
                self.fail(
893
                    "Character '%s' at the end of a search term causes "
894
                    "ElasticHttpError" % char)
895
370 by Abel Deuring
tests with spurious failures re-enabled; spurious IndexMissing exceptions in temp_index_client() and in test_exodus_update_runs_against_temp_but_generates_pending() fixed.
896
    def test_mapping_changes_during_indexing(self):
346.1.1 by Abel Deuring
Define a static mapping for charms. Note: The properties 'provides' and 'requires' are still dynamic.
897
        # When a charm is indexed, only the mappings for the properties
898
        # "requires" and "provides" change.
346.1.3 by Abel Deuring
test failures fixed.
899
        before = self.index_client.get_mapping()['charm']['properties']
900
        before = before['data']['properties']
346.1.1 by Abel Deuring
Define a static mapping for charms. Note: The properties 'provides' and 'requires' are still dynamic.
901
        charm = factory.get_charm_json()
902
        self.index_client.index_charm(charm)
346.1.3 by Abel Deuring
test failures fixed.
903
        after = self.index_client.get_mapping()['charm']['properties']
904
        after = after['data']['properties']
905
        before_provides = before.pop('provides')
906
        before_requires = before.pop('requires')
907
        after_provides = after.pop('provides')
908
        after_requires = after.pop('requires')
346.1.5 by Abel Deuring
migration for the new mapping added.
909
        self.assertEqual(before, after)
346.1.1 by Abel Deuring
Define a static mapping for charms. Note: The properties 'provides' and 'requires' are still dynamic.
910
        self.assertNotEqual(before_provides, after_provides)
911
        self.assertNotEqual(before_requires, after_requires)
912
221.2.5 by Aaron Bentley
Implement reindex and migrate.
913
320.1.1 by Benji York
add indexing of bundles
914
class TestIndexingBundles(TestCase):
915
916
    def setUp(self):
917
        super(TestIndexingBundles, self).setUp()
918
        self.use_index_client()
919
920
    def exists_in_index(self, bundle_id):
921
        try:
922
            result = self.index_client._client.get(
923
                self.index_client.index_name, 'bundle', bundle_id)
924
        except ElasticHttpNotFoundError:
925
            return False
926
        return result['exists']
927
928
    def test_index_bundles(self):
929
        bundle_data = factory.get_bundle_data()
351.1.2 by Benji York
finish up the branch
930
        search_id = Bundle(bundle_data).search_id
931
        self.assertFalse(self.exists_in_index(search_id))
320.1.1 by Benji York
add indexing of bundles
932
        self.index_client.index_bundles([bundle_data])
351.1.2 by Benji York
finish up the branch
933
        self.assertTrue(self.exists_in_index(search_id))
320.1.1 by Benji York
add indexing of bundles
934
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
935
    def test_index_bundles_no_bundles(self):
936
        # Assert that no exception is raised.
937
        with self.assertRaises(AssertionError):
938
            with self.assertRaises(BaseException):
939
                self.index_client.index_bundles([])
940
351.1.1 by Aaron Bentley
Refactor and implement versionless bundle ids.
941
    def test_only_one_revision_per_bundle(self):
942
        bundle_data = factory.get_bundle_data(
358.1.14 by Brad Crittenden
Sort in mongo. Better name basket_with_rev for get_bundle_data.
943
            owner='a', name='b', basket_with_rev='c/1')
351.1.1 by Aaron Bentley
Refactor and implement versionless bundle ids.
944
        bundle_data_2 = factory.get_bundle_data(
358.1.14 by Brad Crittenden
Sort in mongo. Better name basket_with_rev for get_bundle_data.
945
            owner='a', name='b', basket_with_rev='c/2')
351.1.1 by Aaron Bentley
Refactor and implement versionless bundle ids.
946
        self.index_client.index_bundle(bundle_data)
947
        self.index_client.index_bundle(bundle_data_2)
362.1.2 by Curtis Hovey
Always include the doctype with api_search results when the doctype is not specified.
948
        self.assertEqual(['~a/c/2/b'], [b['data']['_id'] for b in
351.1.1 by Aaron Bentley
Refactor and implement versionless bundle ids.
949
                         self.index_client.api_search(doctype=None)])
950
320.1.1 by Benji York
add indexing of bundles
951
346.1.5 by Abel Deuring
migration for the new mapping added.
952
def put_mapping(client, properties, dynamic=True):
221.2.5 by Aaron Bentley
Implement reindex and migrate.
953
    client._client.put_mapping(
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
954
        client.index_name, CHARM, {
955
            CHARM: {
346.1.5 by Abel Deuring
migration for the new mapping added.
956
                'dynamic': dynamic,
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
957
                'properties': {'data': {'properties': properties}}
221.2.5 by Aaron Bentley
Implement reindex and migrate.
958
            }
959
        }
960
    )
961
962
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
963
def put_incompatible_mapping(index_client):
964
    put_mapping(index_client, {
965
        'name': {'type': 'string', 'index': 'analyzed'},
966
    })
967
968
221.2.12 by Aaron Bentley
Rename migrate to update.
969
class TestUpdate(TestCase):
221.2.5 by Aaron Bentley
Implement reindex and migrate.
970
221.2.12 by Aaron Bentley
Rename migrate to update.
971
    def test_update_noop(self):
221.2.5 by Aaron Bentley
Implement reindex and migrate.
972
        temp_client = self.use_index_client()
221.2.12 by Aaron Bentley
Rename migrate to update.
973
        update(temp_client)
221.2.5 by Aaron Bentley
Implement reindex and migrate.
974
221.2.12 by Aaron Bentley
Rename migrate to update.
975
    def test_compatible_update(self):
221.2.5 by Aaron Bentley
Implement reindex and migrate.
976
        index_client = self.use_index_client(put_mapping=False)
977
        put_mapping(index_client, {
324.1.1 by Brad Crittenden
Make generic search routines for future support of charms and bundles.
978
            'box': {'type': 'string', 'index': 'not_analyzed'},
221.2.5 by Aaron Bentley
Implement reindex and migrate.
979
        })
324.1.3 by Brad Crittenden
Add tests for bundle searching methods
980
        mapping = index_client.get_mapping()[CHARM]
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
981
        self.assertNotIn('name', mapping['properties']['data']['properties'])
221.2.12 by Aaron Bentley
Rename migrate to update.
982
        update(index_client)
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
983
        mapping = index_client.get_mapping()[CHARM]['properties']['data']
221.2.5 by Aaron Bentley
Implement reindex and migrate.
984
        self.assertIn('box', mapping['properties'])
985
        self.assertIn('name', mapping['properties'])
986
221.2.12 by Aaron Bentley
Rename migrate to update.
987
    def test_incompatible_update(self):
221.2.5 by Aaron Bentley
Implement reindex and migrate.
988
        index_client = self.use_index_client(put_mapping=False)
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
989
        put_incompatible_mapping(index_client)
221.2.12 by Aaron Bentley
Rename migrate to update.
990
        update(index_client)
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
991
        mapping = index_client.get_mapping()[CHARM]['properties']['data']
221.2.5 by Aaron Bentley
Implement reindex and migrate.
992
        self.assertEqual('not_analyzed',
993
                         mapping['properties']['name']['index'])
994
221.2.14 by Aaron Bentley
Handle no existing index, simplify ensure_index to create_index.
995
    def test_update_no_index(self):
996
        index_client = ElasticSearchClient.from_settings(
997
            get_ini(), 'temp-index')
998
        actual_client = update(index_client)
999
        self.addCleanup(actual_client.delete_index)
1000
        self.assertEqual([actual_client.index_name],
1001
                         index_client.get_aliased())
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
1002
        mapping = index_client.get_mapping()[CHARM]['properties']['data']
221.2.14 by Aaron Bentley
Handle no existing index, simplify ensure_index to create_index.
1003
        self.assertEqual('not_analyzed',
1004
                         mapping['properties']['name']['index'])
1005
346.1.5 by Abel Deuring
migration for the new mapping added.
1006
    def update_to_static_mapping(self, force_reindex):
1007
        index_client = self.use_index_client(put_mapping=False)
1008
        put_mapping(
1009
            index_client,
1010
            {'box': {'type': 'string', 'index': 'not_analyzed'}},
1011
            dynamic=True)
375 by Abel Deuring
fix spurious test failure in test_dynamic_to_static_mapping_forced_reindex()
1012
        # Prevent spurious exceptions
1013
        # "InvalidIndexNameException[[temp_index] Invalid index name
1014
        # [temp_index], an alias with the same name already exists"
346.1.5 by Abel Deuring
migration for the new mapping added.
1015
        update(index_client, force_reindex)
376 by Abel Deuring
use the condition 'wait_for_status=green' in ElasticSearchClient.wait_for_startup(); use wait_for_startup() instead of client._health(...)
1016
        index_client.wait_for_startup()
346.1.5 by Abel Deuring
migration for the new mapping added.
1017
        updated_mapping = index_client.get_mapping()
1018
        # A property 'files' is not defined in the current mapping.
1019
        self.assertNotIn(
1020
            'files',
1021
            updated_mapping['charm']['properties']['data']['properties'])
1022
        index_client.index_charm(factory.get_charm_json())
1023
        return index_client
1024
375 by Abel Deuring
fix spurious test failure in test_dynamic_to_static_mapping_forced_reindex()
1025
    def test_simple_change_dynamic_to_static_mapping(self):
346.1.5 by Abel Deuring
migration for the new mapping added.
1026
        # If an existing mapping is dynamic (the default for ElasticSearch)
1027
        # and if a new mapping is specified as static, the two mappings
1028
        # are considered compatible, but the resulting mapping is
1029
        # still dynamic.
1030
        index_client = self.update_to_static_mapping(force_reindex=False)
1031
        updated_mapping = index_client.get_mapping()
1032
        # charm['files'] is not supposed to be included in the new
1033
        # mapping, but it still exists if force_reindex is not used.
1034
        self.assertIn(
1035
            'files',
1036
            updated_mapping['charm']['properties']['data']['properties'])
1037
1038
    def test_dynamic_to_static_mapping_forced_reindex(self):
1039
        # If an existing mapping is dynamic (the defult) and if a
1040
        # new mapping is specified as static, the two mappings
1041
        # are considered compatible, but the resulting mapping is
1042
        # still dynamic.
1043
        index_client = self.update_to_static_mapping(force_reindex=True)
1044
        updated_mapping = index_client.get_mapping()
1045
        # The mapping is indeed static; charm['files'] is not indexed.
1046
        self.assertNotIn(
1047
            'files',
1048
            updated_mapping['charm']['properties']['data']['properties'])
1049
221.2.5 by Aaron Bentley
Implement reindex and migrate.
1050
1051
class TestReindex(TestCase):
1052
1053
    def test_reindex_aliased(self):
1054
        index_client = ElasticSearchClient.from_settings(
1055
            get_ini(), 'temp_aliased')
1056
        alias = ElasticSearchClient(index_client._client, 'alias')
221.2.14 by Aaron Bentley
Handle no existing index, simplify ensure_index to create_index.
1057
        index_client.create_index()
221.2.5 by Aaron Bentley
Implement reindex and migrate.
1058
        try:
1059
            alias.update_aliased(index_client.index_name, [])
1060
        except:
1061
            index_client.delete_index()
1062
            raise
1063
        self.addCleanup(alias.delete_index)
302.4.10 by Aaron Bentley
In index, put charm document into data attribute, to support ID separation.
1064
        put_incompatible_mapping(index_client)
221.2.5 by Aaron Bentley
Implement reindex and migrate.
1065
        self.assertEqual(['temp_aliased'], alias.get_aliased())
226.1.1 by Aaron Bentley
Fix transient test failure.
1066
        index_client.wait_for_startup()
221.2.5 by Aaron Bentley
Implement reindex and migrate.
1067
        new_aliased = reindex(alias)
1068
        self.assertEqual([new_aliased.index_name], alias.get_aliased())
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
1069
        mapping = alias.get_mapping()[CHARM]['properties']['data']
221.2.5 by Aaron Bentley
Implement reindex and migrate.
1070
        self.assertEqual('not_analyzed',
302.5.1 by Aaron Bentley
Fake merge of pre-versioned-ids-for-charms.
1071
                         mapping['properties']['name']['index'])
241.1.5 by Aaron Bentley
Handle reindexing when the index does not exist.
1072
1073
    def test_reindexed_no_client_charms(self):
1074
        client = ElasticSearchClient.from_settings(get_ini())
241.1.9 by Aaron Bentley
Comment tests that prove no exception raised.
1075
        # This should not raise an exception, even though the index does not
1076
        # exist.
241.1.5 by Aaron Bentley
Handle reindexing when the index does not exist.
1077
        new_client = reindex(client, charms=[])
1078
        new_client.delete_index()