This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new c8abb33884c SOLR-18541: CombinedQueryComponent.mergeIds gives wrong 
numFound, approximateTotalHits and numFoundExact (#5117)
c8abb33884c is described below

commit c8abb33884cb369bfcaac47b6b02b2e1a0c850aa
Author: Nick <[email protected]>
AuthorDate: Tue Oct 27 14:33:37 2026 -0600

    SOLR-18541: CombinedQueryComponent.mergeIds gives wrong numFound, 
approximateTotalHits and numFoundExact (#5117)
    
    Combined queries: skipped or failed shard responses no longer add to 
numFound or approximateTotalHits and mark numFoundExact false; 
approximateTotalHits no longer double-counts across shards.
---
 .../SOLR-18541-combined-query-merge-totals.yml     |  10 +
 .../handler/component/CombinedQueryComponent.java  |  33 +-
 .../CombinedQueryComponentMergeTotalsTest.java     | 375 +++++++++++++++++++++
 .../handler/component/MockResponseBuilder.java     |  25 ++
 .../solr/handler/component/MockShardRequest.java   |  41 +++
 .../handler/component/MockSortSpecBuilder.java     |   7 +
 6 files changed, 478 insertions(+), 13 deletions(-)

diff --git a/changelog/unreleased/SOLR-18541-combined-query-merge-totals.yml 
b/changelog/unreleased/SOLR-18541-combined-query-merge-totals.yml
new file mode 100644
index 00000000000..83f79fac733
--- /dev/null
+++ b/changelog/unreleased/SOLR-18541-combined-query-merge-totals.yml
@@ -0,0 +1,10 @@
+title: >
+  Combined queries: skipped or failed shard responses no longer add to 
numFound or
+  approximateTotalHits and mark numFoundExact false; approximateTotalHits no 
longer
+  double-counts across shards.
+type: fixed
+authors:
+  - name: Nick Shanin
+links:
+  - name: SOLR-18541
+    url: https://issues.apache.org/jira/browse/SOLR-18541
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
 
b/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
index b4b956d7745..9a905d99962 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
@@ -340,16 +340,13 @@ public class CombinedQueryComponent extends 
QueryComponent implements SolrCoreAw
                 instanceof List<?> docList
             && docList.get(queryIndex) instanceof SolrDocumentList 
solrDocumentList) {
           docs = Objects.requireNonNull(solrDocumentList);
-          queryNumFound += docs.getNumFound();
-          if (hitCountIsExact && 
Boolean.FALSE.equals(docs.getNumFoundExact())) {
-            hitCountIsExact = false;
-          }
         }
         failedShardCount +=
             addShardInfo(
                 shardInfo, failedShardCount, srsp, rb, 
queriesToCombineKeys[queryIndex], docs);
         if (srsp.getException() != null) {
           thereArePartialResults = true;
+          hitCountIsExact = false;
           continue;
         }
 
@@ -381,14 +378,6 @@ public class CombinedQueryComponent extends QueryComponent 
implements SolrCoreAw
           maxHitsTerminatedEarly = true;
         }
 
-        Object ath =
-            
responseHeader.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY);
-        if (ath == null) {
-          queryApproximateTotalHits += queryNumFound;
-        } else {
-          queryApproximateTotalHits += ((Number) ath).longValue();
-        }
-
         @SuppressWarnings("unchecked")
         NamedList<List<Object>> sortFieldValues =
             (NamedList<List<Object>>)
@@ -399,9 +388,27 @@ public class CombinedQueryComponent extends QueryComponent 
implements SolrCoreAw
         }
 
         boolean needsUnmarshalling = ss.includesNonScoreOrDocField();
-        if (thisResponseIsPartial && sortFieldValues.size() == 0 && 
needsUnmarshalling) {
+        // decided before the totals below: a skipped shard adds nothing to 
this query's totals,
+        // and its hits are unreported, so the merged count cannot be exact
+        if (thisResponseIsPartial
+            && (docs == null || (sortFieldValues.size() == 0 && 
needsUnmarshalling))) {
+          hitCountIsExact = false;
           continue;
         }
+
+        queryNumFound += docs.getNumFound();
+        if (hitCountIsExact && Boolean.FALSE.equals(docs.getNumFoundExact())) {
+          hitCountIsExact = false;
+        }
+
+        Object ath =
+            
responseHeader.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY);
+        if (ath == null) {
+          queryApproximateTotalHits += docs.getNumFound();
+        } else {
+          queryApproximateTotalHits += ((Number) ath).longValue();
+        }
+
         NamedList<List<Object>> unmarshalledSortFieldValues =
             needsUnmarshalling
                 ? unmarshalSortValues(ss, sortFieldValues, schema)
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentMergeTotalsTest.java
 
b/solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentMergeTotalsTest.java
new file mode 100644
index 00000000000..5dcdca59c4f
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentMergeTotalsTest.java
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.handler.component;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.apache.solr.handler.component.MockShardRequest.header;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IntPointField;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.SortSpec;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * Tests for the totals that {@link CombinedQueryComponent#mergeIds} reports: 
a partial shard that
+ * the merge skips contributes nothing to its query's totals, the 
approximateTotalHits fallback adds
+ * each shard's own count, and the merged exactness flag is true only when 
every contributing shard
+ * reports an exact count and no shard's hits were left out. Fabricated shard 
responses are driven
+ * through {@code mergeIds} directly.
+ */
+public class CombinedQueryComponentMergeTotalsTest extends SolrTestCase {
+
+  private final CombinedQueryComponent cqc = combinedQueryComponent();
+
+  @BeforeClass
+  public static void setup() {
+    assumeWorkingMockito();
+  }
+
+  private static CombinedQueryComponent combinedQueryComponent() {
+    CombinedQueryComponent cqc = new CombinedQueryComponent();
+    cqc.init(new NamedList<>());
+    cqc.inform(Mockito.mock(SolrCore.class));
+    return cqc;
+  }
+
+  private static SolrDocument doc(String id, float score) {
+    SolrDocument doc = new SolrDocument();
+    doc.addField("id", id);
+    doc.addField("score", score);
+    return doc;
+  }
+
+  private static SolrDocumentList docList(long numFound, Boolean exact, 
SolrDocument... docs) {
+    SolrDocumentList list = new SolrDocumentList();
+    for (SolrDocument doc : docs) {
+      list.add(doc);
+    }
+    list.setNumFound(numFound);
+    if (exact != null) {
+      list.setNumFoundExact(exact);
+    }
+    return list;
+  }
+
+  private static SortSpec scoreSortSpec() {
+    return MockSortSpecBuilder.create()
+        .withSortFields(new SortField[] {SortField.FIELD_SCORE})
+        .withSchemaFields(Arrays.asList((SchemaField) null))
+        .build();
+  }
+
+  private static SortSpec intSortSpec() {
+    return MockSortSpecBuilder.create()
+        .withSortFields(new SortField[] {new SortField("intDefault", 
SortField.Type.INT)})
+        .withSchemaFields(List.of(new SchemaField("intDefault", new 
IntPointField())))
+        .withIncludesNonScoreOrDocSortField(true)
+        .build();
+  }
+
+  /** sort_values_0 is keyed by field name; the value list is per doc, in doc 
order. */
+  private static NamedList<List<Object>> intSortValues() {
+    NamedList<List<Object>> sortValues = new NamedList<>();
+    sortValues.add("intDefault", List.<Object>of("1", "2", "3"));
+    return sortValues;
+  }
+
+  /** The fallback adds each shard's own count (2 and 3), not the running 
total (2 then 7). */
+  @Test
+  public void fallbackUsesEachShardsOwnCount() {
+    SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(
+                    
SolrQueryResponse.RESPONSE_HEADER_MAX_HITS_TERMINATED_EARLY_KEY, Boolean.TRUE),
+                docList(2, true, a1, a2),
+                List.of(docList(2, true, a1, a2)),
+                null)
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                null);
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 5, rb.getResponseDocs().getNumFound());
+    assertEquals(
+        "approximateTotalHits",
+        5L,
+        rb.rsp
+            .getResponseHeader()
+            
.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY));
+  }
+
+  /** A skipped partial shard contributes nothing to the totals of the query 
it was skipped for. */
+  @Test
+  public void skippedPartialShardContributesNothing() {
+    SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(
+                    SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
+                    Boolean.TRUE,
+                    
SolrQueryResponse.RESPONSE_HEADER_MAX_HITS_TERMINATED_EARLY_KEY,
+                    Boolean.TRUE,
+                    
SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY,
+                    10L),
+                docList(4, true, a1, a2),
+                List.of(docList(4, true, a1, a2)),
+                null)
+            .withShardResponse(
+                "shard2",
+                
header(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY, 5L),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                intSortValues());
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+    assertEquals(
+        "approximateTotalHits",
+        5L,
+        rb.rsp
+            .getResponseHeader()
+            
.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY));
+    assertEquals("response size", 3, rb.getResponseDocs().size());
+  }
+
+  @Test
+  public void skippedPartialShardWithHitsClearsExactness() {
+    SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE),
+                docList(2, true, a1, a2),
+                List.of(docList(2, true, a1, a2)),
+                null)
+            .withShardResponse(
+                "shard2",
+                header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE),
+                docList(0, true),
+                List.of(docList(0, true)),
+                null);
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 0, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+  }
+
+  /** A skipped partial shard that sent no per-query entry reports no count: 
inexact. */
+  @Test
+  public void skippedPartialShardWithoutCountClearsExactness() {
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE),
+                docList(0, true),
+                null,
+                null)
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                intSortValues());
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+    assertEquals("response size", 3, rb.getResponseDocs().size());
+  }
+
+  /** A skipped partial shard that reported no hits still leaves the count 
inexact. */
+  @Test
+  public void skippedPartialShardWithoutHitsClearsExactness() {
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE),
+                docList(0, true),
+                List.of(docList(0, true)),
+                null)
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                intSortValues());
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+    assertEquals("response size", 3, rb.getResponseDocs().size());
+  }
+
+  /** Both shards report exact counts; the merged flag stays exact. */
+  @Test
+  public void exactnessAllExactStaysExact() {
+    SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(),
+                docList(2, true, a1, a2),
+                List.of(docList(2, true, a1, a2)),
+                null)
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                null);
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFoundExact", Boolean.TRUE, 
rb.getResponseDocs().getNumFoundExact());
+  }
+
+  /** Both shards report inexact counts; the merged flag is inexact. */
+  @Test
+  public void exactnessAllInexactStaysInexact() {
+    SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(),
+                docList(2, false, a1, a2),
+                List.of(docList(2, false, a1, a2)),
+                null)
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, false, b1, b2, b3),
+                List.of(docList(3, false, b1, b2, b3)),
+                null);
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+  }
+
+  /** A partial shard with no per-query entry under a score-only sort is 
skipped: inexact. */
+  @Test
+  public void scoreOnlyPartialShardWithoutPerQueryListIsSkipped() {
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE),
+                docList(0, true),
+                null,
+                null)
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                null);
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+    assertEquals("response size", 3, rb.getResponseDocs().size());
+    assertEquals(
+        "partialResults",
+        Boolean.TRUE,
+        
rb.rsp.getResponseHeader().get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY));
+  }
+
+  @Test
+  public void multipleContributingSubQueriesCountDistinctDocsExactly() {
+    SolrDocument c1 = doc("c1", 5.0f), c2 = doc("c2", 4.0f), c3 = doc("c3", 
3.0f);
+    SolrDocument d1 = doc("d1", 2.0f), d2 = doc("d2", 1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withShardResponse(
+                "shard1",
+                header(),
+                docList(5, true, c1, c2, c3, d1, d2),
+                List.of(docList(3, true, c1, c2, c3), docList(3, true, c1, d1, 
d2)),
+                null);
+    MockResponseBuilder rb =
+        
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0",
 "q1");
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 5, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.TRUE, 
rb.getResponseDocs().getNumFoundExact());
+    assertEquals("response size", 5, rb.getResponseDocs().size());
+  }
+
+  /** A shard that errored leaves its hits out, so the merged count is 
inexact. */
+  @Test
+  public void erroredShardClearsExactness() {
+    SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3", 
1.0f);
+    MockShardRequest sreq =
+        MockShardRequest.create()
+            .withErroredShard("shard1", new RuntimeException("shard 
unavailable"))
+            .withShardResponse(
+                "shard2",
+                header(),
+                docList(3, true, b1, b2, b3),
+                List.of(docList(3, true, b1, b2, b3)),
+                null);
+    MockResponseBuilder rb =
+        MockResponseBuilder.create()
+            .withSortSpec(scoreSortSpec())
+            .withCombinerQueries("q0")
+            .withShardsTolerant();
+    cqc.mergeIds(rb, sreq);
+    assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+    assertEquals("numFoundExact", Boolean.FALSE, 
rb.getResponseDocs().getNumFoundExact());
+    assertEquals(
+        "partialResults",
+        Boolean.TRUE,
+        
rb.rsp.getResponseHeader().get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY));
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java 
b/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
index fa8bca48727..fb53949515d 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
@@ -18,14 +18,19 @@ package org.apache.solr.handler.component;
 
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.schema.SchemaField;
 import org.apache.solr.schema.StrField;
+import org.apache.solr.search.ReturnFields;
 import org.apache.solr.search.SortSpec;
 import org.mockito.Mockito;
 
@@ -66,4 +71,24 @@ public class MockResponseBuilder extends ResponseBuilder {
     this.setSortSpec(sortSpec);
     return this;
   }
+
+  public MockResponseBuilder withCombinerQueries(String... combinerQueries) {
+    SolrParams params = req.getParams();
+    
Mockito.when(params.getParams(CombinerParams.COMBINER_QUERY)).thenReturn(combinerQueries);
+    Mockito.when(params.get(CombinerParams.COMBINER_ALGORITHM, 
CombinerParams.DEFAULT_COMBINER))
+        .thenReturn(CombinerParams.DEFAULT_COMBINER);
+    SolrCore core = Mockito.mock(SolrCore.class);
+    
Mockito.when(core.getCoreContainer()).thenReturn(Mockito.mock(CoreContainer.class));
+    Mockito.when(req.getCore()).thenReturn(core);
+    
Mockito.when(rsp.getReturnFields()).thenReturn(Mockito.mock(ReturnFields.class));
+    return this;
+  }
+
+  public MockResponseBuilder withShardsTolerant() {
+    SolrParams params = req.getParams();
+    Mockito.when(params.get(ShardParams.SHARDS_TOLERANT)).thenReturn("true");
+    Mockito.when(params.getBool(Mockito.eq(CommonParams.PARTIAL_RESULTS), 
Mockito.anyBoolean()))
+        .thenReturn(true);
+    return this;
+  }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java 
b/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
index 3a3b6585714..e2fd6587c7a 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
@@ -17,6 +17,7 @@
 package org.apache.solr.handler.component;
 
 import java.util.ArrayList;
+import java.util.List;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.util.NamedList;
@@ -30,6 +31,14 @@ public class MockShardRequest extends ShardRequest {
     return mockShardRequest;
   }
 
+  public static NamedList<Object> header(Object... nameValues) {
+    NamedList<Object> header = new NamedList<>();
+    for (int i = 0; i < nameValues.length; i += 2) {
+      header.add((String) nameValues[i], nameValues[i + 1]);
+    }
+    return header;
+  }
+
   public MockShardRequest withShardResponse(
       NamedList<Object> responseHeader, SolrDocumentList solrDocuments) {
     ShardResponse shardResponse = buildShardResponse(responseHeader, 
solrDocuments);
@@ -37,6 +46,38 @@ public class MockShardRequest extends ShardRequest {
     return this;
   }
 
+  public MockShardRequest withShardResponse(
+      String shard,
+      NamedList<Object> responseHeader,
+      SolrDocumentList combinedResponse,
+      List<SolrDocumentList> perQueryResponses,
+      NamedList<List<Object>> sortValues0) {
+    NamedList<Object> response = new NamedList<>();
+    response.add("responseHeader", responseHeader);
+    response.add("response", combinedResponse);
+    if (perQueryResponses != null) {
+      response.add("response_per_query", perQueryResponses);
+    }
+    if (sortValues0 != null) {
+      response.add("sort_values_0", sortValues0);
+    }
+    SolrResponse solrResponse = Mockito.mock(SolrResponse.class);
+    Mockito.when(solrResponse.getResponse()).thenReturn(response);
+    ShardResponse shardResponse = new ShardResponse();
+    shardResponse.setShard(shard);
+    shardResponse.setSolrResponse(solrResponse);
+    responses.add(shardResponse);
+    return this;
+  }
+
+  public MockShardRequest withErroredShard(String shard, Throwable exception) {
+    ShardResponse shardResponse = new ShardResponse();
+    shardResponse.setShard(shard);
+    shardResponse.setException(exception);
+    responses.add(shardResponse);
+    return this;
+  }
+
   private ShardResponse buildShardResponse(
       NamedList<Object> responseHeader, SolrDocumentList solrDocuments) {
     SolrResponse solrResponse = Mockito.mock(SolrResponse.class);
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java 
b/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
index 5c42abe0af7..cbfe004b3a7 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
@@ -16,8 +16,10 @@
  */
 package org.apache.solr.handler.component;
 
+import java.util.List;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
+import org.apache.solr.schema.SchemaField;
 import org.apache.solr.search.SortSpec;
 import org.mockito.Mockito;
 
@@ -39,6 +41,11 @@ public class MockSortSpecBuilder {
     return this;
   }
 
+  public MockSortSpecBuilder withSchemaFields(List<SchemaField> schemaFields) {
+    Mockito.when(sortSpec.getSchemaFields()).thenReturn(schemaFields);
+    return this;
+  }
+
   public MockSortSpecBuilder withIncludesNonScoreOrDocSortField(boolean 
include) {
     Mockito.when(sortSpec.includesNonScoreOrDocField()).thenReturn(include);
     return this;

Reply via email to