LCOV - code coverage report
Current view: top level - index/testing - AbstractFakeIndex.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 114 116 98.3 %
Date: 2022-11-19 15:00:39 Functions: 48 50 96.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750