Line data Source code
1 : // Copyright (C) 2021 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.index.testing;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 :
19 : import com.google.common.collect.ImmutableList;
20 : import com.google.common.collect.ImmutableListMultimap;
21 : import com.google.common.collect.ImmutableMap;
22 : import com.google.common.collect.Iterables;
23 : import com.google.gerrit.entities.Account;
24 : import com.google.gerrit.entities.AccountGroup;
25 : import com.google.gerrit.entities.Change;
26 : import com.google.gerrit.entities.InternalGroup;
27 : import com.google.gerrit.entities.Project;
28 : import com.google.gerrit.index.Index;
29 : import com.google.gerrit.index.QueryOptions;
30 : import com.google.gerrit.index.Schema;
31 : import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
32 : import com.google.gerrit.index.project.ProjectData;
33 : import com.google.gerrit.index.project.ProjectIndex;
34 : import com.google.gerrit.index.query.DataSource;
35 : import com.google.gerrit.index.query.FieldBundle;
36 : import com.google.gerrit.index.query.ListResultSet;
37 : import com.google.gerrit.index.query.Predicate;
38 : import com.google.gerrit.index.query.ResultSet;
39 : import com.google.gerrit.server.account.AccountState;
40 : import com.google.gerrit.server.change.MergeabilityComputationBehavior;
41 : import com.google.gerrit.server.config.GerritServerConfig;
42 : import com.google.gerrit.server.config.SitePaths;
43 : import com.google.gerrit.server.index.IndexUtils;
44 : import com.google.gerrit.server.index.account.AccountIndex;
45 : import com.google.gerrit.server.index.change.ChangeField;
46 : import com.google.gerrit.server.index.change.ChangeIndex;
47 : import com.google.gerrit.server.index.group.GroupIndex;
48 : import com.google.gerrit.server.query.change.ChangeData;
49 : import com.google.inject.Inject;
50 : import com.google.inject.assistedinject.Assisted;
51 : import java.time.Instant;
52 : import java.util.Comparator;
53 : import java.util.HashMap;
54 : import java.util.List;
55 : import java.util.Map;
56 : import java.util.stream.IntStream;
57 : import java.util.stream.Stream;
58 : import org.eclipse.jgit.annotations.Nullable;
59 : import org.eclipse.jgit.lib.Config;
60 :
61 : /**
62 : * Fake secondary index implementation for usage in tests. All values are kept in-memory.
63 : *
64 : * <p>This class is thread-safe.
65 : */
66 : public abstract class AbstractFakeIndex<K, V, D> implements Index<K, V> {
67 : private final Schema<V> schema;
68 : /**
69 : * SitePaths (config files) are used to signal that an index is ready. This implementation is
70 : * consistent with other index backends.
71 : */
72 : private final SitePaths sitePaths;
73 :
74 : private final String indexName;
75 : private final Map<K, D> indexedDocuments;
76 : private int queryCount;
77 :
78 146 : AbstractFakeIndex(Schema<V> schema, SitePaths sitePaths, String indexName) {
79 146 : this.schema = schema;
80 146 : this.sitePaths = sitePaths;
81 146 : this.indexName = indexName;
82 146 : this.indexedDocuments = new HashMap<>();
83 146 : this.queryCount = 0;
84 146 : }
85 :
86 : @Override
87 : public Schema<V> getSchema() {
88 146 : return schema;
89 : }
90 :
91 : @Override
92 : public void close() {
93 : // No-op
94 146 : }
95 :
96 : @Override
97 : public void replace(V doc) {
98 146 : synchronized (indexedDocuments) {
99 146 : indexedDocuments.put(keyFor(doc), docFor(doc));
100 146 : }
101 146 : }
102 :
103 : @Override
104 : public void delete(K key) {
105 49 : synchronized (indexedDocuments) {
106 49 : indexedDocuments.remove(key);
107 49 : }
108 49 : }
109 :
110 : @Override
111 : public void deleteAll() {
112 15 : synchronized (indexedDocuments) {
113 15 : indexedDocuments.clear();
114 15 : }
115 15 : }
116 :
117 : public int getQueryCount() {
118 2 : return queryCount;
119 : }
120 :
121 : @Override
122 : public DataSource<V> getSource(Predicate<V> p, QueryOptions opts) {
123 : List<V> results;
124 146 : synchronized (indexedDocuments) {
125 146 : Stream<V> valueStream =
126 146 : indexedDocuments.values().stream()
127 146 : .map(doc -> valueFor(doc))
128 146 : .filter(doc -> p.asMatchable().match(doc))
129 146 : .sorted(sortingComparator());
130 146 : if (opts.searchAfter() != null) {
131 2 : ImmutableList<V> valueList = valueStream.collect(toImmutableList());
132 2 : int fromIndex =
133 2 : IntStream.range(0, valueList.size())
134 2 : .filter(i -> keyFor(valueList.get(i)).equals(opts.searchAfter()))
135 2 : .findFirst()
136 2 : .orElse(-1)
137 : + 1;
138 2 : int toIndex = Math.min(fromIndex + opts.pageSize(), valueList.size());
139 2 : results = valueList.subList(fromIndex, toIndex);
140 2 : } else {
141 146 : results = valueStream.skip(opts.start()).limit(opts.pageSize()).collect(toImmutableList());
142 : }
143 146 : queryCount++;
144 146 : }
145 146 : return new DataSource<>() {
146 : @Override
147 : public int getCardinality() {
148 114 : return results.size();
149 : }
150 :
151 : @Override
152 : public ResultSet<V> read() {
153 146 : return new ListResultSet<>(results) {
154 : @Nullable
155 : @Override
156 : public Object searchAfter() {
157 100 : @Nullable V last = Iterables.getLast(results, null);
158 100 : return last != null ? keyFor(last) : null;
159 : }
160 : };
161 : }
162 :
163 : @Override
164 : public ResultSet<FieldBundle> readRaw() {
165 9 : ImmutableList.Builder<FieldBundle> fieldBundles = ImmutableList.builder();
166 9 : K searchAfter = null;
167 9 : for (V result : results) {
168 6 : ImmutableListMultimap.Builder<String, Object> fields = ImmutableListMultimap.builder();
169 6 : for (SchemaField<V, ?> field : getSchema().getSchemaFields().values()) {
170 6 : if (field.get(result) == null) {
171 4 : continue;
172 : }
173 6 : if (field.isRepeatable()) {
174 6 : fields.putAll(field.getName(), (Iterable<?>) field.get(result));
175 : } else {
176 6 : fields.put(field.getName(), field.get(result));
177 : }
178 6 : }
179 6 : fieldBundles.add(new FieldBundle(fields.build(), /* storesIndexedFields= */ false));
180 6 : searchAfter = keyFor(result);
181 6 : }
182 9 : ImmutableList<FieldBundle> resultSet = fieldBundles.build();
183 9 : K finalSearchAfter = searchAfter;
184 9 : return new ListResultSet<>(resultSet) {
185 : @Override
186 : public Object searchAfter() {
187 0 : return finalSearchAfter;
188 : }
189 : };
190 : }
191 : };
192 : }
193 :
194 : @Override
195 : public void markReady(boolean ready) {
196 16 : IndexUtils.setReady(sitePaths, indexName, schema.getVersion(), ready);
197 16 : }
198 :
199 : /** Method to get a key from a document. */
200 : protected abstract K keyFor(V doc);
201 :
202 : /** Method to get a document the index should hold on to from a Gerrit Java data type. */
203 : protected abstract D docFor(V value);
204 :
205 : /** Method to a Gerrit Java data type from a document that the index was holding on to. */
206 : protected abstract V valueFor(D doc);
207 :
208 : /** Comparator representing the default search order. */
209 : protected abstract Comparator<V> sortingComparator();
210 :
211 : /**
212 : * Fake implementation of {@link ChangeIndex} where all filtering happens in-memory.
213 : *
214 : * <p>This index is special in that ChangeData is a mutable object. Therefore we can't just hold
215 : * onto the object that the caller wanted us to index. We also can't just create a new ChangeData
216 : * from scratch because there are tests that assert that certain computations (e.g. diffs) are
217 : * only done once. So we do what the prod indices do: We read and write fields using FieldDef.
218 : */
219 : public static class FakeChangeIndex
220 : extends AbstractFakeIndex<Change.Id, ChangeData, Map<String, Object>> implements ChangeIndex {
221 : private final ChangeData.Factory changeDataFactory;
222 : private final boolean skipMergable;
223 :
224 : @Inject
225 : FakeChangeIndex(
226 : SitePaths sitePaths,
227 : ChangeData.Factory changeDataFactory,
228 : @Assisted Schema<ChangeData> schema,
229 : @GerritServerConfig Config cfg) {
230 146 : super(schema, sitePaths, "changes");
231 146 : this.changeDataFactory = changeDataFactory;
232 146 : this.skipMergable = !MergeabilityComputationBehavior.fromConfig(cfg).includeInIndex();
233 146 : }
234 :
235 : @Override
236 : protected Change.Id keyFor(ChangeData value) {
237 99 : return value.getId();
238 : }
239 :
240 : @Override
241 : protected Comparator<ChangeData> sortingComparator() {
242 107 : Comparator<ChangeData> lastUpdated =
243 107 : Comparator.comparing(cd -> cd.change().getLastUpdatedOn());
244 107 : Comparator<ChangeData> merged =
245 107 : Comparator.comparing(cd -> cd.getMergedOn().orElse(Instant.EPOCH));
246 107 : Comparator<ChangeData> id = Comparator.comparing(cd -> cd.getId().get());
247 107 : return lastUpdated.thenComparing(merged).thenComparing(id).reversed();
248 : }
249 :
250 : @Override
251 : protected Map<String, Object> docFor(ChangeData value) {
252 99 : ImmutableMap.Builder<String, Object> doc = ImmutableMap.builder();
253 99 : for (SchemaField<ChangeData, ?> field : getSchema().getSchemaFields().values()) {
254 99 : if (ChangeField.MERGEABLE.getName().equals(field.getName()) && skipMergable) {
255 99 : continue;
256 : }
257 99 : Object docifiedValue = field.get(value);
258 99 : if (docifiedValue != null) {
259 99 : doc.put(field.getName(), field.get(value));
260 : }
261 99 : }
262 99 : return doc.build();
263 : }
264 :
265 : @Override
266 : protected ChangeData valueFor(Map<String, Object> doc) {
267 97 : ChangeData cd =
268 97 : changeDataFactory.create(
269 97 : Project.nameKey((String) doc.get(ChangeField.PROJECT_SPEC.getName())),
270 97 : Change.id(Integer.valueOf((String) doc.get(ChangeField.LEGACY_ID_STR.getName()))));
271 97 : for (SchemaField<ChangeData, ?> field : getSchema().getSchemaFields().values()) {
272 97 : field.setIfPossible(cd, new FakeStoredValue(doc.get(field.getName())));
273 97 : }
274 97 : return cd;
275 : }
276 :
277 : @Override
278 0 : public void insert(ChangeData obj) {}
279 : }
280 :
281 : /** Fake implementation of {@link AccountIndex} where all filtering happens in-memory. */
282 : public static class FakeAccountIndex
283 : extends AbstractFakeIndex<Account.Id, AccountState, AccountState> implements AccountIndex {
284 : @Inject
285 : FakeAccountIndex(SitePaths sitePaths, @Assisted Schema<AccountState> schema) {
286 146 : super(schema, sitePaths, "accounts");
287 146 : }
288 :
289 : @Override
290 : protected Account.Id keyFor(AccountState value) {
291 146 : return value.account().id();
292 : }
293 :
294 : @Override
295 : protected AccountState docFor(AccountState value) {
296 146 : return value;
297 : }
298 :
299 : @Override
300 : protected AccountState valueFor(AccountState doc) {
301 104 : return doc;
302 : }
303 :
304 : @Override
305 : protected Comparator<AccountState> sortingComparator() {
306 104 : return Comparator.comparing(a -> a.account().id().get());
307 : }
308 :
309 : @Override
310 1 : public void insert(AccountState obj) {}
311 : }
312 :
313 : /** Fake implementation of {@link GroupIndex} where all filtering happens in-memory. */
314 : public static class FakeGroupIndex
315 : extends AbstractFakeIndex<AccountGroup.UUID, InternalGroup, InternalGroup>
316 : implements GroupIndex {
317 : @Inject
318 : FakeGroupIndex(SitePaths sitePaths, @Assisted Schema<InternalGroup> schema) {
319 146 : super(schema, sitePaths, "groups");
320 146 : }
321 :
322 : @Override
323 : protected AccountGroup.UUID keyFor(InternalGroup value) {
324 146 : return value.getGroupUUID();
325 : }
326 :
327 : @Override
328 : protected InternalGroup docFor(InternalGroup value) {
329 146 : return value;
330 : }
331 :
332 : @Override
333 : protected InternalGroup valueFor(InternalGroup doc) {
334 145 : return doc;
335 : }
336 :
337 : @Override
338 : protected Comparator<InternalGroup> sortingComparator() {
339 145 : return Comparator.comparing(g -> g.getId().get());
340 : }
341 :
342 : @Override
343 15 : public void insert(InternalGroup obj) {}
344 : }
345 :
346 : /** Fake implementation of {@link ProjectIndex} where all filtering happens in-memory. */
347 : public static class FakeProjectIndex
348 : extends AbstractFakeIndex<Project.NameKey, ProjectData, ProjectData> implements ProjectIndex {
349 : @Inject
350 : FakeProjectIndex(SitePaths sitePaths, @Assisted Schema<ProjectData> schema) {
351 146 : super(schema, sitePaths, "projects");
352 146 : }
353 :
354 : @Override
355 : protected Project.NameKey keyFor(ProjectData value) {
356 143 : return value.getProject().getNameKey();
357 : }
358 :
359 : @Override
360 : protected ProjectData docFor(ProjectData value) {
361 143 : return value;
362 : }
363 :
364 : @Override
365 : protected ProjectData valueFor(ProjectData doc) {
366 6 : return doc;
367 : }
368 :
369 : @Override
370 : protected Comparator<ProjectData> sortingComparator() {
371 6 : return Comparator.comparing(p -> p.getProject().getName());
372 : }
373 :
374 : @Override
375 15 : public void insert(ProjectData obj) {}
376 : }
377 : }
|