Add tests for plugin-provided fields controlled by options

Change-Id: Iec2008ad7fb9c6e63135e20a4b91586300e88499
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
index 86b55bc..e495c99 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
@@ -17,8 +17,11 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.io.CharStreams;
 import com.google.common.reflect.TypeToken;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -30,16 +33,23 @@
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor.ChangeAttributeFactory;
 import com.google.gerrit.server.query.change.OutputStreamQuery;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import com.google.gerrit.sshd.PluginCommandModule;
+import com.google.gerrit.sshd.commands.Query;
 import com.google.gson.Gson;
 import com.google.inject.AbstractModule;
+import com.google.inject.servlet.ServletModule;
 import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Stream;
 import org.junit.Test;
+import org.kohsuke.args4j.Option;
 
 @UseSsh
 public class PluginFieldsIT extends AbstractDaemonTest {
@@ -96,6 +106,38 @@
     }
   }
 
+  static class MyOptions implements DynamicBean {
+    @Option(name = "--opt")
+    private String opt;
+  }
+
+  static class OptionAttributeSysModule extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(ChangeAttributeFactory.class)
+          .annotatedWith(Exports.named("simple"))
+          .toInstance(
+              (cd, qp, p) -> {
+                MyOptions opts = (MyOptions) qp.getDynamicBean(p);
+                return opts != null ? new MyInfo("opt " + opts.opt) : null;
+              });
+    }
+  }
+
+  static class OptionAttributeSshModule extends PluginCommandModule {
+    @Override
+    protected void configureCommands() {
+      bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
+    }
+  }
+
+  static class OptionAttributeHttpModule extends ServletModule {
+    @Override
+    protected void configureServlets() {
+      bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
+    }
+  }
+
   @Test
   public void queryChangeApiWithNullAttribute() throws Exception {
     queryChangeWithNullAttribute(
@@ -155,12 +197,73 @@
     assertThat(getter.call(id)).isNull();
   }
 
+  @Test
+  public void queryChangeSshWithOption() throws Exception {
+    queryChangeWithOption(
+        id -> pluginInfoFromSingletonListSsh(adminSshSession.exec(changeQueryCmd(id))),
+        (id, opts) ->
+            pluginInfoFromSingletonListSsh(adminSshSession.exec(changeQueryCmd(id, opts))));
+  }
+
+  @Test
+  public void queryChangeRestWithOption() throws Exception {
+    queryChangeWithOption(
+        id -> pluginInfoFromSingletonListRest(adminRestSession.get(changeQueryUrl(id))),
+        (id, opts) ->
+            pluginInfoFromSingletonListRest(adminRestSession.get(changeQueryUrl(id, opts))));
+  }
+
+  // No test for plugin-provided options over the extension API. There are currently two separate
+  // DynamicMap<DynamicBean> maps initialized in the SSH and HTTP injectors, and plugins have to
+  // define separate SSH/HTTP modules and bind their DynamicBeans in each one. To use the extension
+  // API, we would have to move everything into the sys injector.
+  // TODO(dborowitz): Determine whether this is possible without breaking existing plugins.
+
+  private void queryChangeWithOption(
+      PluginInfoGetter getterWithoutOptions, PluginInfoGetterWithOptions getterWithOptions)
+      throws Exception {
+    Change.Id id = createChange().getChange().getId();
+    assertThat(getterWithoutOptions.call(id)).isNull();
+
+    try (AutoCloseable ignored =
+        installPlugin(
+            "my-plugin",
+            OptionAttributeSysModule.class,
+            OptionAttributeHttpModule.class,
+            OptionAttributeSshModule.class)) {
+      assertThat(getterWithoutOptions.call(id))
+          .containsExactly(new MyInfo("my-plugin", "opt null"));
+      assertThat(getterWithOptions.call(id, ImmutableListMultimap.of("my-plugin--opt", "foo")))
+          .containsExactly(new MyInfo("my-plugin", "opt foo"));
+    }
+
+    assertThat(getterWithoutOptions.call(id)).isNull();
+  }
+
   private String changeQueryUrl(Change.Id id) {
-    return "/changes/?q=" + id;
+    return changeQueryUrl(id, ImmutableListMultimap.of());
+  }
+
+  private String changeQueryUrl(Change.Id id, ImmutableListMultimap<String, String> opts) {
+    String url = "/changes/?q=" + id;
+    String queryString = Joiner.on('&').withKeyValueSeparator('=').join(opts.entries());
+    if (!queryString.isEmpty()) {
+      url += "&" + queryString;
+    }
+    return url;
   }
 
   private String changeQueryCmd(Change.Id id) {
-    return "gerrit query --format json " + id;
+    return changeQueryCmd(id, ImmutableListMultimap.of());
+  }
+
+  private String changeQueryCmd(Change.Id id, ImmutableListMultimap<String, String> pluginOptions) {
+    return "gerrit query --format json "
+        + pluginOptions.entries().stream()
+            .flatMap(e -> Stream.of("--" + e.getKey(), e.getValue()))
+            .collect(joining(" "))
+        + " "
+        + id;
   }
 
   private static List<MyInfo> pluginInfoFromSingletonList(List<ChangeInfo> changeInfos) {
@@ -216,4 +319,10 @@
   private interface PluginInfoGetter {
     List<MyInfo> call(Change.Id id) throws Exception;
   }
+
+  @FunctionalInterface
+  private interface PluginInfoGetterWithOptions {
+    List<MyInfo> call(Change.Id id, ImmutableListMultimap<String, String> pluginOptions)
+        throws Exception;
+  }
 }