Introduce test cases for quotas

Adds basic framework and test case for `maxStartForTaskForQueue`.

Change-Id: I96b37d18e87664e968253f5569f0a715535d7f22
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
index 4e3e251..44d27c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
@@ -32,8 +32,7 @@
   }
 
   public QuotaSection firstMatching(Project.NameKey project) {
-    Config cfg = projectCache.getAllProjects().getConfig("quota.config").get();
-    return firstMatching(cfg, project);
+    return firstMatching(getQuotaConfig(), project);
   }
 
   public QuotaSection firstMatching(Config cfg, Project.NameKey project) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
index 0c1b8b4..63a8bc1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.quota;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
@@ -62,6 +63,17 @@
     initQuotas();
   }
 
+  @VisibleForTesting
+  public TaskQuotas(QuotaFinder quotaFinder, int interactiveThreads, int batchThreads) {
+    this.quotaFinder = quotaFinder;
+    this.quotaConfig = quotaFinder.getQuotaConfig();
+
+    QueueManager.initQueueWithCapacity(QueueManager.Queue.INTERACTIVE, interactiveThreads);
+    QueueManager.initQueueWithCapacity(QueueManager.Queue.BATCH, batchThreads);
+
+    initQuotas();
+  }
+
   private void initQuotas() {
     quotasByNamespace.putAll(
         quotaFinder.getQuotaNamespaces(quotaConfig).stream()
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/TaskQuotasTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/TaskQuotasTest.java
new file mode 100644
index 0000000..05d23d2
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/TaskQuotasTest.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2025 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.quota;
+
+import static com.googlesource.gerrit.plugins.quota.QueueManager.Queue.*;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+import com.google.gerrit.server.git.WorkQueue.Task;
+import java.util.Random;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TaskQuotasTest {
+  private static final String PROJECT_X = "project-x";
+  private static final String USER_A = "USER_A";
+
+  @Test
+  public void testMaxStartForTaskForQueue() throws ConfigInvalidException {
+    TaskQuotas taskQuotas =
+        taskQuotas(
+            2,
+            2,
+"""
+[quota "%s"]
+  maxStartForTaskForQueue = 1 uploadpack %s
+"""
+                .formatted(PROJECT_X, INTERACTIVE.getName()));
+
+    Task<?> u_x_1 = task(INTERACTIVE.getName(), uploadPackTask(PROJECT_X, USER_A));
+    assertTrue(taskQuotas.isReadyToStart(u_x_1));
+    startAndCompleteTask(taskQuotas, u_x_1);
+
+    Task<?> r_x_1 = task(INTERACTIVE.getName(), receivePackTask(PROJECT_X, USER_A));
+    assertTrue(taskQuotas.isReadyToStart(r_x_1));
+    startAndCompleteTask(taskQuotas, r_x_1);
+
+    Task<?> u_x_2 = task(INTERACTIVE.getName(), uploadPackTask(PROJECT_X, USER_A));
+    assertTrue(taskQuotas.isReadyToStart(u_x_2));
+    taskQuotas.onStart(u_x_2);
+
+    Task<?> r_x_2 = task(INTERACTIVE.getName(), receivePackTask(PROJECT_X, USER_A));
+    assertTrue(taskQuotas.isReadyToStart(r_x_2));
+    startAndCompleteTask(taskQuotas, r_x_2);
+
+    Task<?> u_x_3 = task(INTERACTIVE.getName(), uploadPackTask(PROJECT_X, USER_A));
+    assertFalse(taskQuotas.isReadyToStart(u_x_3));
+
+    Task<?> r_x_3 = task(INTERACTIVE.getName(), receivePackTask(PROJECT_X, USER_A));
+    assertTrue(taskQuotas.isReadyToStart(r_x_3));
+    startAndCompleteTask(taskQuotas, r_x_3);
+
+    taskQuotas.onStop(u_x_2);
+
+    assertTrue(taskQuotas.isReadyToStart(u_x_3));
+    startAndCompleteTask(taskQuotas, u_x_3);
+  }
+
+  private Task<?> task(String queueName, String taskString) {
+    Task<?> task = Mockito.mock(Task.class);
+    when(task.getTaskId()).thenReturn(new Random().nextInt());
+    when(task.getQueueName()).thenReturn(queueName);
+    when(task.toString()).thenReturn(taskString);
+    return task;
+  }
+
+  private TaskQuotas taskQuotas(int interactiveThreads, int batchThreads, String cfg)
+      throws ConfigInvalidException {
+    Config quotaConfig = new Config();
+    quotaConfig.fromText(cfg);
+    QuotaFinder finder = spy(new QuotaFinder(null));
+    doReturn(quotaConfig).when(finder).getQuotaConfig();
+    return new TaskQuotas(finder, interactiveThreads, batchThreads);
+  }
+
+  private String uploadPackTask(String project, String user) {
+    return "git-upload-pack %s (%s)".formatted(project, user);
+  }
+
+  private String receivePackTask(String project, String user) {
+    return "git-receive-pack %s (%s)".formatted(project, user);
+  }
+
+  private void startAndCompleteTask(TaskQuotas quotas, Task<?> task) {
+    quotas.onStart(task);
+    quotas.onStop(task);
+  }
+}