File: search_indexes_prose_spec.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (170 lines) | stat: -rw-r--r-- 5,148 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# frozen_string_literal: true

require 'spec_helper'

class SearchIndexHelper
  attr_reader :client, :collection_name

  def initialize(client)
    @client = client

    # https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.md#search-index-management-helpers
    # "...each test uses a randomly generated collection name.  Drivers may
    # generate this collection name however they like, but a suggested
    # implementation is a hex representation of an ObjectId..."
    @collection_name = BSON::ObjectId.new.to_s
  end

  # `soft_create` means to create the collection object without forcing it to
  # be created in the database.
  def collection(soft_create: false)
    @collection ||= client.database[collection_name].tap do |collection|
      collection.create unless soft_create
    end
  end

  # Wait for all of the indexes with the given names to be ready; then return
  # the list of index definitions corresponding to those names.
  def wait_for(*names, &condition)
    timeboxed_wait do
      result = collection.search_indexes
      return filter_results(result, names) if names.all? { |name| ready?(result, name, &condition) }
    end
  end

  # Wait until all of the indexes with the given names are absent from the
  # search index list.
  def wait_for_absense_of(*names)
    names.each do |name|
      timeboxed_wait do
        break if collection.search_indexes(name: name).empty?
      end
    end
  end

  private

  def timeboxed_wait(step: 5, max: 300)
    start = Mongo::Utils.monotonic_time

    loop do
      yield

      sleep step
      raise Timeout::Error, 'wait took too long' if Mongo::Utils.monotonic_time - start > max
    end
  end

  # Returns true if the list of search indexes includes one with the given name,
  # which is ready to be queried.
  def ready?(list, name, &condition)
    condition ||= ->(index) { index['queryable'] }
    list.any? { |index| index['name'] == name && condition[index] }
  end

  def filter_results(result, names)
    result.select { |index| names.include?(index['name']) }
  end
end

describe 'Mongo::Collection#search_indexes prose tests' do
  # https://github.com/mongodb/specifications/blob/master/source/index-management/tests/README.md#setup
  # "These tests must run against an Atlas cluster with a 7.0+ server."
  require_atlas

  let(:client) do
    Mongo::Client.new(
      ENV['ATLAS_URI'],
      database: SpecConfig.instance.test_db,
      ssl: true,
      ssl_verify: true
    )
  end

  let(:helper) { SearchIndexHelper.new(client) }

  let(:name) { 'test-search-index' }
  let(:definition) { { 'mappings' => { 'dynamic' => false } } }
  let(:create_index) { helper.collection.search_indexes.create_one(definition, name: name) }

  after do
    client.close
  end

  # Case 1: Driver can successfully create and list search indexes
  context 'when creating and listing search indexes' do
    let(:index) { helper.wait_for(name).first }

    it 'succeeds' do
      expect(create_index).to be == name
      expect(index['latestDefinition']).to be == definition
    end
  end

  # Case 2: Driver can successfully create multiple indexes in batch
  context 'when creating multiple indexes in batch' do
    let(:specs) do
      [
        { 'name' => 'test-search-index-1', 'definition' => definition },
        { 'name' => 'test-search-index-2', 'definition' => definition }
      ]
    end

    let(:names) { specs.map { |spec| spec['name'] } }
    let(:create_indexes) { helper.collection.search_indexes.create_many(specs) }

    let(:indexes) { helper.wait_for(*names) }

    let(:index1) { indexes[0] }
    let(:index2) { indexes[1] }

    it 'succeeds' do
      expect(create_indexes).to be == names
      expect(index1['latestDefinition']).to be == specs[0]['definition']
      expect(index2['latestDefinition']).to be == specs[1]['definition']
    end
  end

  # Case 3: Driver can successfully drop search indexes
  context 'when dropping search indexes' do
    it 'succeeds' do
      expect(create_index).to be == name
      helper.wait_for(name)

      helper.collection.search_indexes.drop_one(name: name)

      expect { helper.wait_for_absense_of(name) }.not_to raise_error
    end
  end

  # Case 4: Driver can update a search index
  context 'when updating search indexes' do
    let(:new_definition) { { 'mappings' => { 'dynamic' => true } } }

    let(:index) do
      helper
        .wait_for(name) { |idx| idx['queryable'] && idx['status'] == 'READY' }
        .first
    end

    it 'succeeds' do
      expect(create_index).to be == name
      helper.wait_for(name)

      expect do
        helper.collection.search_indexes.update_one(new_definition, name: name)
      end.not_to raise_error

      expect(index['latestDefinition']).to be == new_definition
    end
  end

  # Case 5: dropSearchIndex suppresses namespace not found errors
  context 'when dropping a non-existent search index' do
    it 'ignores `namespace not found` errors' do
      collection = helper.collection(soft_create: true)
      expect { collection.search_indexes.drop_one(name: name) }
        .not_to raise_error
    end
  end
end