Fix '--task--include-statistics' option

Currently, on passing the option tasks plugin fails to add the
attributes with error [1] and output [2].

[1] error populating attribute on changes from plugin task [CONTEXT ratelimit_period="1 MINUTES" request="SSH" ]
java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Lcom.googlesource.gerrit.plugins.task.util.TopKeyMap$Entry; ([Ljava.lang.Object; is in module java.base of loader 'bootstrap'; [Lcom.googlesource.gerrit.plugins.task.util.TopKeyMap$Entry; is in unnamed module of loader java.net.FactoryURLClassLoader @58961770)
        at com.googlesource.gerrit.plugins.task.util.TopKeyMap.<init>(TopKeyMap.java:66)
        at com.googlesource.gerrit.plugins.task.statistics.HitHashMap.initStatistics(HitHashMap.java:144)
        at com.googlesource.gerrit.plugins.task.PredicateCache.initStatistics(PredicateCache.java:59)
        at com.googlesource.gerrit.plugins.task.TaskPluginDefinedInfoFactory.initStatistics(TaskPluginDefinedInfoFactory.java:405)
        at com.googlesource.gerrit.plugins.task.TaskPluginDefinedInfoFactory.createPluginDefinedInfos(TaskPluginDefinedInfoFactory.java:138)

[2] "message": "Something went wrong in plugin: task"

Change-Id: Id76ea65909a93afaffd3747afef1ac41a7ae3aeb
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
index a6627fb..8603ce0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
@@ -55,15 +55,14 @@
     }
   }
 
-  protected Entry[] entries;
+  protected Object[] entries;
 
   public TopKeyMap() {
     this(5);
   }
 
-  @SuppressWarnings("unchecked")
   public TopKeyMap(int length) {
-    entries = (Entry[]) new Object[length];
+    entries = new Object[length];
     for (int i = 0; i < entries.length; i++) {
       entries[i] = new Entry();
     }
@@ -73,10 +72,11 @@
     addIfTop(0, key, value);
   }
 
+  @SuppressWarnings("unchecked")
   protected void addIfTop(int i, long key, V value) {
-    if (entries[entries.length - 1].key < key) {
+    if (((Entry) entries[entries.length - 1]).key < key) {
       for (; i < entries.length; i++) {
-        Entry e = entries[i];
+        Entry e = (Entry) entries[i];
         if (e.key < key) {
           long eKValue = e.key;
           V eValue = e.value;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskIncludeStatisticsTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskIncludeStatisticsTest.java
new file mode 100644
index 0000000..8c619fe
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskIncludeStatisticsTest.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.googlesource.gerrit.plugins.task.TaskFileConstants.TASK_CFG;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.googlesource.gerrit.plugins.task.TaskPluginDefinedInfoFactory.TaskPluginAttribute;
+import java.io.BufferedReader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+@UseSsh
+public class TaskIncludeStatisticsTest extends AbstractDaemonTest {
+  private static final Gson GSON = new Gson();
+
+  @Test
+  public void testIncludeStatisticsDoesNotResultInError() throws Exception {
+    try (AutoCloseable task = installPlugin("task", Modules.Module.class)) {
+      TestRepository<InMemoryRepository> repo = cloneProject(allProjects);
+      fetch(repo, RefNames.REFS_CONFIG + ":meta-config");
+      repo.reset("meta-config");
+      createCommitAndPush(repo, RefNames.REFS_CONFIG, "Update task config", TASK_CFG, getConfig());
+
+      PushOneCommit.Result change = createChange();
+      String sshOutput =
+          adminSshSession.exec(
+              String.format(
+                  "gerrit query change:%s --task--applicable --task--include-statistics --format json",
+                  change.getChange().getId().get()));
+      adminSshSession.assertSuccess();
+
+      Map<Change.Id, TaskPluginAttribute> taskAttrByChange = getTaskAttributes(sshOutput);
+      TaskPluginAttribute taskAttribute = taskAttrByChange.get(change.getChange().getId());
+      assertThat(taskAttribute.message).isNull();
+      assertThat(taskAttribute.roots.size()).isEqualTo(1);
+      assertThat(taskAttribute.roots.get(0).name).isEqualTo("test root");
+    }
+  }
+
+  private String getConfig() {
+    return "[root \"test root\"]\n" + "  applicable = is:open\n" + "  pass = True";
+  }
+
+  private Map<Change.Id, TaskPluginAttribute> getTaskAttributes(String sshOutput) throws Exception {
+    List<Map<String, Object>> changeAttrs = getChangeAttrs(sshOutput);
+    Map<Change.Id, TaskPluginAttribute> taskAttrByChange = new HashMap<>();
+    changeAttrs.forEach(
+        change -> {
+          Double changeId = (Double) change.get("number");
+          taskAttrByChange.put(
+              Change.id(changeId.intValue()),
+              deserializeTaskAttributeFromPluginList(change.get("plugins")));
+        });
+    return taskAttrByChange;
+  }
+
+  private List<Map<String, Object>> getChangeAttrs(String sshOutput) throws Exception {
+    List<Map<String, Object>> changeAttrs = new ArrayList<>();
+    try (BufferedReader buffer = new BufferedReader(new StringReader(sshOutput))) {
+      buffer
+          .lines()
+          .forEach(
+              line -> {
+                Map<String, Object> changeAttr =
+                    GSON.fromJson(line, new TypeToken<Map<String, Object>>() {}.getType());
+                if (!"stats".equals(changeAttr.get("type"))) {
+                  changeAttrs.add(changeAttr);
+                }
+              });
+    }
+    return changeAttrs;
+  }
+
+  private TaskPluginAttribute deserializeTaskAttributeFromPluginList(@Nullable Object plugins) {
+    if (plugins == null) {
+      return null;
+    }
+    return GSON.fromJson(
+        GSON.toJson(((List<?>) plugins).get(0)), new TypeToken<TaskPluginAttribute>() {}.getType());
+  }
+}