Line data Source code
1 : // Copyright (C) 2019 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.acceptance;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.collect.ImmutableList.toImmutableList;
19 : import static com.google.common.truth.Truth.assertThat;
20 : import static java.util.Objects.requireNonNull;
21 :
22 : import com.google.common.base.MoreObjects;
23 : import com.google.common.collect.ImmutableListMultimap;
24 : import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
25 : import com.google.gerrit.common.Nullable;
26 : import com.google.gerrit.entities.Change;
27 : import com.google.gerrit.extensions.annotations.Exports;
28 : import com.google.gerrit.extensions.common.ChangeInfo;
29 : import com.google.gerrit.extensions.common.PluginDefinedInfo;
30 : import com.google.gerrit.extensions.registration.DynamicSet;
31 : import com.google.gerrit.server.DynamicOptions;
32 : import com.google.gerrit.server.DynamicOptions.DynamicBean;
33 : import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
34 : import com.google.gerrit.server.query.change.ChangeData;
35 : import com.google.gerrit.server.restapi.change.GetChange;
36 : import com.google.gerrit.server.restapi.change.QueryChanges;
37 : import com.google.gerrit.sshd.commands.Query;
38 : import com.google.gson.Gson;
39 : import com.google.gson.reflect.TypeToken;
40 : import com.google.inject.AbstractModule;
41 : import com.google.inject.Inject;
42 : import java.util.Collection;
43 : import java.util.HashMap;
44 : import java.util.List;
45 : import java.util.Map;
46 : import java.util.Objects;
47 : import org.kohsuke.args4j.Option;
48 :
49 3 : public class AbstractPluginFieldsTest extends AbstractDaemonTest {
50 : @Inject private ChangeOperations changeOperations;
51 :
52 : protected static class MyInfo extends PluginDefinedInfo {
53 : @Nullable String theAttribute;
54 :
55 3 : public MyInfo(@Nullable String theAttribute) {
56 3 : this.theAttribute = theAttribute;
57 3 : }
58 :
59 3 : MyInfo(String name, @Nullable String theAttribute) {
60 3 : this.name = requireNonNull(name);
61 3 : this.theAttribute = theAttribute;
62 3 : }
63 :
64 : @Override
65 : public boolean equals(Object o) {
66 3 : if (!(o instanceof MyInfo)) {
67 0 : return false;
68 : }
69 3 : MyInfo i = (MyInfo) o;
70 3 : return Objects.equals(name, i.name) && Objects.equals(theAttribute, i.theAttribute);
71 : }
72 :
73 : @Override
74 : public int hashCode() {
75 0 : return Objects.hash(name, theAttribute);
76 : }
77 :
78 : @Override
79 : public String toString() {
80 0 : return MoreObjects.toStringHelper(this)
81 0 : .add("name", name)
82 0 : .add("theAttribute", theAttribute)
83 0 : .toString();
84 : }
85 : }
86 :
87 3 : protected static class PluginDefinedSimpleAttributeModule extends AbstractModule {
88 : @Override
89 : public void configure() {
90 3 : DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
91 3 : .toInstance(
92 : (cds, bp, p) -> {
93 3 : Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
94 3 : cds.forEach(cd -> out.put(cd.getId(), new MyInfo("change " + cd.getId())));
95 3 : return out;
96 : });
97 3 : }
98 : }
99 :
100 3 : protected static class PluginDefinedBulkExceptionModule extends AbstractModule {
101 : @Override
102 : protected void configure() {
103 3 : DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
104 3 : .toInstance(
105 : (cds, bp, p) -> {
106 3 : throw new RuntimeException("Sample Exception");
107 : });
108 3 : }
109 : }
110 :
111 3 : protected static class PluginDefinedChangesByCommitBulkAttributeModule extends AbstractModule {
112 : @Override
113 : public void configure() {
114 3 : DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
115 3 : .toInstance(
116 : (cds, bp, p) -> {
117 3 : Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
118 3 : cds.forEach(
119 : cd ->
120 3 : out.put(
121 3 : cd.getId(),
122 3 : !cd.commitMessage().contains("no-info")
123 3 : ? new MyInfo("change " + cd.getId())
124 3 : : null));
125 3 : return out;
126 : });
127 3 : }
128 : }
129 :
130 3 : protected static class PluginDefinedSingleCallBulkAttributeModule extends AbstractModule {
131 : @Override
132 : public void configure() {
133 3 : DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
134 3 : .to(SingleCallBulkFactoryAttribute.class);
135 3 : }
136 : }
137 :
138 3 : protected static class SingleCallBulkFactoryAttribute implements ChangePluginDefinedInfoFactory {
139 3 : public static int timesCreateCalled = 0;
140 :
141 : @Override
142 : public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
143 : Collection<ChangeData> cds, DynamicOptions.BeanProvider beanProvider, String plugin) {
144 3 : timesCreateCalled++;
145 3 : Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
146 3 : cds.forEach(cd -> out.put(cd.getId(), new MyInfo("change " + cd.getId())));
147 3 : return out;
148 : }
149 : }
150 :
151 : private static class MyOptions implements DynamicBean {
152 : @Option(name = "--opt")
153 : private String opt;
154 : }
155 :
156 3 : public static class BulkAttributeFactoryWithOption implements ChangePluginDefinedInfoFactory {
157 : protected MyOptions opts;
158 :
159 : @Override
160 : public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
161 : Collection<ChangeData> cds, DynamicOptions.BeanProvider beanProvider, String plugin) {
162 3 : if (opts == null) {
163 3 : opts = (MyOptions) beanProvider.getDynamicBean(plugin);
164 : }
165 3 : Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
166 3 : cds.forEach(cd -> out.put(cd.getId(), new MyInfo("opt " + opts.opt)));
167 3 : return out;
168 : }
169 : }
170 :
171 3 : protected static class PluginDefinedOptionAttributeModule extends AbstractModule {
172 : @Override
173 : public void configure() {
174 3 : DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
175 3 : .to(BulkAttributeFactoryWithOption.class);
176 3 : bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
177 3 : bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
178 3 : bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class);
179 3 : }
180 : }
181 :
182 : protected void getSingleChangeWithPluginDefinedBulkAttribute(BulkPluginInfoGetterWithId getter)
183 : throws Exception {
184 3 : Change.Id id = createChange().getChange().getId();
185 :
186 3 : Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call(id);
187 3 : assertThat(pluginInfos.get(id)).isNull();
188 :
189 3 : try (AutoCloseable ignored =
190 3 : installPlugin("my-plugin", PluginDefinedSimpleAttributeModule.class)) {
191 3 : pluginInfos = getter.call(id);
192 3 : assertThat(pluginInfos.get(id)).containsExactly(new MyInfo("my-plugin", "change " + id));
193 : }
194 :
195 3 : pluginInfos = getter.call(id);
196 3 : assertThat(pluginInfos.get(id)).isNull();
197 3 : }
198 :
199 : protected void getMultipleChangesWithPluginDefinedBulkAttribute(BulkPluginInfoGetter getter)
200 : throws Exception {
201 3 : Change.Id id1 = createChange().getChange().getId();
202 3 : Change.Id id2 = createChange().getChange().getId();
203 :
204 3 : Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
205 3 : assertThat(pluginInfos.get(id1)).isNull();
206 3 : assertThat(pluginInfos.get(id2)).isNull();
207 :
208 3 : try (AutoCloseable ignored =
209 3 : installPlugin("my-plugin", PluginDefinedSimpleAttributeModule.class)) {
210 3 : pluginInfos = getter.call();
211 3 : assertThat(pluginInfos.get(id1)).containsExactly(new MyInfo("my-plugin", "change " + id1));
212 3 : assertThat(pluginInfos.get(id2)).containsExactly(new MyInfo("my-plugin", "change " + id2));
213 : }
214 :
215 3 : pluginInfos = getter.call();
216 3 : assertThat(pluginInfos.get(id1)).isNull();
217 3 : assertThat(pluginInfos.get(id2)).isNull();
218 3 : }
219 :
220 : protected void getChangesByCommitMessageWithPluginDefinedBulkAttribute(
221 : BulkPluginInfoGetter getter) throws Exception {
222 3 : Change.Id changeWithNoInfo = changeOperations.newChange().commitMessage("no-info").create();
223 3 : Change.Id changeWithInfo = changeOperations.newChange().commitMessage("info").create();
224 :
225 3 : Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
226 3 : assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
227 3 : assertThat(pluginInfos.get(changeWithInfo)).isNull();
228 :
229 3 : try (AutoCloseable ignored =
230 3 : installPlugin("my-plugin", PluginDefinedChangesByCommitBulkAttributeModule.class)) {
231 3 : pluginInfos = getter.call();
232 3 : assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
233 3 : assertThat(pluginInfos.get(changeWithInfo))
234 3 : .containsExactly(new MyInfo("my-plugin", "change " + changeWithInfo));
235 : }
236 :
237 3 : pluginInfos = getter.call();
238 3 : assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
239 3 : assertThat(pluginInfos.get(changeWithInfo)).isNull();
240 3 : }
241 :
242 : protected void getMultipleChangesWithPluginDefinedBulkAttributeInSingleCall(
243 : BulkPluginInfoGetter getter) throws Exception {
244 3 : Change.Id id1 = createChange().getChange().getId();
245 3 : Change.Id id2 = createChange().getChange().getId();
246 3 : int timesCalled = SingleCallBulkFactoryAttribute.timesCreateCalled;
247 :
248 3 : Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
249 3 : assertThat(pluginInfos.get(id1)).isNull();
250 3 : assertThat(pluginInfos.get(id2)).isNull();
251 :
252 3 : try (AutoCloseable ignored =
253 3 : installPlugin("my-plugin", PluginDefinedSingleCallBulkAttributeModule.class)) {
254 3 : pluginInfos = getter.call();
255 3 : assertThat(pluginInfos.get(id1)).containsExactly(new MyInfo("my-plugin", "change " + id1));
256 3 : assertThat(pluginInfos.get(id2)).containsExactly(new MyInfo("my-plugin", "change " + id2));
257 3 : assertThat(SingleCallBulkFactoryAttribute.timesCreateCalled).isEqualTo(timesCalled + 1);
258 : }
259 :
260 3 : pluginInfos = getter.call();
261 3 : assertThat(pluginInfos.get(id1)).isNull();
262 3 : assertThat(pluginInfos.get(id2)).isNull();
263 3 : }
264 :
265 : protected void getChangeWithPluginDefinedBulkAttributeOption(
266 : BulkPluginInfoGetterWithId getterWithoutOptions,
267 : BulkPluginInfoGetterWithIdAndOptions getterWithOptions)
268 : throws Exception {
269 3 : Change.Id id = createChange().getChange().getId();
270 3 : assertThat(getterWithoutOptions.call(id).get(id)).isNull();
271 :
272 3 : try (AutoCloseable ignored =
273 3 : installPlugin("my-plugin", PluginDefinedOptionAttributeModule.class)) {
274 3 : assertThat(getterWithoutOptions.call(id).get(id))
275 3 : .containsExactly(new MyInfo("my-plugin", "opt null"));
276 3 : assertThat(
277 3 : getterWithOptions.call(id, ImmutableListMultimap.of("my-plugin--opt", "foo")).get(id))
278 3 : .containsExactly(new MyInfo("my-plugin", "opt foo"));
279 : }
280 :
281 3 : assertThat(getterWithoutOptions.call(id).get(id)).isNull();
282 3 : }
283 :
284 : protected void getChangeWithPluginDefinedBulkAttributeWithException(
285 : BulkPluginInfoGetterWithId getter) throws Exception {
286 3 : Change.Id id = createChange().getChange().getId();
287 3 : assertThat(getter.call(id).get(id)).isNull();
288 :
289 3 : try (AutoCloseable ignored =
290 3 : installPlugin("my-plugin", PluginDefinedBulkExceptionModule.class)) {
291 3 : List<PluginDefinedInfo> outputInfos = getter.call(id).get(id);
292 3 : assertThat(outputInfos).hasSize(1);
293 3 : assertThat(outputInfos.get(0).name).isEqualTo("my-plugin");
294 3 : assertThat(outputInfos.get(0).message).isEqualTo("Something went wrong in plugin: my-plugin");
295 : }
296 :
297 3 : assertThat(getter.call(id).get(id)).isNull();
298 3 : }
299 :
300 : protected static List<PluginDefinedInfo> pluginInfoFromSingletonList(
301 : List<ChangeInfo> changeInfos) {
302 0 : assertThat(changeInfos).hasSize(1);
303 0 : return pluginInfoFromChangeInfo(changeInfos.get(0));
304 : }
305 :
306 : @Nullable
307 : protected static List<PluginDefinedInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
308 1 : List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
309 1 : if (pluginInfo == null) {
310 1 : return null;
311 : }
312 1 : return pluginInfo.stream().map(PluginDefinedInfo.class::cast).collect(toImmutableList());
313 : }
314 :
315 : protected static Map<Change.Id, List<PluginDefinedInfo>> pluginInfosFromChangeInfos(
316 : List<ChangeInfo> changeInfos) {
317 1 : Map<Change.Id, List<PluginDefinedInfo>> out = new HashMap<>();
318 1 : changeInfos.forEach(ci -> out.put(Change.id(ci._number), pluginInfoFromChangeInfo(ci)));
319 1 : return out;
320 : }
321 :
322 : /**
323 : * Decode {@code MyInfo}s from a raw list of maps returned from Gson.
324 : *
325 : * <p>This method is used instead of decoding {@code ChangeInfo} or {@code ChangAttribute}, since
326 : * Gson would decode the {@code plugins} field as a {@code List<PluginDefinedInfo>}, which would
327 : * return the base type and silently ignore any fields that are defined only in the subclass.
328 : * Instead, decode the enclosing {@code ChangeInfo} or {@code ChangeAttribute} as a raw {@code
329 : * Map<String, Object>}, and pass the {@code "plugins"} value to this method.
330 : *
331 : * @param gson Gson converter.
332 : * @param plugins list of {@code MyInfo} objects, each as a raw map returned from Gson.
333 : * @return decoded list of {@code MyInfo}s.
334 : */
335 : @Nullable
336 : protected static List<PluginDefinedInfo> decodeRawPluginsList(
337 : Gson gson, @Nullable Object plugins) {
338 2 : if (plugins == null) {
339 2 : return null;
340 : }
341 2 : checkArgument(plugins instanceof List, "not a list: %s", plugins);
342 2 : return gson.fromJson(gson.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
343 : }
344 :
345 : protected static Map<Change.Id, List<PluginDefinedInfo>> getPluginInfosFromChangeInfos(
346 : Gson gson, List<Map<String, Object>> changeInfos) {
347 2 : Map<Change.Id, List<PluginDefinedInfo>> out = new HashMap<>();
348 2 : changeInfos.forEach(
349 : change -> {
350 : Double changeId =
351 : (Double)
352 2 : (change.get("number") != null ? change.get("number") : change.get("_number"));
353 2 : out.put(
354 2 : Change.id(changeId.intValue()), decodeRawPluginsList(gson, change.get("plugins")));
355 2 : });
356 2 : return out;
357 : }
358 :
359 : @FunctionalInterface
360 : protected interface BulkPluginInfoGetter {
361 : Map<Change.Id, List<PluginDefinedInfo>> call() throws Exception;
362 : }
363 :
364 : @FunctionalInterface
365 : protected interface BulkPluginInfoGetterWithId {
366 : Map<Change.Id, List<PluginDefinedInfo>> call(Change.Id id) throws Exception;
367 : }
368 :
369 : @FunctionalInterface
370 : protected interface BulkPluginInfoGetterWithIdAndOptions {
371 : Map<Change.Id, List<PluginDefinedInfo>> call(
372 : Change.Id id, ImmutableListMultimap<String, String> pluginOptions) throws Exception;
373 : }
374 : }
|