• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.ZCatalog / 15520766933

08 Jun 2025 05:13PM UTC coverage: 85.394% (+0.08%) from 85.315%
15520766933

Pull #159

github

web-flow
Merge branch 'master' into issue-148-try02
Pull Request #159: Searches with "not" in a KeywordIndex do not return records that do not contain a value for the index (2nd)

526 of 762 branches covered (69.03%)

Branch coverage included in aggregate %.

21 of 21 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

3163 of 3558 relevant lines covered (88.9%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.79
/src/Products/ZCatalog/tests/test_catalog.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE.
11
#
12
##############################################################################
13

14
import random
1✔
15
import unittest
1✔
16
from itertools import chain
1✔
17

18
import ExtensionClass
1✔
19
from BTrees.IIBTree import IISet
1✔
20

21
from Products.PluginIndexes.FieldIndex.FieldIndex import FieldIndex
1✔
22
from Products.PluginIndexes.KeywordIndex.KeywordIndex import KeywordIndex
1✔
23
from Products.ZCTextIndex.OkapiIndex import OkapiIndex
1✔
24
from Products.ZCTextIndex.ZCTextIndex import PLexicon
1✔
25
from Products.ZCTextIndex.ZCTextIndex import ZCTextIndex
1✔
26

27

28
class ZDummy(ExtensionClass.Base):
1✔
29
    def __init__(self, num):
1✔
30
        self.num = num
1✔
31

32
    def title(self):
1✔
33
        return f'{self.num:d}'
1✔
34

35

36
class Dummy(ExtensionClass.Base):
1✔
37

38
    att1 = 'att1'
1✔
39
    att2 = 'att2'
1✔
40
    att3 = ['att3']
1✔
41
    foo = 'foo'
1✔
42

43
    def __init__(self, num):
1✔
44
        self.num = num
1✔
45
        if isinstance(num, int) and (self.num % 10) == 0:
1✔
46
            self.ends_in_zero = True
1✔
47

48
    def col1(self):
1✔
49
        return 'col1'
1✔
50

51
    def col2(self):
1✔
52
        return 'col2'
1✔
53

54
    def col3(self):
1✔
55
        return ['col3']
1✔
56

57

58
class MultiFieldIndex(FieldIndex):
1✔
59

60
    def getIndexQueryNames(self):
1✔
61
        return [self.id, 'bar']
1✔
62

63

64
class ObjRS(ExtensionClass.Base):
1✔
65

66
    def __init__(self, num):
1✔
67
        self.number = num
1✔
68

69

70
class TestAddDelColumn(unittest.TestCase):
1✔
71

72
    def _make_one(self):
1✔
73
        from Products.ZCatalog.Catalog import Catalog
1✔
74
        return Catalog()
1✔
75

76
    def test_add(self):
1✔
77
        catalog = self._make_one()
1✔
78
        catalog.addColumn('id')
1✔
79
        self.assertIn('id', catalog.schema, 'add column failed')
1✔
80

81
    def test_add_bad(self):
1✔
82
        from Products.ZCatalog.Catalog import CatalogError
1✔
83
        catalog = self._make_one()
1✔
84
        self.assertRaises(CatalogError, catalog.addColumn, '_id')
1✔
85

86
    def test_add_with_space(self):
1✔
87
        catalog = self._make_one()
1✔
88
        catalog.addColumn(' space ')
1✔
89
        self.assertNotIn(' space ', catalog.schema,
1✔
90
                         'space not stripped in add column')
91
        self.assertIn('space', catalog.schema,
1✔
92
                      'stripping space in add column failed')
93

94
    def test_add_brains(self):
1✔
95
        catalog = self._make_one()
1✔
96
        catalog.addColumn('col1')
1✔
97
        catalog.addColumn('col3')
1✔
98
        for i in range(3):
1✔
99
            catalog.catalogObject(Dummy(3), repr(i))
1✔
100
        self.assertNotIn('col2', catalog.data.values()[0])
1✔
101
        catalog.addColumn('col2', default_value='new')
1✔
102
        self.assertIn('col2', catalog.schema, 'add column failed')
1✔
103
        self.assertIn('new', catalog.data.values()[0])
1✔
104

105
    def test_add_threshold(self):
1✔
106
        catalog = self._make_one()
1✔
107
        catalog.addColumn('col1')
1✔
108
        for i in range(3):
1✔
109
            catalog.catalogObject(Dummy(3), repr(i))
1✔
110
        catalog.addColumn('col2', threshold=None)
1✔
111

112
    def test_del(self):
1✔
113
        catalog = self._make_one()
1✔
114
        catalog.addColumn('id')
1✔
115
        catalog.delColumn('id')
1✔
116
        self.assertNotIn('id', catalog.schema, 'del column failed')
1✔
117

118
    def test_del_brains(self):
1✔
119
        catalog = self._make_one()
1✔
120
        catalog.addColumn('col1')
1✔
121
        catalog.addColumn('col2')
1✔
122
        catalog.addColumn('col3')
1✔
123
        for i in range(3):
1✔
124
            catalog.catalogObject(Dummy(3), repr(i))
1✔
125
        self.assertIn('col2', catalog.data.values()[0])
1✔
126
        catalog.delColumn('col2')
1✔
127
        self.assertNotIn('col2', catalog.schema, 'del column failed')
1✔
128
        self.assertNotIn('col2', catalog.data.values()[0])
1✔
129

130
    def test_del_threshold(self):
1✔
131
        catalog = self._make_one()
1✔
132
        catalog.addColumn('col1')
1✔
133
        for i in range(3):
1✔
134
            catalog.catalogObject(Dummy(3), repr(i))
1✔
135
        catalog.delColumn('col1', threshold=None)
1✔
136

137

138
class TestAddDelIndexes(unittest.TestCase):
1✔
139

140
    def _make_one(self):
1✔
141
        from Products.ZCatalog.Catalog import Catalog
1✔
142
        return Catalog()
1✔
143

144
    def test_add_field_index(self):
1✔
145
        catalog = self._make_one()
1✔
146
        idx = FieldIndex('id')
1✔
147
        catalog.addIndex('id', idx)
1✔
148
        self.assertIsInstance(catalog.indexes['id'], FieldIndex)
1✔
149

150
    def test_add_text_index(self):
1✔
151
        catalog = self._make_one()
1✔
152
        catalog.lexicon = PLexicon('lexicon')
1✔
153
        idx = ZCTextIndex('id', caller=catalog,
1✔
154
                          index_factory=OkapiIndex, lexicon_id='lexicon')
155
        catalog.addIndex('id', idx)
1✔
156
        i = catalog.indexes['id']
1✔
157
        self.assertIsInstance(i, ZCTextIndex)
1✔
158

159
    def test_add_keyword_index(self):
1✔
160
        catalog = self._make_one()
1✔
161
        idx = KeywordIndex('id')
1✔
162
        catalog.addIndex('id', idx)
1✔
163
        i = catalog.indexes['id']
1✔
164
        self.assertIsInstance(i, KeywordIndex)
1✔
165

166
    def test_add_with_space(self):
1✔
167
        catalog = self._make_one()
1✔
168
        idx = KeywordIndex(' space ')
1✔
169
        catalog.addIndex(' space ', idx)
1✔
170
        self.assertNotIn(' space ', catalog.indexes,
1✔
171
                         'space not stripped in add index')
172
        self.assertIn('space', catalog.indexes,
1✔
173
                      'stripping space in add index failed')
174
        i = catalog.indexes['space']
1✔
175
        # Note: i.id still has spaces in it.
176
        self.assertIsInstance(i, KeywordIndex)
1✔
177

178
    def test_del_field_index(self):
1✔
179
        catalog = self._make_one()
1✔
180
        idx = FieldIndex('id')
1✔
181
        catalog.addIndex('id', idx)
1✔
182
        catalog.delIndex('id')
1✔
183
        self.assertNotIn('id', catalog.indexes)
1✔
184

185
    def test_del_text_index(self):
1✔
186
        catalog = self._make_one()
1✔
187
        catalog.lexicon = PLexicon('lexicon')
1✔
188
        idx = ZCTextIndex('id', caller=catalog,
1✔
189
                          index_factory=OkapiIndex, lexicon_id='lexicon')
190
        catalog.addIndex('id', idx)
1✔
191
        catalog.delIndex('id')
1✔
192
        self.assertNotIn('id', catalog.indexes)
1✔
193

194
    def test_del_keyword_index(self):
1✔
195
        catalog = self._make_one()
1✔
196
        idx = KeywordIndex('id')
1✔
197
        catalog.addIndex('id', idx)
1✔
198
        catalog.delIndex('id')
1✔
199
        self.assertNotIn('id', catalog.indexes)
1✔
200

201

202
class TestCatalog(unittest.TestCase):
1✔
203

204
    upper = 10
1✔
205

206
    nums = list(range(upper))
1✔
207
    for i in range(upper):
1✔
208
        j = random.randrange(0, upper)
1✔
209
        tmp = nums[i]
1✔
210
        nums[i] = nums[j]
1✔
211
        nums[j] = tmp
1✔
212

213
    def _make_one(self, extra=None):
1✔
214
        from Products.ZCatalog.Catalog import Catalog
1✔
215
        catalog = Catalog()
1✔
216
        catalog.lexicon = PLexicon('lexicon')
1✔
217

218
        att1 = FieldIndex('att1')
1✔
219
        catalog.addIndex('att1', att1)
1✔
220

221
        att2 = ZCTextIndex('att2', caller=catalog,
1✔
222
                           index_factory=OkapiIndex, lexicon_id='lexicon')
223
        catalog.addIndex('att2', att2)
1✔
224

225
        att3 = KeywordIndex('att3')
1✔
226
        catalog.addIndex('att3', att3)
1✔
227

228
        if extra is not None:
1✔
229
            extra(catalog)
1✔
230

231
        for x in range(0, self.upper):
1✔
232
            catalog.catalogObject(Dummy(self.nums[x]), repr(x))
1✔
233
        return catalog.__of__(Dummy('foo'))
1✔
234

235
    def test_clear(self):
1✔
236
        catalog = self._make_one()
1✔
237
        self.assertGreater(len(catalog), 0)
1✔
238
        catalog.clear()
1✔
239
        self.assertEqual(catalog._length(), 0)
1✔
240
        self.assertEqual(len(catalog), 0)
1✔
241
        self.assertEqual(len(catalog.data), 0)
1✔
242
        self.assertEqual(len(catalog.paths), 0)
1✔
243
        self.assertEqual(len(catalog.uids), 0)
1✔
244
        for index_id in catalog.indexes:
1✔
245
            index = catalog.getIndex(index_id)
1✔
246
            self.assertEqual(index.numObjects(), 0)
1✔
247

248
    def test_getitem(self):
1✔
249
        def extra(catalog):
1✔
250
            catalog.addColumn('att1')
1✔
251

252
        catalog = self._make_one(extra=extra)
1✔
253
        catalog_rids = set(catalog.data)
1✔
254
        brain_class = catalog._v_result_class
1✔
255
        brains = []
1✔
256
        brain_rids = set()
1✔
257
        for rid in catalog_rids:
1✔
258
            brain = catalog[rid]
1✔
259
            brains.append(brain)
1✔
260
            brain_rids.add(brain.getRID())
1✔
261
            self.assertIsInstance(brain, brain_class)
1✔
262
            self.assertEqual(brain.att1, 'att1')
1✔
263

264
        self.assertEqual(len(brains), len(catalog))
1✔
265
        self.assertEqual(catalog_rids, brain_rids)
1✔
266

267
    # updateBrains
268
    # __setstate__
269
    # useBrains
270
    # getIndex
271
    # updateMetadata
272

273
    def testCatalogObjectUpdateMetadataFalse(self):
1✔
274
        def extra(catalog):
1✔
275
            catalog.addColumn('att1')
1✔
276
            num = FieldIndex('num')
1✔
277
            catalog.addIndex('num', num)
1✔
278

279
        catalog = self._make_one(extra=extra)
1✔
280
        ob = Dummy(9999)
1✔
281
        catalog.catalogObject(ob, '9999')
1✔
282
        brain = catalog(num=9999)[0]
1✔
283
        self.assertEqual(brain.att1, 'att1')
1✔
284
        ob.att1 = 'foobar'
1✔
285
        catalog.catalogObject(ob, '9999', update_metadata=0)
1✔
286
        brain = catalog(num=9999)[0]
1✔
287
        self.assertEqual(brain.att1, 'att1')
1✔
288
        catalog.catalogObject(ob, '9999')
1✔
289
        brain = catalog(num=9999)[0]
1✔
290
        self.assertEqual(brain.att1, 'foobar')
1✔
291

292
    def testUniqueValuesForLength(self):
1✔
293
        catalog = self._make_one()
1✔
294
        a = catalog.uniqueValuesFor('att1')
1✔
295
        self.assertEqual(
1✔
296
            len(a), 1,
297
            f'bad number of unique values {a}'
298
        )
299

300
    def testUniqueValuesForContent(self):
1✔
301
        catalog = self._make_one()
1✔
302
        a = catalog.uniqueValuesFor('att1')
1✔
303
        self.assertEqual(a[0], 'att1', f'bad content {a[0]}')
1✔
304

305
    # hasuid
306
    # recordify
307
    # instantiate
308
    # getMetadataForRID
309
    # getIndexDataForRID
310
    # make_query
311

312
    def testKeywordIndexWithMinRange(self):
1✔
313
        catalog = self._make_one()
1✔
314
        a = catalog(att3={'query': 'att', 'range': 'min'})
1✔
315
        self.assertEqual(len(a), self.upper)
1✔
316

317
    def testKeywordIndexWithMaxRange(self):
1✔
318
        catalog = self._make_one()
1✔
319
        a = catalog(att3={'query': 'att35', 'range': ':max'})
1✔
320
        self.assertEqual(len(a), self.upper)
1✔
321

322
    def testKeywordIndexWithMinMaxRangeCorrectSyntax(self):
1✔
323
        catalog = self._make_one()
1✔
324
        a = catalog(att3={'query': ['att', 'att35'], 'range': 'min:max'})
1✔
325
        self.assertEqual(len(a), self.upper)
1✔
326

327
    def testKeywordIndexWithMinMaxRangeWrongSyntax(self):
1✔
328
        # checkKeywordIndex with min/max range wrong syntax.
329
        catalog = self._make_one()
1✔
330
        a = catalog(att3={'query': ['att'], 'range': 'min:max'})
1✔
331
        self.assertNotEqual(len(a), self.upper)
1✔
332

333
    def testCombinedTextandKeywordQuery(self):
1✔
334
        catalog = self._make_one()
1✔
335
        a = catalog(att3='att3', att2='att2')
1✔
336
        self.assertEqual(len(a), self.upper)
1✔
337
        a = catalog(att3='att3', att2='none')
1✔
338
        self.assertEqual(len(a), 0)
1✔
339

340
    def testResultLength(self):
1✔
341
        catalog = self._make_one()
1✔
342
        a = catalog(att1='att1')
1✔
343
        self.assertEqual(
1✔
344
            len(a), self.upper,
345
            f'length should be {self.upper}, its {len(a)}'
346
        )
347

348
    def test_query_empty(self):
1✔
349
        # Queries with empty mappings used to return all.
350
        def extra(catalog):
1✔
351
            col1 = FieldIndex('col1')
1✔
352
            catalog.addIndex('col1', col1)
1✔
353
        catalog = self._make_one(extra=extra)
1✔
354
        self.assertGreater(len(catalog), 0)
1✔
355
        all_data = catalog({})
1✔
356
        self.assertEqual(len(all_data), 0)
1✔
357

358
    def test_query_empty_keys(self):
1✔
359
        # Queries with empty keys used to return all.
360
        def extra(catalog):
1✔
361
            col1 = FieldIndex('col1')
1✔
362
            catalog.addIndex('col1', col1)
1✔
363
        catalog = self._make_one(extra=extra)
1✔
364
        a = catalog({'col1': ''})
1✔
365
        self.assertEqual(
1✔
366
            len(a), 0,
367
            f'length should be 0, its {len(a)}'
368
        )
369

370
    def test_field_index_length(self):
1✔
371
        catalog = self._make_one()
1✔
372
        a = catalog(att1='att1')
1✔
373
        self.assertEqual(len(a), self.upper)
1✔
374
        a = catalog(att1='none')
1✔
375
        self.assertEqual(len(a), 0)
1✔
376

377
    def test_text_index_length(self):
1✔
378
        catalog = self._make_one()
1✔
379
        a = catalog(att2='att2')
1✔
380
        self.assertEqual(len(a), self.upper)
1✔
381
        a = catalog(att2='none')
1✔
382
        self.assertEqual(len(a), 0)
1✔
383

384
    def test_keyword_index_length(self):
1✔
385
        catalog = self._make_one()
1✔
386
        a = catalog(att3='att3')
1✔
387
        self.assertEqual(len(a), self.upper)
1✔
388
        a = catalog(att3='none')
1✔
389
        self.assertEqual(len(a), 0)
1✔
390

391
    def test_keyword_index_index_not_query_order(self):
1✔
392
        # Queries with empty keys used to return all.
393
        from Products.ZCatalog.Catalog import Catalog
1✔
394
        catalog = Catalog()
1✔
395
        catalog.addIndex('keywords', KeywordIndex('keywords'))
1✔
396
        catalog.addIndex('field', FieldIndex('field'))
1✔
397

398
        class DummyKWNot(ExtensionClass.Base):
1✔
399
            field = 'foo'
1✔
400

401
            def __init__(self, keywords=None):
1✔
402
                if keywords:
1✔
403
                    self.keywords = keywords
1✔
404

405
        catalog.catalogObject(DummyKWNot([10, 11, 12]), "1")
1✔
406
        catalog.catalogObject(DummyKWNot([11, 12]), "2")
1✔
407
        catalog.catalogObject(DummyKWNot(), "3")
1✔
408

409
        from Products.ZCatalog.plan import CatalogPlan
1✔
410
        from unittest.mock import patch
1✔
411

412
        with patch.object(
1✔
413
                CatalogPlan, "plan", return_value=["field", "keywords"]):
414
            a = catalog({'keywords': {"not": [10]}, 'field': 'foo'})
1✔
415
            self.assertEqual(len(a), 2)
1✔
416

417
        with patch.object(
1✔
418
                CatalogPlan, "plan", return_value=["keywords", "field"]):
419
            a = catalog({'keywords': {"not": [10]}, 'field': 'foo'})
1✔
420
            self.assertEqual(len(a), 2)
1✔
421

422

423
class TestCatalogSortBatch(unittest.TestCase):
1✔
424

425
    upper = 100
1✔
426

427
    nums = list(range(upper))
1✔
428
    for i in range(upper):
1✔
429
        j = random.randrange(0, upper)
1✔
430
        tmp = nums[i]
1✔
431
        nums[i] = nums[j]
1✔
432
        nums[j] = tmp
1✔
433

434
    def _make_one(self, extra=None):
1✔
435
        from Products.ZCatalog.Catalog import Catalog
1✔
436
        catalog = Catalog()
1✔
437
        catalog.lexicon = PLexicon('lexicon')
1✔
438
        att1 = FieldIndex('att1')
1✔
439
        att2 = ZCTextIndex('att2', caller=catalog,
1✔
440
                           index_factory=OkapiIndex, lexicon_id='lexicon')
441
        catalog.addIndex('att2', att2)
1✔
442
        num = FieldIndex('num')
1✔
443

444
        catalog.addIndex('att1', att1)
1✔
445
        catalog.addIndex('num', num)
1✔
446
        catalog.addColumn('num')
1✔
447

448
        foo = MultiFieldIndex('foo')
1✔
449
        catalog.addIndex('foo', foo)
1✔
450

451
        if extra is not None:
1✔
452
            extra(catalog)
1✔
453

454
        for x in range(0, self.upper):
1✔
455
            catalog.catalogObject(Dummy(self.nums[x]), repr(x))
1✔
456
        return catalog.__of__(Dummy('foo'))
1✔
457

458
    def test_sorted_search_indexes_empty(self):
1✔
459
        catalog = self._make_one()
1✔
460
        result = catalog._sorted_search_indexes({})
1✔
461
        self.assertEqual(len(result), 0)
1✔
462

463
    def test_sorted_search_indexes_one(self):
1✔
464
        catalog = self._make_one()
1✔
465
        result = catalog._sorted_search_indexes({'att1': 'a'})
1✔
466
        self.assertEqual(result, ['att1'])
1✔
467

468
    def test_sorted_search_indexes_many(self):
1✔
469
        catalog = self._make_one()
1✔
470
        query = {'att1': 'a', 'att2': 'b', 'num': 1}
1✔
471
        result = catalog._sorted_search_indexes(query)
1✔
472
        self.assertEqual(set(result), {'att1', 'att2', 'num'})
1✔
473

474
    def test_sorted_search_indexes_priority(self):
1✔
475
        # att2 and col2 don't support ILimitedResultIndex, att1 does
476
        catalog = self._make_one()
1✔
477
        query = {'att1': 'a', 'att2': 'b', 'col2': 'c'}
1✔
478
        result = catalog._sorted_search_indexes(query)
1✔
479
        self.assertEqual(result.index('att2'), 0)
1✔
480
        self.assertEqual(result.index('att1'), 1)
1✔
481

482
    def test_sorted_search_indexes_match_alternate_attr(self):
1✔
483
        catalog = self._make_one()
1✔
484
        query = {'bar': 'b'}
1✔
485
        result = catalog._sorted_search_indexes(query)
1✔
486
        self.assertEqual(result, ['foo'])
1✔
487

488
    def test_sorted_search_indexes_no_match(self):
1✔
489
        catalog = self._make_one()
1✔
490
        result = catalog._sorted_search_indexes({'baz': 'a'})
1✔
491
        self.assertEqual(result, [])
1✔
492

493
    def test_sortResults(self):
1✔
494
        catalog = self._make_one()
1✔
495
        brains = catalog({'att1': 'att1'})
1✔
496
        rs = IISet([b.getRID() for b in brains])
1✔
497
        si = catalog.getIndex('num')
1✔
498
        result = catalog.sortResults(rs, si)
1✔
499
        self.assertEqual([r.num for r in result], list(range(100)))
1✔
500

501
    def test_sortResults_reversed(self):
1✔
502
        catalog = self._make_one()
1✔
503
        brains = catalog({'att1': 'att1'})
1✔
504
        rs = IISet([b.getRID() for b in brains])
1✔
505
        si = catalog.getIndex('num')
1✔
506
        result = catalog.sortResults(rs, si, reverse=True)
1✔
507
        self.assertEqual([r.num for r in result], list(reversed(range(100))))
1✔
508

509
    def test_sortResults_limit(self):
1✔
510
        catalog = self._make_one()
1✔
511
        brains = catalog({'att1': 'att1'})
1✔
512
        rs = IISet([b.getRID() for b in brains])
1✔
513
        si = catalog.getIndex('num')
1✔
514
        result = catalog.sortResults(rs, si, limit=10)
1✔
515
        self.assertEqual(len(result), 10)
1✔
516
        self.assertEqual(result.actual_result_count, 100)
1✔
517
        self.assertEqual([r.num for r in result], list(range(10)))
1✔
518

519
    def test_sortResults_limit_reversed(self):
1✔
520
        catalog = self._make_one()
1✔
521
        brains = catalog({'att1': 'att1'})
1✔
522
        rs = IISet([b.getRID() for b in brains])
1✔
523
        si = catalog.getIndex('num')
1✔
524
        result = catalog.sortResults(rs, si, reverse=True, limit=10)
1✔
525
        self.assertEqual(len(result), 10)
1✔
526
        self.assertEqual(result.actual_result_count, 100)
1✔
527
        expected = list(reversed(range(90, 100)))
1✔
528
        self.assertEqual([r.num for r in result], expected)
1✔
529

530
    def testLargeSortedResultSetWithSmallIndex(self):
1✔
531
        # This exercises the optimization in the catalog that iterates
532
        # over the sort index rather than the result set when the result
533
        # set is much larger than the sort index.
534
        catalog = self._make_one()
1✔
535
        a = catalog(att1='att1', sort_on='att1')
1✔
536
        self.assertEqual(len(a), self.upper)
1✔
537
        self.assertEqual(a.actual_result_count, self.upper)
1✔
538

539
    def testSortLimit(self):
1✔
540
        catalog = self._make_one()
1✔
541
        full = list(catalog(att1='att1', sort_on='num'))
1✔
542
        a = catalog(att1='att1', sort_on='num', sort_limit=10)
1✔
543
        self.assertEqual([r.num for r in a], [r.num for r in full[:10]])
1✔
544
        self.assertEqual(a.actual_result_count, self.upper)
1✔
545
        a = catalog(att1='att1', sort_on='num',
1✔
546
                    sort_limit=10, sort_order='reverse')
547
        rev = [r.num for r in full[-10:]]
1✔
548
        rev.reverse()
1✔
549
        self.assertEqual([r.num for r in a], rev)
1✔
550
        self.assertEqual(a.actual_result_count, self.upper)
1✔
551

552
    def testBigSortLimit(self):
1✔
553
        catalog = self._make_one()
1✔
554
        a = catalog(
1✔
555
            att1='att1', sort_on='num', sort_limit=self.upper * 3)
556
        self.assertEqual(a.actual_result_count, self.upper)
1✔
557
        self.assertEqual(a[0].num, 0)
1✔
558
        a = catalog(att1='att1', sort_on='num',
1✔
559
                    sort_limit=self.upper * 3, sort_order='reverse')
560
        self.assertEqual(a.actual_result_count, self.upper)
1✔
561
        self.assertEqual(a[0].num, self.upper - 1)
1✔
562

563
    def testSortLimitViaBatchingArgsBeforeStart(self):
1✔
564
        catalog = self._make_one()
1✔
565
        query = dict(att1='att1', sort_on='num', b_start=-5, b_size=8)
1✔
566
        result = catalog(query)
1✔
567
        self.assertEqual(result.actual_result_count, 100)
1✔
568
        self.assertEqual([r.num for r in result], list(range(0, 3)))
1✔
569

570
    def testSortLimitViaBatchingArgsStart(self):
1✔
571
        catalog = self._make_one()
1✔
572
        query = dict(att1='att1', sort_on='num', b_start=0, b_size=5)
1✔
573
        result = catalog(query)
1✔
574
        self.assertEqual(result.actual_result_count, 100)
1✔
575
        self.assertEqual([r.num for r in result], list(range(0, 5)))
1✔
576

577
    def testSortLimitViaBatchingEarlyFirstHalf(self):
1✔
578
        catalog = self._make_one()
1✔
579
        query = dict(att1='att1', sort_on='num', b_start=11, b_size=17)
1✔
580
        result = catalog(query)
1✔
581
        self.assertEqual(result.actual_result_count, 100)
1✔
582
        self.assertEqual([r.num for r in result], list(range(11, 28)))
1✔
583

584
    def testSortLimitViaBatchingArgsLateFirstHalf(self):
1✔
585
        catalog = self._make_one()
1✔
586
        query = dict(att1='att1', sort_on='num', b_start=30, b_size=15)
1✔
587
        result = catalog(query)
1✔
588
        self.assertEqual(result.actual_result_count, 100)
1✔
589
        self.assertEqual([r.num for r in result], list(range(30, 45)))
1✔
590

591
    def testSortLimitViaBatchingArgsLeftMiddle(self):
1✔
592
        catalog = self._make_one()
1✔
593
        query = dict(att1='att1', sort_on='num', b_start=45, b_size=8)
1✔
594
        result = catalog(query)
1✔
595
        self.assertEqual(result.actual_result_count, 100)
1✔
596
        self.assertEqual([r.num for r in result], list(range(45, 53)))
1✔
597

598
    def testSortLimitViaBatchingArgsRightMiddle(self):
1✔
599
        catalog = self._make_one()
1✔
600
        query = dict(att1='att1', sort_on='num', b_start=48, b_size=8)
1✔
601
        result = catalog(query)
1✔
602
        self.assertEqual(result.actual_result_count, 100)
1✔
603
        self.assertEqual([r.num for r in result], list(range(48, 56)))
1✔
604

605
    def testSortLimitViaBatchingArgsRightMiddleSortOnTwoSecond(self):
1✔
606
        catalog = self._make_one()
1✔
607
        query = dict(att1='att1', sort_on=('att1', 'num'),
1✔
608
                     sort_order=('', 'reverse'), b_start=48, b_size=8)
609
        result = catalog(query)
1✔
610
        self.assertEqual(result.actual_result_count, 100)
1✔
611
        self.assertEqual([r.num for r in result], list(range(51, 43, -1)))
1✔
612

613
    def testSortLimitViaBatchingArgsEarlySecondHalf(self):
1✔
614
        catalog = self._make_one()
1✔
615
        query = dict(att1='att1', sort_on='num', b_start=55, b_size=15)
1✔
616
        result = catalog(query)
1✔
617
        self.assertEqual(result.actual_result_count, 100)
1✔
618
        self.assertEqual([r.num for r in result], list(range(55, 70)))
1✔
619

620
    def testSortLimitViaBatchingArgsEarlySecondHalfSortOnTwoFirst(self):
1✔
621
        catalog = self._make_one()
1✔
622
        query = dict(att1='att1', sort_on=('att1', 'num'),
1✔
623
                     sort_order=('reverse', ''), b_start=55, b_size=15)
624
        result = catalog(query)
1✔
625
        self.assertEqual(result.actual_result_count, 100)
1✔
626
        self.assertEqual([r.num for r in result], list(range(55, 70)))
1✔
627

628
    def testSortLimitViaBatchingArgsEarlySecondHalfSortOnTwoSecond(self):
1✔
629
        catalog = self._make_one()
1✔
630
        query = dict(att1='att1', sort_on=('att1', 'num'),
1✔
631
                     sort_order=('', 'reverse'), b_start=55, b_size=15)
632
        result = catalog(query)
1✔
633
        self.assertEqual(result.actual_result_count, 100)
1✔
634
        self.assertEqual([r.num for r in result], list(range(44, 29, -1)))
1✔
635

636
    def testSortLimitViaBatchingArgsEarlySecondHalfSortOnTwoBoth(self):
1✔
637
        catalog = self._make_one()
1✔
638
        query = dict(att1='att1', sort_on=('att1', 'num'),
1✔
639
                     sort_order=('reverse', 'reverse'), b_start=55, b_size=15)
640
        result = catalog(query)
1✔
641
        self.assertEqual(result.actual_result_count, 100)
1✔
642
        self.assertEqual([r.num for r in result], list(range(44, 29, -1)))
1✔
643

644
    def testSortLimitViaBatchingArgsSecondHalf(self):
1✔
645
        catalog = self._make_one()
1✔
646
        query = dict(att1='att1', sort_on='num', b_start=70, b_size=15)
1✔
647
        result = catalog(query)
1✔
648
        self.assertEqual(result.actual_result_count, 100)
1✔
649
        self.assertEqual([r.num for r in result], list(range(70, 85)))
1✔
650

651
    def testSortLimitViaBatchingArgsEnd(self):
1✔
652
        catalog = self._make_one()
1✔
653
        query = dict(att1='att1', sort_on='num', b_start=90, b_size=10)
1✔
654
        result = catalog(query)
1✔
655
        self.assertEqual(result.actual_result_count, 100)
1✔
656
        self.assertEqual([r.num for r in result], list(range(90, 100)))
1✔
657

658
    def testSortLimitViaBatchingArgsOverEnd(self):
1✔
659
        catalog = self._make_one()
1✔
660
        query = dict(att1='att1', sort_on='num', b_start=90, b_size=15)
1✔
661
        result = catalog(query)
1✔
662
        self.assertEqual(result.actual_result_count, 100)
1✔
663
        self.assertEqual([r.num for r in result], list(range(90, 100)))
1✔
664

665
    def testSortLimitViaBatchingArgsOutside(self):
1✔
666
        catalog = self._make_one()
1✔
667
        query = dict(att1='att1', sort_on='num', b_start=110, b_size=10)
1✔
668
        result = catalog(query)
1✔
669
        self.assertEqual(result.actual_result_count, 100)
1✔
670
        self.assertEqual([r.num for r in result], [])
1✔
671

672
    def testSortedResultLengthWithMissingDocs(self):
1✔
673
        catalog = self._make_one()
1✔
674
        # remove the `0` document from the num index only
675
        num_index = catalog.getIndex('num')
1✔
676
        pos_of_zero = self.nums.index(0)
1✔
677
        uid = catalog.uids.get(repr(pos_of_zero))
1✔
678
        self.assertEqual(catalog[uid].num, 0)
1✔
679
        num_index.unindex_object(uid)
1✔
680
        # make sure it was removed
681
        self.assertEqual(len(num_index), 99)
1✔
682
        # sort over the smaller num index
683
        query = dict(att1='att1', sort_on='num', sort_limit=10)
1✔
684
        result = catalog(query)
1✔
685
        # the `0` document was removed
686
        self.assertEqual(result[0].num, 1)
1✔
687
        self.assertEqual(len(result), 10)
1✔
688
        # there are only 99 documents left
689
        self.assertEqual(result.actual_result_count, 99)
1✔
690

691
    # _get_sort_attr
692
    # _getSortIndex
693

694
    def test_search_not(self):
1✔
695
        catalog = self._make_one()
1✔
696
        query = dict(att1='att1', num={'not': [0, 1]})
1✔
697
        result = catalog(query)
1✔
698
        self.assertEqual(len(result), self.upper - 2)
1✔
699

700
    def test_search_not_nothing(self):
1✔
701
        def extra(catalog):
1✔
702
            col1 = FieldIndex('col1')
1✔
703
            catalog.addIndex('col1', col1)
1✔
704
        catalog = self._make_one(extra)
1✔
705
        query = dict(att1='att1', col1={'not': 'col1'})
1✔
706
        result = catalog(query)
1✔
707
        self.assertEqual(len(result), 0)
1✔
708

709
    def test_search_not_no_value_in_index(self):
1✔
710
        def extra(catalog):
1✔
711
            ends_in_zero = FieldIndex('ends_in_zero')
1✔
712
            catalog.addIndex('ends_in_zero', ends_in_zero)
1✔
713
        catalog = self._make_one(extra=extra)
1✔
714
        query = dict(att1='att1', ends_in_zero={'not': False})
1✔
715
        result = catalog(query)
1✔
716
        self.assertEqual(len(result), 10)
1✔
717

718
    def test_sort_on_good_index(self):
1✔
719
        catalog = self._make_one()
1✔
720
        upper = self.upper
1✔
721
        a = catalog(att1='att1', sort_on='num')
1✔
722
        self.assertEqual(len(a), upper)
1✔
723
        for x in range(self.upper):
1✔
724
            self.assertEqual(a[x].num, x)
1✔
725

726
    def test_sort_on_bad_index(self):
1✔
727
        from Products.ZCatalog.Catalog import CatalogError
1✔
728
        catalog = self._make_one()
1✔
729

730
        def badsortindex():
1✔
731
            catalog(sort_on='foofaraw')
1✔
732
        self.assertRaises(CatalogError, badsortindex)
1✔
733

734
    def test_sort_on_wrong_index(self):
1✔
735
        from Products.ZCatalog.Catalog import CatalogError
1✔
736
        catalog = self._make_one()
1✔
737

738
        def wrongsortindex():
1✔
739
            catalog(sort_on='att2')
1✔
740
        self.assertRaises(CatalogError, wrongsortindex)
1✔
741

742
    def test_sort_on(self):
1✔
743
        catalog = self._make_one()
1✔
744
        upper = self.upper
1✔
745
        a = catalog(sort_on='num', att2='att2')
1✔
746
        self.assertEqual(len(a), upper)
1✔
747
        for x in range(self.upper):
1✔
748
            self.assertEqual(a[x].num, x)
1✔
749

750
    def test_sort_on_missing(self):
1✔
751
        catalog = self._make_one()
1✔
752
        upper = self.upper
1✔
753
        a = catalog(att2='att2')
1✔
754
        self.assertEqual(len(a), upper)
1✔
755

756
    def test_sort_on_two(self):
1✔
757
        catalog = self._make_one()
1✔
758
        upper = self.upper
1✔
759
        a = catalog(sort_on=('att1', 'num'), att1='att1')
1✔
760
        self.assertEqual(len(a), upper)
1✔
761
        for x in range(self.upper):
1✔
762
            self.assertEqual(a[x].num, x)
1✔
763

764
    def test_sort_on_two_reverse(self):
1✔
765
        catalog = self._make_one()
1✔
766
        upper = self.upper
1✔
767
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
768
                    sort_order='reverse')
769
        self.assertEqual(len(a), upper)
1✔
770
        for x in range(upper - 1):
1✔
771
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
772

773
    def test_sort_on_two_reverse_with_limit(self):
1✔
774
        catalog = self._make_one()
1✔
775
        for num in range(-10, 0):
1✔
776
            obj = Dummy(num)
1✔
777
            obj.att1 = "att1foo"
1✔
778
            obj.att2 = "att2"
1✔
779
            catalog.catalogObject(obj, repr(num))
1✔
780
        a = catalog(
1✔
781
            att2='att2',
782
            sort_on=('att1', 'num'),
783
            sort_order='reverse',
784
            sort_limit=20,
785
        )
786
        self.assertEqual(
1✔
787
            [x.num for x in a[:10]],
788
            [-1, -2, -3, -4, -5, -6, -7, -8, -9, -10],
789
        )
790
        self.assertEqual(
1✔
791
            [x.num for x in a[10:]],
792
            [99, 98, 97, 96, 95, 94, 93, 92, 91, 90],
793
        )
794

795
    def test_sort_on_two_reverse_neither(self):
1✔
796
        catalog = self._make_one()
1✔
797
        upper = self.upper
1✔
798
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
799
                    sort_order=('', ''))
800
        self.assertEqual(len(a), upper)
1✔
801
        for x in range(upper - 1):
1✔
802
            self.assertLess(a[x].num, a[x + 1].num)
1✔
803

804
    def test_sort_on_two_reverse_first(self):
1✔
805
        catalog = self._make_one()
1✔
806
        upper = self.upper
1✔
807
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
808
                    sort_order=('reverse', ''))
809
        self.assertEqual(len(a), upper)
1✔
810
        for x in range(upper - 1):
1✔
811
            self.assertLess(a[x].num, a[x + 1].num)
1✔
812

813
    def test_sort_on_two_reverse_second(self):
1✔
814
        catalog = self._make_one()
1✔
815
        upper = self.upper
1✔
816
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
817
                    sort_order=('', 'reverse'))
818
        self.assertEqual(len(a), upper)
1✔
819
        for x in range(upper - 1):
1✔
820
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
821

822
    def test_sort_on_two_reverse_both(self):
1✔
823
        catalog = self._make_one()
1✔
824
        upper = self.upper
1✔
825
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
826
                    sort_order=('reverse', 'reverse'))
827
        self.assertEqual(len(a), upper)
1✔
828
        for x in range(upper - 1):
1✔
829
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
830

831
    def test_sort_on_two_reverse_too_many(self):
1✔
832
        catalog = self._make_one()
1✔
833
        upper = self.upper
1✔
834
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
835
                    sort_order=('', '', 'reverse', ''))
836
        self.assertEqual(len(a), upper)
1✔
837
        for x in range(upper - 1):
1✔
838
            self.assertLess(a[x].num, a[x + 1].num)
1✔
839

840
    def test_sort_on_two_small_limit(self):
1✔
841
        catalog = self._make_one()
1✔
842
        a = catalog(sort_on=('att1', 'num'), att1='att1', sort_limit=10)
1✔
843
        self.assertEqual(len(a), 10)
1✔
844
        for x in range(9):
1✔
845
            self.assertLess(a[x].num, a[x + 1].num)
1✔
846

847
    def test_sort_on_two_small_limit_reverse(self):
1✔
848
        catalog = self._make_one()
1✔
849
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
850
                    sort_limit=10, sort_order='reverse')
851
        self.assertEqual(len(a), 10)
1✔
852
        for x in range(9):
1✔
853
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
854

855
    def test_sort_on_two_big_limit(self):
1✔
856
        catalog = self._make_one()
1✔
857
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
858
                    sort_limit=self.upper * 3)
859
        self.assertEqual(len(a), 100)
1✔
860
        for x in range(99):
1✔
861
            self.assertLess(a[x].num, a[x + 1].num)
1✔
862

863
    def test_sort_on_two_big_limit_reverse(self):
1✔
864
        catalog = self._make_one()
1✔
865
        a = catalog(sort_on=('att1', 'num'), att1='att1',
1✔
866
                    sort_limit=self.upper * 3, sort_order='reverse')
867
        self.assertEqual(len(a), 100)
1✔
868
        for x in range(99):
1✔
869
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
870

871
    def test_sort_on_three(self):
1✔
872
        def extra(catalog):
1✔
873
            col2 = FieldIndex('col2')
1✔
874
            catalog.addIndex('col2', col2)
1✔
875
        catalog = self._make_one(extra)
1✔
876
        a = catalog(sort_on=('att1', 'col2', 'num'), att1='att1')
1✔
877
        self.assertEqual(len(a), self.upper)
1✔
878
        for x in range(self.upper):
1✔
879
            self.assertEqual(a[x].num, x)
1✔
880

881
    def test_sort_on_three_reverse(self):
1✔
882
        def extra(catalog):
1✔
883
            col2 = FieldIndex('col2')
1✔
884
            catalog.addIndex('col2', col2)
1✔
885
        catalog = self._make_one(extra)
1✔
886
        a = catalog(sort_on=('att1', 'col2', 'num'), att1='att1',
1✔
887
                    sort_order='reverse')
888
        self.assertEqual(len(a), self.upper)
1✔
889
        for x in range(self.upper - 1):
1✔
890
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
891

892
    def test_sort_on_three_reverse_last(self):
1✔
893
        def extra(catalog):
1✔
894
            col2 = FieldIndex('col2')
1✔
895
            catalog.addIndex('col2', col2)
1✔
896
        catalog = self._make_one(extra)
1✔
897
        a = catalog(sort_on=('att1', 'col2', 'num'), att1='att1',
1✔
898
                    sort_order=('', '', 'reverse'))
899
        self.assertEqual(len(a), self.upper)
1✔
900
        for x in range(self.upper - 1):
1✔
901
            self.assertGreater(a[x].num, a[x + 1].num)
1✔
902

903
    def test_sort_on_three_small_limit(self):
1✔
904
        def extra(catalog):
1✔
905
            col2 = FieldIndex('col2')
1✔
906
            catalog.addIndex('col2', col2)
1✔
907
        catalog = self._make_one(extra)
1✔
908
        a = catalog(sort_on=('att1', 'col2', 'num'), att1='att1',
1✔
909
                    sort_limit=10)
910
        self.assertEqual(len(a), 10)
1✔
911
        for x in range(9):
1✔
912
            self.assertLess(a[x].num, a[x + 1].num)
1✔
913

914
    def test_sort_on_three_big_limit(self):
1✔
915
        def extra(catalog):
1✔
916
            col2 = FieldIndex('col2')
1✔
917
            catalog.addIndex('col2', col2)
1✔
918
        catalog = self._make_one(extra)
1✔
919
        a = catalog(sort_on=('att1', 'col2', 'num'), att1='att1',
1✔
920
                    sort_limit=self.upper * 3)
921
        self.assertEqual(len(a), 100)
1✔
922
        for x in range(99):
1✔
923
            self.assertLess(a[x].num, a[x + 1].num)
1✔
924

925

926
class TestUnCatalog(unittest.TestCase):
1✔
927

928
    upper = 5
1✔
929

930
    def _make_one(self):
1✔
931
        from Products.ZCatalog.Catalog import Catalog
1✔
932
        catalog = Catalog()
1✔
933
        catalog.lexicon = PLexicon('lexicon')
1✔
934
        att1 = FieldIndex('att1')
1✔
935
        att2 = ZCTextIndex('att2', caller=catalog,
1✔
936
                           index_factory=OkapiIndex, lexicon_id='lexicon')
937
        att3 = KeywordIndex('att3')
1✔
938
        catalog.addIndex('att1', att1)
1✔
939
        catalog.addIndex('att2', att2)
1✔
940
        catalog.addIndex('att3', att3)
1✔
941

942
        for x in range(0, self.upper):
1✔
943
            catalog.catalogObject(Dummy(x), repr(x))
1✔
944
        return catalog.__of__(Dummy('foo'))
1✔
945

946
    def _uncatalog(self, catalog):
1✔
947
        for x in range(0, self.upper):
1✔
948
            catalog.uncatalogObject(repr(x))
1✔
949

950
    def test_uncatalog_field_index(self):
1✔
951
        catalog = self._make_one()
1✔
952
        self._uncatalog(catalog)
1✔
953
        a = catalog(att1='att1')
1✔
954
        self.assertEqual(len(a), 0, f'len: {len(a)}')
1✔
955

956
    def test_uncatalog_text_index(self):
1✔
957
        catalog = self._make_one()
1✔
958
        self._uncatalog(catalog)
1✔
959
        a = catalog(att2='att2')
1✔
960
        self.assertEqual(len(a), 0, f'len: {len(a)}')
1✔
961

962
    def test_uncatalog_keyword_index(self):
1✔
963
        catalog = self._make_one()
1✔
964
        self._uncatalog(catalog)
1✔
965
        a = catalog(att3='att3')
1✔
966
        self.assertEqual(len(a), 0, f'len: {len(a)}')
1✔
967

968
    def test_bad_uncatalog(self):
1✔
969
        catalog = self._make_one()
1✔
970
        try:
1✔
971
            catalog.uncatalogObject('asdasdasd')
1✔
UNCOV
972
        except Exception:
×
973
            self.fail('uncatalogObject raised exception on bad uid')
974

975
    def test_uncatalog_twice(self):
1✔
976
        catalog = self._make_one()
1✔
977
        catalog.uncatalogObject('0')
1✔
978

979
        def _second(self):
1✔
UNCOV
980
            catalog.uncatalogObject('0')
×
981
        self.assertRaises(Exception, _second)
1✔
982

983
    def test_uncatalog_ength(self):
1✔
984
        catalog = self._make_one()
1✔
985
        self._uncatalog(catalog)
1✔
986
        self.assertEqual(len(catalog), 0)
1✔
987

988

989
class TestRangeSearch(unittest.TestCase):
1✔
990

991
    def _make_one(self):
1✔
992
        from Products.ZCatalog.Catalog import Catalog
1✔
993
        return Catalog()
1✔
994

995
    def test_range_search(self):
1✔
996
        catalog = self._make_one()
1✔
997
        index = FieldIndex('number')
1✔
998
        catalog.addIndex('number', index)
1✔
999
        catalog.addColumn('number')
1✔
1000
        for i in range(50):
1✔
1001
            obj = ObjRS(random.randrange(0, 200))
1✔
1002
            catalog.catalogObject(obj, i)
1✔
1003
        catalog = catalog.__of__(ObjRS(20))
1✔
1004

1005
        for i in range(10):
1✔
1006
            m = random.randrange(0, 200)
1✔
1007
            n = m + 10
1✔
1008
            for r in catalog(number={'query': (m, n), 'range': 'min:max'}):
1✔
1009
                size = r.number
1✔
1010
                self.assertTrue(
1✔
1011
                    m <= size and size <= n,
1012
                    f'{r.number:d} vs [{m:d},{n:d}]'
1013
                )
1014

1015

1016
class TestMergeResults(unittest.TestCase):
1✔
1017

1018
    def _make_one(self):
1✔
1019
        from Products.ZCatalog.Catalog import Catalog
1✔
1020
        return Catalog()
1✔
1021

1022
    def _make_many(self):
1✔
1023
        from Products.ZCatalog.Catalog import mergeResults
1✔
1024
        catalogs = []
1✔
1025
        for i in range(3):
1✔
1026
            cat = self._make_one()
1✔
1027
            cat.lexicon = PLexicon('lexicon')
1✔
1028
            cat.addIndex('num', FieldIndex('num'))
1✔
1029
            cat.addIndex('big', FieldIndex('big'))
1✔
1030
            cat.addIndex('number', FieldIndex('number'))
1✔
1031
            i = ZCTextIndex('title', caller=cat, index_factory=OkapiIndex,
1✔
1032
                            lexicon_id='lexicon')
1033
            cat.addIndex('title', i)
1✔
1034
            cat = cat.__of__(ZDummy(16336))
1✔
1035
            for i in range(10):
1✔
1036
                obj = ZDummy(i)
1✔
1037
                obj.big = i > 5
1✔
1038
                obj.number = True
1✔
1039
                cat.catalogObject(obj, str(i))
1✔
1040
            catalogs.append(cat)
1✔
1041
        return catalogs, mergeResults
1✔
1042

1043
    def _sort(self, iterable, reverse=False):
1✔
1044
        L = list(iterable)
1✔
1045
        if reverse:
1✔
1046
            L.sort(reverse=True)
1✔
1047
        else:
1048
            L.sort()
1✔
1049
        return L
1✔
1050

1051
    def test_no_filter_or_sort(self):
1✔
1052
        catalogs, mergeResults = self._make_many()
1✔
1053
        results = [cat.searchResults(
1✔
1054
                   dict(number=True), _merge=0) for cat in catalogs]
1055
        merged_rids = [r.getRID() for r in mergeResults(
1✔
1056
            results, has_sort_keys=False, reverse=False)]
1057
        expected = [r.getRID() for r in chain(*results)]
1✔
1058
        self.assertEqual(self._sort(merged_rids), self._sort(expected))
1✔
1059

1060
    def test_sorted_only(self):
1✔
1061
        catalogs, mergeResults = self._make_many()
1✔
1062
        results = [cat.searchResults(
1✔
1063
                   dict(number=True, sort_on='num'), _merge=0)
1064
                   for cat in catalogs]
1065
        merged_rids = [r.getRID() for r in mergeResults(
1✔
1066
            results, has_sort_keys=True, reverse=False)]
1067
        expected = self._sort(chain(*results))
1✔
1068
        expected = [rid for sortkey, rid, getitem in expected]
1✔
1069
        self.assertEqual(merged_rids, expected)
1✔
1070

1071
    def test_sort_reverse(self):
1✔
1072
        catalogs, mergeResults = self._make_many()
1✔
1073
        results = [cat.searchResults(
1✔
1074
                   dict(number=True, sort_on='num'), _merge=0)
1075
                   for cat in catalogs]
1076
        merged_rids = [r.getRID() for r in mergeResults(
1✔
1077
            results, has_sort_keys=True, reverse=True)]
1078
        expected = self._sort(chain(*results), reverse=True)
1✔
1079
        expected = [rid for sortkey, rid, getitem in expected]
1✔
1080
        self.assertEqual(merged_rids, expected)
1✔
1081

1082
    def test_limit_sort(self):
1✔
1083
        catalogs, mergeResults = self._make_many()
1✔
1084
        results = [cat.searchResults(
1✔
1085
                   dict(att1='att1', number=True, sort_on='num',
1086
                        sort_limit=2), _merge=0)
1087
                   for cat in catalogs]
1088
        merged_rids = [r.getRID() for r in mergeResults(
1✔
1089
            results, has_sort_keys=True, reverse=False)]
1090
        expected = self._sort(chain(*results))
1✔
1091
        expected = [rid for sortkey, rid, getitem in expected]
1✔
1092
        self.assertEqual(merged_rids, expected)
1✔
1093

1094
    def test_scored(self):
1✔
1095
        catalogs, mergeResults = self._make_many()
1✔
1096
        results = [cat.searchResults(title='4 or 5 or 6', _merge=0)
1✔
1097
                   for cat in catalogs]
1098
        merged_rids = [r.getRID() for r in mergeResults(
1✔
1099
            results, has_sort_keys=True, reverse=False)]
1100
        expected = self._sort(chain(*results))
1✔
1101
        expected = [rid for sortkey, (nscore, score, rid), getitem in expected]
1✔
1102
        self.assertEqual(merged_rids, expected)
1✔
1103

1104
    def test_small_index_sort(self):
1✔
1105
        # Test that small index sort optimization is not used for merging
1106
        catalogs, mergeResults = self._make_many()
1✔
1107
        results = [cat.searchResults(
1✔
1108
                   dict(number=True, sort_on='big'), _merge=0)
1109
                   for cat in catalogs]
1110
        merged_rids = [r.getRID() for r in mergeResults(
1✔
1111
            results, has_sort_keys=True, reverse=False)]
1112
        expected = self._sort(chain(*results))
1✔
1113
        expected = [rid for sortkey, rid, getitem in expected]
1✔
1114
        self.assertEqual(merged_rids, expected)
1✔
1115

1116

1117
class TestScoring(unittest.TestCase):
1✔
1118

1119
    def _make_one(self):
1✔
1120
        from Products.ZCatalog.Catalog import Catalog
1✔
1121
        catalog = Catalog()
1✔
1122
        catalog.lexicon = PLexicon('lexicon')
1✔
1123
        idx = ZCTextIndex('title', caller=catalog,
1✔
1124
                          index_factory=OkapiIndex, lexicon_id='lexicon')
1125
        catalog.addIndex('title', idx)
1✔
1126
        catalog.addIndex('true', FieldIndex('true'))
1✔
1127
        catalog.addColumn('title')
1✔
1128
        for i in (1, 2, 3, 10, 11, 110, 111):
1✔
1129
            obj = ZDummy(i)
1✔
1130
            obj.true = True
1✔
1131
            if i == 110:
1✔
1132
                obj.true = False
1✔
1133
            catalog.catalogObject(obj, str(i))
1✔
1134
        return catalog.__of__(ZDummy(1))
1✔
1135

1136
    def test_simple_search(self):
1✔
1137
        cat = self._make_one()
1✔
1138
        brains = cat(title='10')
1✔
1139
        self.assertEqual(len(brains), 1)
1✔
1140
        self.assertEqual(brains[0].title, '10')
1✔
1141

1142
    def test_or_search(self):
1✔
1143
        cat = self._make_one()
1✔
1144
        brains = cat(title='2 OR 3')
1✔
1145
        self.assertEqual(len(brains), 2)
1✔
1146

1147
    def test_scored_search(self):
1✔
1148
        cat = self._make_one()
1✔
1149
        brains = cat(title='1*')
1✔
1150
        self.assertEqual(len(brains), 5)
1✔
1151
        self.assertEqual(brains[0].title, '111')
1✔
1152

1153
    def test_combined_scored_search(self):
1✔
1154
        cat = self._make_one()
1✔
1155
        brains = cat(title='1*', true=True)
1✔
1156
        self.assertEqual(len(brains), 4)
1✔
1157
        self.assertEqual(brains[0].title, '111')
1✔
1158

1159
    def test_combined_scored_search_planned(self):
1✔
1160
        from ..plan import Benchmark
1✔
1161
        from ..plan import PriorityMap
1✔
1162
        cat = self._make_one()
1✔
1163
        query = dict(title='1*', true=True)
1✔
1164
        plan = cat.getCatalogPlan()
1✔
1165
        plan_key = plan.make_key(query)
1✔
1166
        catalog_id = plan.get_id()
1✔
1167
        # plan with title first
1168
        PriorityMap.set_entry(catalog_id, plan_key, dict(
1✔
1169
            title=Benchmark(1, 1, False),
1170
            true=Benchmark(2, 1, False),
1171
        ))
1172
        brains = cat(query)
1✔
1173
        self.assertEqual(len(brains), 4)
1✔
1174
        # plan with true first
1175
        PriorityMap.set_entry(catalog_id, plan_key, dict(
1✔
1176
            title=Benchmark(2, 1, False),
1177
            true=Benchmark(1, 1, False),
1178
        ))
1179
        brains = cat(query)
1✔
1180
        self.assertEqual(len(brains), 4)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc