You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ge...@apache.org on 2019/10/08 10:32:43 UTC

[lucene-solr] branch branch_8x updated (48e19ab -> 8d040bf)

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

gerlowskija pushed a change to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git.


    from 48e19ab  LUCENE-8991: disable HashMap assertions (by default) on java9 and java1.8 as well
     new 20e3829  SOLR-13539: Introduce EmbeddedSolrServerTestBase
     new f74edc6  SOLR-13539: Improve atomic-update test coverage
     new 5dc480f  SOLR-13539: Fix MV removeregex atomic-updates
     new 8d040bf  Fix atomic-update test failures

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 solr/CHANGES.txt                                   |   3 +
 .../client/solrj/embedded/EmbeddedSolrServer.java  |  85 +++-
 .../src/java/org/apache/solr/schema/BoolField.java |   6 +-
 .../processor/AtomicUpdateDocumentMerger.java      |   6 +-
 .../test-files/solr/collection1/conf/schema.xml    |  30 ++
 .../test/org/apache/solr/update/RootFieldTest.java |   4 +-
 .../AbstractAtomicUpdatesMultivalueTestBase.java   | 428 +++++++++++++++++++++
 .../solr/update/processor/AtomicUpdatesTest.java   | 108 +++++-
 ...java => JavaBinAtomicUpdateMultivalueTest.java} |  14 +-
 ...sor.java => XMLAtomicUpdateMultivalueTest.java} |  14 +-
 .../org/apache/solr/client/solrj/GetByIdTest.java  |  34 +-
 .../solr/client/solrj/LargeVolumeTestBase.java     |  14 +-
 .../solrj/embedded/LargeVolumeBinaryJettyTest.java |   2 +-
 .../solrj/embedded/LargeVolumeJettyTest.java       |   2 +-
 .../solrj/embedded/SolrExampleEmbeddedTest.java    |   2 +-
 .../solr/client/solrj/request/SolrPingTest.java    |   8 +-
 ...irectJsonQueryRequestFacetingEmbeddedTest.java} | 204 ++++++----
 .../client/solrj/response/TermsResponseTest.java   |  20 +-
 .../solrj/response/TestSpellCheckResponse.java     |  33 +-
 .../solrj/response/TestSuggesterResponse.java      |   8 +-
 .../apache/solr/EmbeddedSolrServerTestBase.java    | 160 ++++++++
 .../java/org/apache/solr/SolrJettyTestBase.java    |  54 +--
 22 files changed, 1016 insertions(+), 223 deletions(-)
 create mode 100644 solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java
 copy solr/core/src/test/org/apache/solr/update/processor/{CustomUpdateRequestProcessor.java => JavaBinAtomicUpdateMultivalueTest.java} (74%)
 copy solr/core/src/test/org/apache/solr/update/processor/{CustomUpdateRequestProcessor.java => XMLAtomicUpdateMultivalueTest.java} (75%)
 copy solr/solrj/src/test/org/apache/solr/client/solrj/request/json/{DirectJsonQueryRequestFacetingIntegrationTest.java => DirectJsonQueryRequestFacetingEmbeddedTest.java} (73%)
 create mode 100644 solr/test-framework/src/java/org/apache/solr/EmbeddedSolrServerTestBase.java


[lucene-solr] 04/04: Fix atomic-update test failures

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git

commit 8d040bf1db7ada3e1bccf8428471b4fe192a7a1b
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Oct 7 14:14:04 2019 -0400

    Fix atomic-update test failures
---
 .../solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java b/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java
index 77f17c8..05fd0e7 100644
--- a/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java
+++ b/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java
@@ -45,6 +45,7 @@ public abstract class AbstractAtomicUpdatesMultivalueTestBase extends EmbeddedSo
 
   @BeforeClass
   public static void beforeClass() throws Exception {
+    System.setProperty("enable.update.log","true");
     initCore("solrconfig.xml", "schema.xml");
   }
 


[lucene-solr] 03/04: SOLR-13539: Fix MV removeregex atomic-updates

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git

commit 5dc480f88f03e120c692e7695e4a80ef7e662113
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Oct 7 09:29:31 2019 -0400

    SOLR-13539: Fix MV removeregex atomic-updates
    
    Prior to this commit, the ByteArrayUtf8CharSequence issues had been
    fixed on single value removeregex commands, but not if multiple regex's
    were used.
    
    This commit fixes our NamedList parsing for this additional case.  It
    also adds some tests for related atomic-update cases.
    
    Co-Authored-By: Tim Owen
---
 solr/CHANGES.txt                                   |   3 +
 .../processor/AtomicUpdateDocumentMerger.java      |   6 +-
 .../solr/update/processor/AtomicUpdatesTest.java   | 108 ++++++++++++++++++++-
 3 files changed, 113 insertions(+), 4 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 8011512..5a01693 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -198,6 +198,9 @@ Bug Fixes
 
 * SOLR-13790: LRUStatsCache size explosion and ineffective caching. (ab)
 
+* SOLR-13539: Fix for class-cast issues during atomic-update 'removeregex' operations. This also incorporated some
+  tests Tim wrote as a part of SOLR-9505. (Tim Owen via Jason Gerlowski)
+
 Other Changes
 ----------------------
 
diff --git a/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java b/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
index ea42552..f0972db 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/AtomicUpdateDocumentMerger.java
@@ -541,9 +541,9 @@ public class AtomicUpdateDocumentMerger {
   private Collection<Pattern> preparePatterns(Object fieldVal) {
     final Collection<Pattern> patterns = new LinkedHashSet<>(1);
     if (fieldVal instanceof Collection) {
-      Collection<String> patternVals = (Collection<String>) fieldVal;
-      for (String patternVal : patternVals) {
-        patterns.add(Pattern.compile(patternVal));
+      Collection<Object> patternVals = (Collection<Object>) fieldVal;
+      for (Object patternVal : patternVals) {
+        patterns.add(Pattern.compile(patternVal.toString()));
       }
     } else {
       patterns.add(Pattern.compile(fieldVal.toString()));
diff --git a/solr/core/src/test/org/apache/solr/update/processor/AtomicUpdatesTest.java b/solr/core/src/test/org/apache/solr/update/processor/AtomicUpdatesTest.java
index 1240486..48c76b7 100644
--- a/solr/core/src/test/org/apache/solr/update/processor/AtomicUpdatesTest.java
+++ b/solr/core/src/test/org/apache/solr/update/processor/AtomicUpdatesTest.java
@@ -75,6 +75,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "cat:ccc", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -88,6 +89,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "cat:ccc", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "21");
@@ -142,6 +144,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -155,6 +158,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "1021");
@@ -210,6 +214,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -223,6 +228,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "1021");
@@ -274,6 +280,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -287,6 +294,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "1021");
@@ -339,6 +347,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']");
 
     doc = new SolrInputDocument();
     doc.setField("id", "1001");
@@ -351,6 +360,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "1021");
@@ -423,6 +433,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -436,6 +447,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:222", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "intRemove:333", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "1021");
@@ -489,6 +501,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:22222222", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "intRemove:33333333", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -502,6 +515,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "intRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "intRemove:22222222", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "intRemove:33333333", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "1021");
@@ -559,6 +573,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
       assertQ(req("q", "dateRemove:*", "indent", "true"), "//result[@numFound = '4']");
     }
     assertQ(req("q", "dateRemove:\"2014-09-02T12:00:00Z\"", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "dateRemove:\"2014-09-03T12:00:00Z\"", "indent", "true"), "//result[@numFound = '3']");
 
     doc = new SolrInputDocument();
     doc.setField("id", "10001");
@@ -672,6 +687,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "dateRemove:*", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "dateRemove:\"2014-09-02T12:00:00Z\"", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "dateRemove:\"2014-09-03T12:00:00Z\"", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "10021");
@@ -794,6 +810,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "floatRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "floatRemove:\"222.222\"", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "floatRemove:\"333.333\"", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -808,6 +825,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "floatRemove:[* TO *]", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "floatRemove:\"222.222\"", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "floatRemove:\"333.333\"", "indent", "true"), "//result[@numFound = '3']"); // remove only removed first occurrence
 
     doc = new SolrInputDocument();
     doc.setField("id", "10021");
@@ -832,7 +850,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
     assertQ(req("q", "floatRemove:\"111.111\"", "indent", "true"), "//result[@numFound = '3']");
   }
 
- @Test
+  @Test
   public void testRemoveregex() throws Exception {
     SolrInputDocument doc;
 
@@ -862,6 +880,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '3']");
+    assertQ(req("q", "cat:ccc", "indent", "true"), "//result[@numFound = '3']");
 
 
     doc = new SolrInputDocument();
@@ -875,6 +894,7 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
 
     assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '4']");
     assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '2']");
+    assertQ(req("q", "cat:ccc", "indent", "true"), "//result[@numFound = '2']"); // removeregex does remove all occurrences
 
     doc = new SolrInputDocument();
     doc.setField("id", "21");
@@ -900,6 +920,43 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
   }
 
   @Test
+  public void testRemoveregexMustMatchWholeValue() throws Exception {
+    SolrInputDocument doc;
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "1");
+    doc.setField("cat", new String[]{"aaa", "bbb", "ccc", "ccc", "ddd"});
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '1']");
+
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "1");
+    List<String> removeList = new ArrayList<>();
+    removeList.add("bb");
+    doc.setField("cat", ImmutableMap.of("removeregex", removeList)); //behavior when hitting Solr through ZK
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '1']"); // Was not removed - regex didn't match whole value
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "1");
+    removeList = new ArrayList<>();
+    removeList.add("bbb");
+    doc.setField("cat", ImmutableMap.of("removeregex", removeList)); //behavior when hitting Solr through ZK
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '0']"); // Was removed now - regex matches
+  }
+
+  @Test
   public void testAdd() throws Exception {
     SolrInputDocument doc = new SolrInputDocument();
     doc.setField("id", "3");
@@ -976,6 +1033,55 @@ public class AtomicUpdatesTest extends SolrTestCaseJ4 {
   }
 
   @Test
+  public void testAddMultiple() throws Exception {
+    SolrInputDocument doc = new SolrInputDocument();
+    doc.setField("id", "3");
+    doc.setField("cat", new String[]{"aaa", "ccc"});
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '0']");
+
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "3");
+    doc.setField("cat", ImmutableMap.of("add", "bbb"));
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '1']");
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "3");
+    doc.setField("cat", ImmutableMap.of("add", "bbb"));
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '1']"); // Should now have 2 occurrences of bbb
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "3");
+    doc.setField("cat", ImmutableMap.of("remove", "bbb"));
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '1']"); // remove only removed first occurrence
+
+    doc = new SolrInputDocument();
+    doc.setField("id", "3");
+    doc.setField("cat", ImmutableMap.of("remove", "bbb"));
+    assertU(adoc(doc));
+    assertU(commit());
+
+    assertQ(req("q", "cat:*", "indent", "true"), "//result[@numFound = '1']");
+    assertQ(req("q", "cat:bbb", "indent", "true"), "//result[@numFound = '0']"); // remove now removed last occurrence
+  }
+
+  @Test
   public void testSet() throws Exception {
     SolrInputDocument doc;
 


[lucene-solr] 01/04: SOLR-13539: Introduce EmbeddedSolrServerTestBase

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git

commit 20e382984a704c1dabf704dae71857066f4c06dd
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Aug 29 09:11:51 2019 -0400

    SOLR-13539: Introduce EmbeddedSolrServerTestBase
    
    This groundwork commit allows tests to randomize request content-type
    more flexibly.  This will be taken advantage of by subsequent commits.
    
    Co-Authored-By: Thomas Woeckinger
    Closes: #755
---
 .../test/org/apache/solr/update/RootFieldTest.java |   4 +-
 .../org/apache/solr/client/solrj/GetByIdTest.java  |  34 ++---
 .../solr/client/solrj/LargeVolumeTestBase.java     |  14 +-
 .../solrj/embedded/LargeVolumeBinaryJettyTest.java |   2 +-
 .../solrj/embedded/LargeVolumeJettyTest.java       |   2 +-
 .../solrj/embedded/SolrExampleEmbeddedTest.java    |   2 +-
 .../solr/client/solrj/request/SolrPingTest.java    |   8 +-
 .../client/solrj/response/TermsResponseTest.java   |  20 +--
 .../solrj/response/TestSpellCheckResponse.java     |  33 +++--
 .../solrj/response/TestSuggesterResponse.java      |   8 +-
 .../apache/solr/EmbeddedSolrServerTestBase.java    | 160 +++++++++++++++++++++
 .../java/org/apache/solr/SolrJettyTestBase.java    |  54 +++----
 12 files changed, 244 insertions(+), 97 deletions(-)

diff --git a/solr/core/src/test/org/apache/solr/update/RootFieldTest.java b/solr/core/src/test/org/apache/solr/update/RootFieldTest.java
index 7c0ad2b..8015d19 100644
--- a/solr/core/src/test/org/apache/solr/update/RootFieldTest.java
+++ b/solr/core/src/test/org/apache/solr/update/RootFieldTest.java
@@ -17,7 +17,7 @@
 
 package org.apache.solr.update;
 
-import org.apache.solr.SolrJettyTestBase;
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.common.SolrDocument;
@@ -32,7 +32,7 @@ import org.junit.rules.ExpectedException;
 
 import static org.hamcrest.CoreMatchers.is;
 
-public class RootFieldTest extends SolrJettyTestBase {
+public class RootFieldTest extends EmbeddedSolrServerTestBase {
   private static boolean useRootSchema;
   private static final String MESSAGE = "Update handler should create and process _root_ field " +
       "unless there is no such a field in schema";
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/GetByIdTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/GetByIdTest.java
index 6085a08..3078a0a 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/GetByIdTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/GetByIdTest.java
@@ -18,7 +18,7 @@ package org.apache.solr.client.solrj;
 
 import java.util.Arrays;
 
-import org.apache.solr.SolrJettyTestBase;
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.params.CommonParams;
@@ -27,13 +27,13 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-public class GetByIdTest extends SolrJettyTestBase {
-  
+public class GetByIdTest extends EmbeddedSolrServerTestBase {
+
   @BeforeClass
   public static void beforeClass() throws Exception {
     initCore();
   }
-  
+
   @Before
   @Override
   public void setUp() throws Exception {
@@ -43,39 +43,39 @@ public class GetByIdTest extends SolrJettyTestBase {
         sdoc("id", "1", "term_s", "Microsoft", "term2_s", "MSFT"),
         sdoc("id", "2", "term_s", "Apple", "term2_s", "AAPL"),
         sdoc("id", "3", "term_s", "Yahoo", "term2_s", "YHOO")));
-    
+
     getSolrClient().commit(true, true);
   }
-  
+
   @Test
   public void testGetId() throws Exception {
     SolrDocument rsp = getSolrClient().getById("0");
     assertNull(rsp);
-    
+
     rsp = getSolrClient().getById("1");
     assertEquals("1", rsp.get("id"));
     assertEquals("Microsoft", rsp.get("term_s"));
     assertEquals("MSFT", rsp.get("term2_s"));
 
-    rsp = getSolrClient().getById("2");    
+    rsp = getSolrClient().getById("2");
     assertEquals("2", rsp.get("id"));
     assertEquals("Apple", rsp.get("term_s"));
     assertEquals("AAPL", rsp.get("term2_s"));
   }
-  
+
   @Test
   public void testGetIdWithParams() throws Exception {
     final SolrParams ID_FL_ONLY = params(CommonParams.FL, "id");
-    
+
     SolrDocument rsp = getSolrClient().getById("0", ID_FL_ONLY);
     assertNull(rsp);
-    
+
     rsp = getSolrClient().getById("1", ID_FL_ONLY);
     assertEquals("1", rsp.get("id"));
     assertNull("This field should have been removed from the response.", rsp.get("term_s"));
     assertNull("This field should have been removed from the response.", rsp.get("term2_s"));
 
-    rsp = getSolrClient().getById("2", ID_FL_ONLY);    
+    rsp = getSolrClient().getById("2", ID_FL_ONLY);
     assertEquals("2", rsp.get("id"));
     assertNull("This field should have been removed from the response.", rsp.get("term_s"));
     assertNull("This field should have been removed from the response.", rsp.get("term2_s"));
@@ -88,25 +88,25 @@ public class GetByIdTest extends SolrJettyTestBase {
     assertEquals("1", rsp.get(0).get("id"));
     assertEquals("Microsoft", rsp.get(0).get("term_s"));
     assertEquals("MSFT", rsp.get(0).get("term2_s"));
-    
+
     assertEquals("2", rsp.get(1).get("id"));
     assertEquals("Apple", rsp.get(1).get("term_s"));
     assertEquals("AAPL", rsp.get(1).get("term2_s"));
-    
+
     assertEquals("3", rsp.get(2).get("id"));
     assertEquals("Yahoo", rsp.get(2).get("term_s"));
     assertEquals("YHOO", rsp.get(2).get("term2_s"));
   }
-  
+
   @Test
   public void testGetIdsWithParams() throws Exception {
     SolrDocumentList rsp = getSolrClient().getById(Arrays.asList("0", "1", "2"), params(CommonParams.FL, "id"));
     assertEquals(2, rsp.getNumFound());
-    
+
     assertEquals("1", rsp.get(0).get("id"));
     assertNull("This field should have been removed from the response.", rsp.get(0).get("term_s"));
     assertNull("This field should have been removed from the response.", rsp.get(0).get("term2_s"));
-    
+
     assertEquals("2", rsp.get(1).get("id"));
     assertNull("This field should have been removed from the response.", rsp.get(1).get("term_s"));
     assertNull("This field should have been removed from the response.", rsp.get(1).get("term2_s"));
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/LargeVolumeTestBase.java b/solr/solrj/src/test/org/apache/solr/client/solrj/LargeVolumeTestBase.java
index 8f43c33..eb1dbc5 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/LargeVolumeTestBase.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/LargeVolumeTestBase.java
@@ -16,7 +16,12 @@
  */
 package org.apache.solr.client.solrj;
 
-import org.apache.solr.SolrJettyTestBase;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.client.solrj.response.UpdateResponse;
@@ -25,16 +30,11 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  *
  * @since solr 1.3
  */
-public abstract class LargeVolumeTestBase extends SolrJettyTestBase
+public abstract class LargeVolumeTestBase extends EmbeddedSolrServerTestBase
 {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeBinaryJettyTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeBinaryJettyTest.java
index 5b5bd11..3323dd0 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeBinaryJettyTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeBinaryJettyTest.java
@@ -27,6 +27,6 @@ import org.junit.BeforeClass;
 public class LargeVolumeBinaryJettyTest extends LargeVolumeTestBase {
   @BeforeClass
   public static void beforeTest() throws Exception {
-    createAndStartJetty(legacyExampleCollection1SolrHome());
+    initCore();
   }
 }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeJettyTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeJettyTest.java
index e7cb58f..9c172da 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeJettyTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/LargeVolumeJettyTest.java
@@ -24,6 +24,6 @@ import org.junit.BeforeClass;
 public class LargeVolumeJettyTest extends LargeVolumeTestBase {
   @BeforeClass
   public static void beforeTest() throws Exception {
-    createAndStartJetty(legacyExampleCollection1SolrHome());
+    initCore();
   }
 }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/SolrExampleEmbeddedTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/SolrExampleEmbeddedTest.java
index b4d89d4..05d8717 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/SolrExampleEmbeddedTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/SolrExampleEmbeddedTest.java
@@ -29,6 +29,6 @@ public class SolrExampleEmbeddedTest extends SolrExampleTests {
 
   @BeforeClass
   public static void beforeTest() throws Exception {
-    initCore();
+    createAndStartJetty(legacyExampleCollection1SolrHome());
   }
 }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/SolrPingTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/SolrPingTest.java
index e65049b..388cc78 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/SolrPingTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/SolrPingTest.java
@@ -16,10 +16,10 @@
  */
 package org.apache.solr.client.solrj.request;
 
-import junit.framework.Assert;
+import java.io.File;
 
 import org.apache.commons.io.FileUtils;
-import org.apache.solr.SolrJettyTestBase;
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.client.solrj.response.SolrPingResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrInputDocument;
@@ -27,12 +27,12 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import java.io.File;
+import junit.framework.Assert;
 
 /**
  * Test SolrPing in Solrj
  */
-public class SolrPingTest extends SolrJettyTestBase {
+public class SolrPingTest extends EmbeddedSolrServerTestBase {
   
   @BeforeClass
   public static void beforeClass() throws Exception {
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/response/TermsResponseTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/response/TermsResponseTest.java
index 6815889..57d6a73 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/response/TermsResponseTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/response/TermsResponseTest.java
@@ -15,31 +15,33 @@
  * limitations under the License.
  */
 package org.apache.solr.client.solrj.response;
+
 import java.util.List;
-import junit.framework.Assert;
 
-import org.apache.solr.SolrJettyTestBase;
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.client.solrj.SolrQuery;
-import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.TermsResponse.Term;
+import org.apache.solr.common.SolrInputDocument;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import junit.framework.Assert;
+
 /**
  * Test for TermComponent's response in Solrj
  */
-public class TermsResponseTest extends SolrJettyTestBase {
-
+public class TermsResponseTest extends EmbeddedSolrServerTestBase {
+  
   @BeforeClass
-  public static void beforeTest() throws Exception {
+  public static void beforeClass() throws Exception {
     initCore();
   }
-  
+
   @Before
   @Override
-  public void setUp() throws Exception{
+  public void setUp() throws Exception {
     super.setUp();
     clearIndex();
     assertU(commit());
@@ -62,7 +64,7 @@ public class TermsResponseTest extends SolrJettyTestBase {
     query.setTermsPrefix("s");
     query.addTermsField("terms_s");
     query.setTermsMinCount(1);
-    
+
     QueryRequest request = new QueryRequest(query);
     List<Term> terms = request.process(getSolrClient()).getTermsResponse().getTerms("terms_s");
 
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSpellCheckResponse.java b/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSpellCheckResponse.java
index 443091b..8ffdefe 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSpellCheckResponse.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSpellCheckResponse.java
@@ -15,8 +15,10 @@
  * limitations under the License.
  */
 package org.apache.solr.client.solrj.response;
-import junit.framework.Assert;
-import org.apache.solr.SolrJettyTestBase;
+
+import java.util.List;
+
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.SpellCheckResponse.Collation;
@@ -27,7 +29,7 @@ import org.apache.solr.common.params.SpellingParams;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import java.util.List;
+import junit.framework.Assert;
 
 /**
  * Test for SpellCheckComponent's response in Solrj
@@ -35,12 +37,13 @@ import java.util.List;
  *
  * @since solr 1.3
  */
-public class TestSpellCheckResponse extends SolrJettyTestBase {
+public class TestSpellCheckResponse extends EmbeddedSolrServerTestBase {
+
   @BeforeClass
-  public static void beforeTest() throws Exception {
+  public static void beforeClass() throws Exception {
     initCore();
   }
-  
+
   static String field = "name";
 
   @Test
@@ -101,7 +104,7 @@ public class TestSpellCheckResponse extends SolrJettyTestBase {
     // Hmmm... the API for SpellCheckResponse could be nicer:
     response.getSuggestions().get(0).getAlternatives().get(0);
   }
-  
+
   @Test
   public void testSpellCheckCollationResponse() throws Exception {
     getSolrClient();
@@ -128,7 +131,7 @@ public class TestSpellCheckResponse extends SolrJettyTestBase {
     doc.setField("name", "fat of homer");
     client.add(doc);
     client.commit(true, true);
-     
+
     //Test Backwards Compatibility
     SolrQuery query = new SolrQuery("name:(+fauth +home +loane)");
     query.set(CommonParams.QT, "/spell");
@@ -139,15 +142,15 @@ public class TestSpellCheckResponse extends SolrJettyTestBase {
     SpellCheckResponse response = request.process(client).getSpellCheckResponse();
     response = request.process(client).getSpellCheckResponse();
     assertTrue("name:(+faith +hope +loaves)".equals(response.getCollatedResult()));
-    
+
     //Test Expanded Collation Results
     query.set(SpellingParams.SPELLCHECK_COLLATE_EXTENDED_RESULTS, true);
     query.set(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES, 10);
-    query.set(SpellingParams.SPELLCHECK_MAX_COLLATIONS, 2); 
+    query.set(SpellingParams.SPELLCHECK_MAX_COLLATIONS, 2);
     request = new QueryRequest(query);
     response = request.process(client).getSpellCheckResponse();
     assertTrue("name:(+faith +hope +love)".equals(response.getCollatedResult()) || "name:(+faith +hope +loaves)".equals(response.getCollatedResult()));
-    
+
     List<Collation> collations = response.getCollatedResults();
     assertEquals(2, collations.size());
     for(Collation collation : collations)
@@ -174,7 +177,7 @@ public class TestSpellCheckResponse extends SolrJettyTestBase {
         }
       }
     }
-    
+
     query.set(SpellingParams.SPELLCHECK_COLLATE_EXTENDED_RESULTS, false);
     response = request.process(client).getSpellCheckResponse();
     {
@@ -182,12 +185,12 @@ public class TestSpellCheckResponse extends SolrJettyTestBase {
       assertEquals(2, collations.size());
       String collation1 = collations.get(0).getCollationQueryString();
       String collation2 = collations.get(1).getCollationQueryString();
-      assertFalse(collation1 + " equals " + collation2, 
+      assertFalse(collation1 + " equals " + collation2,
           collation1.equals(collation2));
       for(Collation collation : collations) {
         assertTrue("name:(+faith +hope +love)".equals(collation.getCollationQueryString()) || "name:(+faith +hope +loaves)".equals(collation.getCollationQueryString()));  
-      }      
+      }
     }
-    
+
   }
 }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSuggesterResponse.java b/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSuggesterResponse.java
index 0b3cf2c..5eb28ec 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSuggesterResponse.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/response/TestSuggesterResponse.java
@@ -15,11 +15,12 @@
  * limitations under the License.
  */
 package org.apache.solr.client.solrj.response;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.solr.SolrJettyTestBase;
+import org.apache.solr.EmbeddedSolrServerTestBase;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.QueryRequest;
@@ -32,9 +33,10 @@ import org.junit.Test;
  * Test for SuggesterComponent's response in Solrj
  *
  */
-public class TestSuggesterResponse extends SolrJettyTestBase {
+public class TestSuggesterResponse extends EmbeddedSolrServerTestBase {
+
   @BeforeClass
-  public static void beforeTest() throws Exception {
+  public static void beforeClass() throws Exception {
     initCore();
   }
 
diff --git a/solr/test-framework/src/java/org/apache/solr/EmbeddedSolrServerTestBase.java b/solr/test-framework/src/java/org/apache/solr/EmbeddedSolrServerTestBase.java
new file mode 100644
index 0000000..8df8dea
--- /dev/null
+++ b/solr/test-framework/src/java/org/apache/solr/EmbeddedSolrServerTestBase.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.ContentStreamBase;
+import org.apache.solr.common.util.ContentStreamBase.ByteArrayStream;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.After;
+import org.junit.AfterClass;
+
+import com.google.common.io.ByteStreams;
+
+abstract public class EmbeddedSolrServerTestBase extends SolrTestCaseJ4 {
+
+  protected static final String DEFAULT_CORE_NAME = "collection1";
+
+  public static EmbeddedSolrServer client = null;
+
+  @After
+  public synchronized void afterClass() throws Exception {
+    if (client != null) client.close();
+    client = null;
+  }
+
+  @AfterClass
+  public static void afterEmbeddedSolrServerTestBase() throws Exception {
+
+  }
+
+  public synchronized EmbeddedSolrServer getSolrClient() {
+    if (client == null) {
+      client = createNewSolrClient();
+    }
+    return client;
+  }
+
+  /**
+   * Create a new solr client. Subclasses should override for other options.
+   */
+  public EmbeddedSolrServer createNewSolrClient() {
+    return new EmbeddedSolrServer(h.getCoreContainer(), DEFAULT_CORE_NAME) {
+      @Override
+      public void close() {
+        // do not close core container
+      }
+    };
+  }
+
+  public void upload(final String collection, final ContentStream... contents) {
+    final Path base = Paths.get(getSolrClient().getCoreContainer().getSolrHome(), collection);
+    writeTo(base, contents);
+  }
+
+  private void writeTo(final Path base, final ContentStream... contents) {
+    try {
+      if (!Files.exists(base)) {
+        Files.createDirectories(base);
+      }
+
+      for (final ContentStream content : contents) {
+        final File file = new File(base.toFile(), content.getName());
+        file.getParentFile().mkdirs();
+
+        try (OutputStream os = new FileOutputStream(file)) {
+          ByteStreams.copy(content.getStream(), os);
+        }
+      }
+    } catch (final IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public Collection<ContentStream> download(final String collection, final String... names) {
+    final Path base = Paths.get(getSolrClient().getCoreContainer().getSolrHome(), collection);
+    final List<ContentStream> result = new ArrayList<>();
+
+    if (Files.exists(base)) {
+      for (final String name : names) {
+        final File file = new File(base.toFile(), name);
+        if (file.exists() && file.canRead()) {
+          try {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            ByteStreams.copy(new FileInputStream(file), os);
+            final ByteArrayStream stream = new ContentStreamBase.ByteArrayStream(os.toByteArray(), name);
+            result.add(stream);
+          } catch (final IOException e) {
+            throw new RuntimeException(e);
+          }
+        }
+      }
+    }
+
+    return result;
+  }
+
+  public static void initCore() throws Exception {
+    final String home = legacyExampleCollection1SolrHome();
+    final String config = home + "/" + DEFAULT_CORE_NAME + "/conf/solrconfig.xml";
+    final String schema = home + "/" + DEFAULT_CORE_NAME + "/conf/schema.xml";
+    initCore(config, schema, home);
+  }
+
+  public static String legacyExampleCollection1SolrHome() throws IOException {
+    final String sourceHome = ExternalPaths.SOURCE_HOME;
+    if (sourceHome == null)
+      throw new IllegalStateException("No source home! Cannot create the legacy example solr home directory.");
+
+    final File tempSolrHome = LuceneTestCase.createTempDir().toFile();
+    FileUtils.copyFileToDirectory(new File(sourceHome, "server/solr/solr.xml"), tempSolrHome);
+    final File collectionDir = new File(tempSolrHome, DEFAULT_CORE_NAME);
+    FileUtils.forceMkdir(collectionDir);
+    final File configSetDir = new File(sourceHome, "server/solr/configsets/sample_techproducts_configs/conf");
+    FileUtils.copyDirectoryToDirectory(configSetDir, collectionDir);
+
+    final Properties props = new Properties();
+    props.setProperty("name", DEFAULT_CORE_NAME);
+
+    try (Writer writer = new OutputStreamWriter(FileUtils.openOutputStream(new File(collectionDir, "core.properties")),
+        "UTF-8");) {
+      props.store(writer, null);
+    }
+
+    return tempSolrHome.getAbsolutePath();
+  }
+
+}
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrJettyTestBase.java b/solr/test-framework/src/java/org/apache/solr/SolrJettyTestBase.java
index db415a2..6dcccb4 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrJettyTestBase.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrJettyTestBase.java
@@ -16,10 +16,16 @@
  */
 package org.apache.solr;
 
+import java.io.File;
+import java.io.OutputStreamWriter;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.SortedMap;
+
 import org.apache.commons.io.FileUtils;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
 import org.apache.solr.client.solrj.embedded.JettyConfig;
 import org.apache.solr.client.solrj.embedded.JettySolrRunner;
 import org.apache.solr.client.solrj.impl.HttpSolrClient;
@@ -31,16 +37,9 @@ import org.junit.BeforeClass;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.OutputStreamWriter;
-import java.lang.invoke.MethodHandles;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
-import java.util.Properties;
-import java.util.SortedMap;
-
 
-abstract public class SolrJettyTestBase extends SolrTestCaseJ4 
+abstract public class SolrJettyTestBase extends SolrTestCaseJ4
 {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
@@ -55,8 +54,8 @@ abstract public class SolrJettyTestBase extends SolrTestCaseJ4
   public static String context;
 
   public static JettySolrRunner createAndStartJetty(String solrHome, String configFile, String schemaFile, String context,
-                                            boolean stopAtShutdown, SortedMap<ServletHolder,String> extraServlets) 
-      throws Exception { 
+                                            boolean stopAtShutdown, SortedMap<ServletHolder,String> extraServlets)
+      throws Exception {
     // creates the data dir
 
     context = context==null ? "/solr" : context;
@@ -132,7 +131,6 @@ abstract public class SolrJettyTestBase extends SolrTestCaseJ4
     }
   }
 
-
   public synchronized SolrClient getSolrClient() {
     if (client == null) {
       client = createNewSolrClient();
@@ -147,23 +145,13 @@ abstract public class SolrJettyTestBase extends SolrTestCaseJ4
    * Subclasses should override for other options.
    */
   public SolrClient createNewSolrClient() {
-    if (jetty != null) {
-      try {
-        // setup the client...
-        String url = jetty.getBaseUrl().toString() + "/" + "collection1";
-        HttpSolrClient client = getHttpSolrClient(url, DEFAULT_CONNECTION_TIMEOUT);
-        return client;
-      }
-      catch( Exception ex ) {
-        throw new RuntimeException( ex );
-      }
-    } else {
-      return new EmbeddedSolrServer( h.getCoreContainer(), "collection1" ) {
-        @Override
-        public void close() {
-          // do not close core container
-        }
-      };
+    try {
+      // setup the client...
+      final String url = jetty.getBaseUrl().toString() + "/" + "collection1";
+      final HttpSolrClient client = getHttpSolrClient(url, DEFAULT_CONNECTION_TIMEOUT);
+      return client;
+    } catch (final Exception ex) {
+      throw new RuntimeException(ex);
     }
   }
 
@@ -179,13 +167,6 @@ abstract public class SolrJettyTestBase extends SolrTestCaseJ4
     }
   }
 
-  public static void initCore() throws Exception {
-    String exampleHome = legacyExampleCollection1SolrHome();
-    String exampleConfig = exampleHome+"/collection1/conf/solrconfig.xml";
-    String exampleSchema = exampleHome+"/collection1/conf/schema.xml";
-    initCore(exampleConfig, exampleSchema, exampleHome);
-  }
-
   public static String legacyExampleCollection1SolrHome() {
     String sourceHome = ExternalPaths.SOURCE_HOME;
     if (sourceHome == null)
@@ -226,5 +207,4 @@ abstract public class SolrJettyTestBase extends SolrTestCaseJ4
     return legacyExampleSolrHome;
   }
 
-
 }


[lucene-solr] 02/04: SOLR-13539: Improve atomic-update test coverage

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git

commit f74edc6787093116f736055ed28712b01224c7f7
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Oct 7 08:26:11 2019 -0400

    SOLR-13539: Improve atomic-update test coverage
    
    Closes #665
---
 .../client/solrj/embedded/EmbeddedSolrServer.java  |  85 ++-
 .../src/java/org/apache/solr/schema/BoolField.java |   6 +-
 .../test-files/solr/collection1/conf/schema.xml    |  30 ++
 .../AbstractAtomicUpdatesMultivalueTestBase.java   | 427 +++++++++++++++
 .../JavaBinAtomicUpdateMultivalueTest.java         |  28 +
 .../processor/XMLAtomicUpdateMultivalueTest.java   |  28 +
 ...DirectJsonQueryRequestFacetingEmbeddedTest.java | 592 +++++++++++++++++++++
 7 files changed, 1173 insertions(+), 23 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/client/solrj/embedded/EmbeddedSolrServer.java b/solr/core/src/java/org/apache/solr/client/solrj/embedded/EmbeddedSolrServer.java
index db4396f..4a5b45d 100644
--- a/solr/core/src/java/org/apache/solr/client/solrj/embedded/EmbeddedSolrServer.java
+++ b/solr/core/src/java/org/apache/solr/client/solrj/embedded/EmbeddedSolrServer.java
@@ -16,6 +16,8 @@
  */
 package org.apache.solr.client.solrj.embedded;
 
+import static org.apache.solr.common.params.CommonParams.PATH;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -24,6 +26,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.function.Supplier;
 
 import org.apache.commons.io.output.ByteArrayOutputStream;
 import org.apache.solr.client.solrj.SolrClient;
@@ -56,8 +59,6 @@ import org.apache.solr.response.ResultContext;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.servlet.SolrRequestParsers;
 
-import static org.apache.solr.common.params.CommonParams.PATH;
-
 /**
  * SolrClient that connects directly to a CoreContainer.
  *
@@ -68,6 +69,21 @@ public class EmbeddedSolrServer extends SolrClient {
   protected final CoreContainer coreContainer;
   protected final String coreName;
   private final SolrRequestParsers _parser;
+  private final RequestWriterSupplier supplier;
+
+  public enum RequestWriterSupplier {
+    JavaBin(() -> new BinaryRequestWriter()), XML(() -> new RequestWriter());
+
+    private Supplier<RequestWriter> supplier;
+
+    private RequestWriterSupplier(final Supplier<RequestWriter> supplier) {
+      this.supplier = supplier;
+    }
+
+    public RequestWriter newRequestWriter() {
+      return supplier.get();
+    }
+  }
 
   /**
    * Create an EmbeddedSolrServer using a given solr home directory
@@ -111,12 +127,30 @@ public class EmbeddedSolrServer extends SolrClient {
    * @param coreName      the core to route requests to by default (optional)
    */
   public EmbeddedSolrServer(CoreContainer coreContainer, String coreName) {
+    this(coreContainer, coreName, RequestWriterSupplier.JavaBin);
+  }
+
+  /**
+   * Create an EmbeddedSolrServer wrapping a CoreContainer.
+   * <p>
+   * Note that EmbeddedSolrServer will shutdown the wrapped CoreContainer when {@link #close()} is called.
+   *
+   * @param coreContainer
+   *          the core container
+   * @param coreName
+   *          the core to route requests to by default
+   * @param supplier
+   *          the supplier used to create a {@link RequestWriter}
+   */
+  public EmbeddedSolrServer(CoreContainer coreContainer, String coreName,
+      RequestWriterSupplier supplier) {
     if (coreContainer == null) {
       throw new NullPointerException("CoreContainer instance required");
     }
     this.coreContainer = coreContainer;
     this.coreName = coreName;
     _parser = new SolrRequestParsers(null);
+    this.supplier = supplier;
   }
 
   // TODO-- this implementation sends the response to XML and then parses it.
@@ -242,32 +276,41 @@ public class EmbeddedSolrServer extends SolrClient {
   private Set<ContentStream> getContentStreams(SolrRequest request) throws IOException {
     if (request.getMethod() == SolrRequest.METHOD.GET) return null;
     if (request instanceof ContentStreamUpdateRequest) {
-      ContentStreamUpdateRequest csur = (ContentStreamUpdateRequest) request;
-      Collection<ContentStream> cs = csur.getContentStreams();
+      final ContentStreamUpdateRequest csur = (ContentStreamUpdateRequest) request;
+      final Collection<ContentStream> cs = csur.getContentStreams();
       if (cs != null) return new HashSet<>(cs);
     }
-    RequestWriter.ContentWriter contentWriter = request.getContentWriter(CommonParams.JAVABIN_MIME);
-    final String cType = contentWriter == null ? CommonParams.JAVABIN_MIME : contentWriter.getContentType();
 
-    return Collections.singleton(new ContentStreamBase() {
+    final RequestWriter.ContentWriter contentWriter = request.getContentWriter(null);
+
+    String cType;
+    final BAOS baos = new BAOS();
+    if (contentWriter != null) {
+      contentWriter.write(baos);
+      cType = contentWriter.getContentType();
+    } else {
+      final RequestWriter rw = supplier.newRequestWriter();
+      cType = rw.getUpdateContentType();
+      rw.write(request, baos);
+    }
 
-      @Override
-      public InputStream getStream() throws IOException {
-        BAOS baos = new BAOS();
-        if (contentWriter != null) {
-          contentWriter.write(baos);
-        } else {
-          new BinaryRequestWriter().write(request, baos);
+    final byte[] buf = baos.toByteArray();
+    if (buf.length > 0) {
+      return Collections.singleton(new ContentStreamBase() {
+
+        @Override
+        public InputStream getStream() throws IOException {
+          return new ByteArrayInputStream(buf);
         }
-        return new ByteArrayInputStream(baos.toByteArray());
-      }
 
-      @Override
-      public String getContentType() {
-        return cType;
+        @Override
+        public String getContentType() {
+          return cType;
+        }
+      });
+    }
 
-      }
-    });
+    return null;
   }
 
   private JavaBinCodec createJavaBinCodec(final StreamingResponseCallback callback, final BinaryResponseWriter.Resolver resolver) {
diff --git a/solr/core/src/java/org/apache/solr/schema/BoolField.java b/solr/core/src/java/org/apache/solr/schema/BoolField.java
index 8cad743..5fb2d85 100644
--- a/solr/core/src/java/org/apache/solr/schema/BoolField.java
+++ b/solr/core/src/java/org/apache/solr/schema/BoolField.java
@@ -45,6 +45,7 @@ import org.apache.solr.response.TextResponseWriter;
 import org.apache.solr.search.QParser;
 import org.apache.solr.search.function.OrdFieldSource;
 import org.apache.solr.uninverting.UninvertingReader.Type;
+
 /**
  *
  */
@@ -260,8 +261,8 @@ class BoolFieldSource extends ValueSource {
           return -1;
         }
       }
+
       @Override
-      
       public boolean boolVal(int doc) throws IOException {
         return getOrdForDoc(doc) == trueOrd;
       }
@@ -298,9 +299,10 @@ class BoolFieldSource extends ValueSource {
   }
 
   private static final int hcode = OrdFieldSource.class.hashCode();
+
   @Override
   public int hashCode() {
     return hcode + field.hashCode();
-  };
+  }
 
 }
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema.xml b/solr/core/src/test-files/solr/collection1/conf/schema.xml
index e0a96cc..9f46cec 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema.xml
@@ -498,6 +498,14 @@
     </analyzer>
   </fieldType>
   <fieldType name="severityType" class="${solr.tests.EnumFieldType}" enumsConfig="enumsConfig.xml" enumName="severity"/>
+  
+  <fieldType name="binary" class="solr.BinaryField" />
+  <fieldType name="collation" class="solr.CollationField" language="en" />
+  <fieldType name="externalFile" class="solr.ExternalFileField" />
+  <fieldType name="icuCollation" class="solr.ICUCollationField" locale="en" />
+  <fieldType name="latLonPointSpatial" class="solr.LatLonPointSpatialField" />
+  <fieldType name="randomSort" class="solr.RandomSortField" />
+  <fieldType name="point" class="solr.PointType" subFieldSuffix="_coordinate" />
 
   <fieldType name="sortable_text" class="solr.SortableTextField">
     <analyzer>
@@ -632,6 +640,28 @@
   <field name="dateRemove" type="date" indexed="true" stored="true" multiValued="true"/>
   <field name="floatRemove" type="float" indexed="true" stored="true" multiValued="true"/>
 
+  <field name="binaryRemove" type="binary" indexed="true" stored="true" multiValued="true"/>
+  <field name="booleanRemove" type="boolean" indexed="true" stored="true" multiValued="true"/>
+  <field name="collationRemove" type="collation" indexed="true" stored="true" multiValued="true"/>
+  <field name="datePointRemove" type="pdate" indexed="true" stored="true" multiValued="true"/>
+  <field name="dateRangeRemove" type="dateRange" indexed="true" stored="true" multiValued="true"/>
+  <field name="doublePointRemove" type="pdouble" indexed="true" stored="true" multiValued="true"/>
+  <field name="externalFileRemove" type="externalFile" indexed="true" stored="true" multiValued="true"/>
+  <field name="floatPointRemove" type="pfloat" indexed="true" stored="true" multiValued="true"/>
+  <field name="icuCollationRemove" type="icuCollation" indexed="true" stored="true" multiValued="true"/>
+  <field name="intPointRemove" type="pint" indexed="true" stored="true" multiValued="true"/>
+  <field name="latLonPointSpatialRemove" type="latLonPointSpatial" indexed="true" stored="true" multiValued="true"/>
+  <field name="latLonRemove" type="location" indexed="true" stored="true" multiValued="true"/>
+  <field name="longPointRemove" type="plong" indexed="true" stored="true" multiValued="true"/>
+  <field name="point_0_coordinate" type="float" indexed="true" stored="true" multiValued="true"/>
+  <field name="point_1_coordinate" type="float" indexed="true" stored="true" multiValued="true"/>
+  <field name="pointRemove" type="point" indexed="true" stored="true" multiValued="true"/>
+  <field name="randomSortRemove" type="randomSort" indexed="true" stored="true" multiValued="true"/>
+  <field name="spatialRecursivePrefixTreeRemove" type="location_rpt" indexed="true" stored="true" multiValued="true"/>
+  <field name="stringRemove" type="string" indexed="true" stored="true" multiValued="true"/>
+  <field name="textRemove" type="text" indexed="true" stored="true" multiValued="true"/>
+  <field name="uuidRemove" type="uuid" indexed="true" stored="true" multiValued="true"/>
+
   <field name="nopositionstext" type="nopositions" indexed="true" stored="true"/>
 
   <field name="tlong" type="tlong" indexed="true" stored="true"/>
diff --git a/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java b/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java
new file mode 100644
index 0000000..77f17c8
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/update/processor/AbstractAtomicUpdatesMultivalueTestBase.java
@@ -0,0 +1,427 @@
+/*
+ * 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.update.processor;
+
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.not;
+
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.curator.shaded.com.google.common.collect.Lists;
+import org.apache.solr.EmbeddedSolrServerTestBase;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer.RequestWriterSupplier;
+import org.apache.solr.common.util.ByteArrayUtf8CharSequence;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public abstract class AbstractAtomicUpdatesMultivalueTestBase extends EmbeddedSolrServerTestBase {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig.xml", "schema.xml");
+  }
+
+  @Before
+  public void before() throws SolrServerException, IOException {
+    getSolrClient().deleteByQuery("*:*");
+  }
+
+  abstract RequestWriterSupplier getRequestWriterSupplier();
+
+  @Override
+  public synchronized EmbeddedSolrServer getSolrClient() {
+    return new EmbeddedSolrServer(h.getCoreContainer(), DEFAULT_CORE_NAME, getRequestWriterSupplier()) {
+
+      @Override
+      public void close() {
+        // do not close core container
+      }
+    };
+  }
+
+  private static void assertQR(final String fieldName, final String queryValue, final int numFound) {
+    assertQ(req("q", fieldName + ":" + queryValue, "indent", "true"), "//result[@numFound = '" + numFound + "']");
+  }
+
+  private void runTestForField(final String fieldName, final Object[] values, final String[] queries,
+      final Optional<Function<Object,Object>> valueConverter)
+      throws SolrServerException, IOException {
+
+    final Function<Object,Object> vc = valueConverter.orElse(o -> o);
+
+    getSolrClient().add(Arrays.asList(
+        sdoc("id", "20000", fieldName, Arrays.asList(values[0], values[1], values[2])),
+        sdoc("id", "20001", fieldName, Arrays.asList(values[1], values[2], values[3]))));
+    getSolrClient().commit(true, true);
+
+    if (queries != null) {
+      assertQR(fieldName, queries[0], 1);
+      assertQR(fieldName, queries[1], 2);
+      assertQR(fieldName, queries[2], 2);
+      assertQR(fieldName, queries[3], 1);
+    }
+
+    Collection<Object> fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(3, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[0]), vc.apply(values[1]), vc.apply(values[2])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[3]))));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(3, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[1]), vc.apply(values[2]), vc.apply(values[3])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[0]))));
+
+    getSolrClient().add(sdoc("id", "20000", fieldName, ImmutableMap.of("remove",
+        Lists.newArrayList(values[0]))));
+    getSolrClient().commit(true, true);
+
+    if (queries != null) {
+      assertQR(fieldName, queries[0], 0);
+      assertQR(fieldName, queries[1], 2);
+      assertQR(fieldName, queries[2], 2);
+      assertQR(fieldName, queries[3], 1);
+    }
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[1]), vc.apply(values[2])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[0]), vc.apply(values[3]))));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(3, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[1]), vc.apply(values[2]), vc.apply(values[3])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[0]))));
+
+    getSolrClient().add(sdoc("id", "20001", fieldName, ImmutableMap.of("remove",
+        Lists.newArrayList(values[0], values[1], values[2]))));
+    getSolrClient().commit(true, true);
+
+    if (queries != null) {
+      assertQR(fieldName, queries[0], 0);
+      assertQR(fieldName, queries[1], 1);
+      assertQR(fieldName, queries[2], 1);
+      assertQR(fieldName, queries[3], 1);
+    }
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[1]), vc.apply(values[2])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[0]), vc.apply(values[3]))));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(1, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[3])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[0]), vc.apply(values[1]), vc.apply(values[2]))));
+
+    getSolrClient().add(Arrays.asList(sdoc("id", "20000", fieldName, ImmutableMap.of("add",
+        Lists.newArrayList(values[0]), "remove", Lists.newArrayList(values[1], values[2]))),
+        sdoc("id", "20001", fieldName,
+            ImmutableMap.of("add", Lists.newArrayList(values[0]), "remove", Lists.newArrayList(values[3])))));
+    getSolrClient().commit(true, true);
+
+    if (queries != null) {
+      assertQR(fieldName, queries[0], 2);
+      assertQR(fieldName, queries[1], 0);
+      assertQR(fieldName, queries[2], 0);
+      assertQR(fieldName, queries[3], 0);
+    }
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(1, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[0])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[1]), vc.apply(values[2]), vc.apply(values[3]))));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(1, fieldValues.size());
+    assertThat(fieldValues, hasItems(vc.apply(values[0])));
+    assertThat(fieldValues, not(hasItems(vc.apply(values[1]), vc.apply(values[2]), vc.apply(values[3]))));
+
+    getSolrClient().add(Arrays.asList(sdoc("id", "20000", fieldName, ImmutableMap.of("set",
+        Lists.newArrayList(values[0], values[1], values[2], values[3]))), sdoc("id", "20001", fieldName,
+            ImmutableMap.of("set",
+                Lists.newArrayList(values[0], values[1], values[2], values[3])))));
+    getSolrClient().commit(true, true);
+
+    if (queries != null) {
+      assertQR(fieldName, queries[0], 2);
+      assertQR(fieldName, queries[1], 2);
+      assertQR(fieldName, queries[2], 2);
+      assertQR(fieldName, queries[3], 2);
+    }
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(4, fieldValues.size());
+    assertThat(fieldValues,
+        hasItems(vc.apply(values[0]), vc.apply(values[1]), vc.apply(values[2]), vc.apply(values[3])));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(4, fieldValues.size());
+    assertThat(fieldValues,
+        hasItems(vc.apply(values[0]), vc.apply(values[1]), vc.apply(values[2]), vc.apply(values[3])));
+  }
+
+  private String[] toStringArray(final Object[] values) {
+    return Arrays.stream(values).map(v -> v.toString()).collect(Collectors.toList()).toArray(new String[] {});
+  }
+
+  private void runTestForFieldWithQuery(final String fieldName, final Object[] values)
+      throws SolrServerException, IOException {
+    runTestForField(fieldName, values, toStringArray(values), Optional.empty());
+  }
+
+  private void runTestForFieldWithQuery(final String fieldName, final Object[] values, final String[] queries)
+      throws SolrServerException, IOException {
+    runTestForField(fieldName, values, queries, Optional.empty());
+  }
+
+  private void runTestForFieldWithQuery(final String fieldName, final Object[] values, final String[] queries,
+      final Function<Object,Object> valueConverter)
+      throws SolrServerException, IOException {
+    runTestForField(fieldName, values, queries, Optional.of(valueConverter));
+  }
+
+  private void runTestForFieldWithoutQuery(final String fieldName, final Object[] values)
+      throws SolrServerException, IOException {
+    runTestForField(fieldName, values, null, Optional.empty());
+  }
+
+  @Test
+  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-13762")
+  public void testMultivalueBinaryField() throws SolrServerException, IOException {
+    runTestForFieldWithoutQuery("binaryRemove",
+        new byte[][] {new byte[] {0}, new byte[] {1}, new byte[] {2}, new byte[] {3}});
+  }
+
+  @Test
+  public void testMultivalueBooleanField() throws SolrServerException, IOException {
+
+    final String fieldName = "booleanRemove";
+
+    getSolrClient().add(Arrays.asList(
+        sdoc("id", "20000", fieldName, Lists.newArrayList(true, false)),
+        sdoc("id", "20001", fieldName, Lists.newArrayList(false, true))));
+    getSolrClient().commit(true, true);
+
+    assertQR(fieldName, "true", 2);
+    assertQR(fieldName, "false", 2);
+
+    Collection<Object> fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(true, false));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(true, false));
+
+    getSolrClient().add(sdoc("id", "20000", fieldName, ImmutableMap.of("remove",
+        Lists.newArrayList(false))));
+    getSolrClient().commit(true, true);
+
+    assertQR(fieldName, "true", 2);
+    assertQR(fieldName, "false", 1);
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(1, fieldValues.size());
+    assertThat(fieldValues, hasItems(true));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(true, false));
+
+    getSolrClient().add(sdoc("id", "20001", fieldName, ImmutableMap.of("remove",
+        Lists.newArrayList(true, false))));
+    getSolrClient().commit(true, true);
+
+    assertQR(fieldName, "true", 1);
+    assertQR(fieldName, "false", 0);
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(1, fieldValues.size());
+    assertThat(fieldValues, hasItems(true));
+    assertThat(fieldValues, not(hasItems(false)));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertNull(fieldValues);
+
+    getSolrClient().add(Arrays.asList(sdoc("id", "20000", fieldName, ImmutableMap.of("add",
+        Lists.newArrayList(false, false)))));
+    getSolrClient().commit(true, true);
+
+    assertQR(fieldName, "true", 1);
+    assertQR(fieldName, "false", 1);
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(3, fieldValues.size());
+    assertThat(fieldValues, hasItems(true, false));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertNull(fieldValues);
+
+    getSolrClient().add(Arrays.asList(sdoc("id", "20000", fieldName, ImmutableMap.of("set",
+        Lists.newArrayList(true, false))), sdoc("id", "20001", fieldName,
+            ImmutableMap.of("set",
+                Lists.newArrayList(false, true)))));
+    getSolrClient().commit(true, true);
+
+    assertQR(fieldName, "true", 2);
+    assertQR(fieldName, "false", 2);
+
+    fieldValues = getSolrClient().getById("20000").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(true, false));
+    fieldValues = getSolrClient().getById("20001").getFieldValues(fieldName);
+    assertEquals(2, fieldValues.size());
+    assertThat(fieldValues, hasItems(true, false));
+  }
+
+  @Test
+  public void testMultivalueCollationField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("collationRemove", new String[] {"cf1", "cf2", "cf3", "cf4"});
+  }
+
+  @Test
+  public void testMultivalueDatePointField() throws SolrServerException, IOException {
+
+    final String s1 = "1980-01-01T00:00:00Z";
+    final Date d1 = Date.from(ZonedDateTime.parse(s1).toInstant());
+    final String s2 = "1990-01-01T00:00:00Z";
+    final Date d2 = Date.from(ZonedDateTime.parse(s2).toInstant());
+    final String s3 = "2000-01-01T00:00:00Z";
+    final Date d3 = Date.from(ZonedDateTime.parse(s3).toInstant());
+    final String s4 = "2010-01-01T00:00:00Z";
+    final Date d4 = Date.from(ZonedDateTime.parse(s4).toInstant());
+
+    runTestForFieldWithQuery("datePointRemove", new Date[] {d1, d2, d3, d4},
+        new String[] {"\"" + s1 + "\"", "\"" + s2 + "\"", "\"" + s3 + "\"", "\"" + s4 + "\""});
+  }
+
+  @Test
+  public void testMultivalueDateRangeField() throws SolrServerException, IOException {
+
+    final String s1 = "1980-01-01T00:00:00Z";
+    final String s2 = "1990-01-01T00:00:00Z";
+    final String s3 = "2000-01-01T00:00:00Z";
+    final String s4 = "2010-01-01T00:00:00Z";
+
+    runTestForFieldWithQuery("dateRangeRemove", new String[] {s1, s2, s3, s4},
+        new String[] {"\"" + s1 + "\"", "\"" + s2 + "\"", "\"" + s3 + "\"", "\"" + s4 + "\""});
+  }
+
+  @Test
+  public void testMultivalueDoublePointField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("doublePointRemove", new Double[] {1.0d, 2.0d, 3.0d, 4.0d});
+  }
+
+  @Test
+  public void testMultivalueEnumField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("enumRemove_sev_enum", new Object[] {"Low", "Medium", "High", "Critical"});
+  }
+
+  @Test
+  public void testMultivalueEnumFieldWithNumbers() throws SolrServerException, IOException {
+    final Object[] values = new Object[] {"Low", "Medium", "High", 11};
+    runTestForFieldWithQuery("enumRemove_sev_enum", values, toStringArray(values), o -> {
+      if (Integer.valueOf(11).equals(o)) {
+        return "Critical";
+      } else {
+        return o;
+      }
+    });
+  }
+
+  @Test
+  public void testMultivalueExternalFileField() throws SolrServerException, IOException {
+    runTestForFieldWithoutQuery("externalFileRemove",
+        new String[] {"file1.txt", "file2.txt", "file3.txt", "file4.txt"});
+  }
+
+  @Test
+  public void testMultivalueFloatPointField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("floatPointRemove", new Float[] {1.0f, 2.0f, 3.0f, 4.0f});
+  }
+
+  @Test
+  public void testMultivalueICUCollationField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("icuCollationRemove", new String[] {"iuccf1", "icucf2", "icucf3", "icucf4"});
+  }
+
+  @Test
+  public void testMultivalueIntPointField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("intPointRemove", new Integer[] {1, 2, 3, 4});
+  }
+
+  @Test
+  public void testMultivalueLatLonPointSpatialField() throws SolrServerException, IOException {
+    runTestForFieldWithoutQuery("latLonPointSpatialRemove",
+        new String[] {"1.0,-1.0", "2.0,-2.0", "3.0,-3.0", "4.0,-4.0"});
+  }
+
+  @Test
+  public void testMultivalueLatLonField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("latLonRemove", new String[] {"1.0,-1.0", "2.0,-2.0", "3.0,-3.0", "4.0,-4.0"});
+  }
+
+  @Test
+  public void testMultivalueLongPointField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("longPointRemove", new Long[] {1l, 2l, 3l, 4l});
+  }
+
+  @Test
+  public void testMultivaluePointField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("pointRemove", new String[] {"1,1", "2,2", "3,3", "4,4"});
+  }
+
+  @Test
+  public void testMultivalueRandomSortField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("randomSortRemove", new String[] {"rsf1", "rsf2", "rsf3", "rsf4"});
+  }
+
+  @Test
+  public void testMultivalueSpatialRecursivePrefixTreeFieldType() throws SolrServerException, IOException {
+    runTestForFieldWithoutQuery("spatialRecursivePrefixTreeRemove", new String[] {"1,1", "2,2", "3,3", "4,4"});
+  }
+
+  @Test
+  public void testMultivalueStringField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("stringRemove", new String[] {"str1", "str2", "str3", "str4"});
+  }
+
+  @Test
+  public void testMultivalueStringFieldUsingCharSequence() throws SolrServerException, IOException {
+    final ByteArrayUtf8CharSequence[] values = new ByteArrayUtf8CharSequence[] {new ByteArrayUtf8CharSequence("str1"),
+        new ByteArrayUtf8CharSequence("str2"),
+        new ByteArrayUtf8CharSequence("str3"), new ByteArrayUtf8CharSequence("str4")};
+    runTestForFieldWithQuery("stringRemove", values, toStringArray(values), o -> o.toString());
+  }
+
+  @Test
+  public void testMultivalueTextField() throws SolrServerException, IOException {
+    runTestForFieldWithQuery("textRemove", new String[] {"text1", "text2", "text3", "text4"});
+  }
+
+  @Test
+  public void testMultivalueUUIDField() throws SolrServerException, IOException {
+    final String[] values = new String[] {UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+        UUID.randomUUID().toString(), UUID.randomUUID().toString()};
+    runTestForFieldWithQuery("uuidRemove", values);
+  }
+
+}
diff --git a/solr/core/src/test/org/apache/solr/update/processor/JavaBinAtomicUpdateMultivalueTest.java b/solr/core/src/test/org/apache/solr/update/processor/JavaBinAtomicUpdateMultivalueTest.java
new file mode 100644
index 0000000..5f9889e
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/update/processor/JavaBinAtomicUpdateMultivalueTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.update.processor;
+
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer.RequestWriterSupplier;
+
+public class JavaBinAtomicUpdateMultivalueTest extends AbstractAtomicUpdatesMultivalueTestBase {
+
+  @Override
+  RequestWriterSupplier getRequestWriterSupplier() {
+    return RequestWriterSupplier.JavaBin;
+  }
+
+}
diff --git a/solr/core/src/test/org/apache/solr/update/processor/XMLAtomicUpdateMultivalueTest.java b/solr/core/src/test/org/apache/solr/update/processor/XMLAtomicUpdateMultivalueTest.java
new file mode 100644
index 0000000..1a5f62b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/update/processor/XMLAtomicUpdateMultivalueTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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.update.processor;
+
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer.RequestWriterSupplier;
+
+public class XMLAtomicUpdateMultivalueTest extends AbstractAtomicUpdatesMultivalueTestBase {
+
+  @Override
+  RequestWriterSupplier getRequestWriterSupplier() {
+    return RequestWriterSupplier.XML;
+  }
+
+}
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingEmbeddedTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingEmbeddedTest.java
new file mode 100644
index 0000000..606debb
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingEmbeddedTest.java
@@ -0,0 +1,592 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.io.File;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.solr.EmbeddedSolrServerTestBase;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.client.solrj.response.json.BucketJsonFacet;
+import org.apache.solr.client.solrj.response.json.NestableJsonFacet;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressSSL
+public class DirectJsonQueryRequestFacetingEmbeddedTest extends EmbeddedSolrServerTestBase {
+
+  private static final String COLLECTION_NAME = "techproducts";
+  private static final int NUM_TECHPRODUCTS_DOCS = 32;
+  private static final int NUM_IN_STOCK = 17;
+  private static final int NUM_ELECTRONICS = 12;
+  private static final int NUM_CURRENCY = 4;
+  private static final int NUM_MEMORY = 3;
+  private static final int NUM_CORSAIR = 3;
+  private static final int NUM_BELKIN = 2;
+  private static final int NUM_CANON = 2;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    final String sourceHome = ExternalPaths.SOURCE_HOME;
+
+    final File tempSolrHome = LuceneTestCase.createTempDir().toFile();
+    FileUtils.copyFileToDirectory(new File(sourceHome, "server/solr/solr.xml"), tempSolrHome);
+    final File collectionDir = new File(tempSolrHome, COLLECTION_NAME);
+    FileUtils.forceMkdir(collectionDir);
+    final File configSetDir = new File(sourceHome, "server/solr/configsets/sample_techproducts_configs/conf");
+    FileUtils.copyDirectoryToDirectory(configSetDir, collectionDir);
+
+    final Properties props = new Properties();
+    props.setProperty("name", COLLECTION_NAME);
+
+    try (Writer writer = new OutputStreamWriter(FileUtils.openOutputStream(new File(collectionDir, "core.properties")),
+        "UTF-8");) {
+      props.store(writer, null);
+    }
+
+    final String config = tempSolrHome.getAbsolutePath() + "/" + COLLECTION_NAME + "/conf/solrconfig.xml";
+    final String schema = tempSolrHome.getAbsolutePath() + "/" + COLLECTION_NAME + "/conf/managed-schema";
+    initCore(config, schema, tempSolrHome.getAbsolutePath(), COLLECTION_NAME);
+
+    client = new EmbeddedSolrServer(h.getCoreContainer(), COLLECTION_NAME) {
+      @Override
+      public void close() {
+        // do not close core container
+      }
+    };
+
+    ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update");
+    up.setParam("collection", COLLECTION_NAME);
+    up.addFile(getFile("solrj/techproducts.xml"), "application/xml");
+    up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
+    UpdateResponse updateResponse = up.process(client);
+    assertEquals(0, updateResponse.getStatus());
+  }
+
+  @Test
+  public void testSingleTermsFacet() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, topLevelFacetData.getCount());
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_cats",
+        new FacetBucket("electronics", NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testMultiTermsFacet() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "    },",
+        "    'top_manufacturers': {",
+        "      'type': 'terms',",
+        "      'field': 'manu_id_s',",
+        "      'limit': 3",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, topLevelFacetData.getCount());
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_cats",
+        new FacetBucket("electronics", NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_manufacturers",
+        new FacetBucket("corsair", NUM_CORSAIR),
+        new FacetBucket("belkin", NUM_BELKIN),
+        new FacetBucket("canon", NUM_CANON));
+  }
+
+  @Test
+  public void testSingleRangeFacet() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'prices': {",
+        "      'type': 'range',",
+        "      'field': 'price',",
+        "      'start': 0,",
+        "      'end': 100,",
+        "      'gap': 20",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, topLevelFacetData.getCount());
+    assertHasFacetWithBucketValues(topLevelFacetData, "prices",
+        new FacetBucket(0.0f, 5),
+        new FacetBucket(20.0f, 0),
+        new FacetBucket(40.0f, 0),
+        new FacetBucket(60.0f, 1),
+        new FacetBucket(80.0f, 1));
+  }
+
+  @Test
+  public void testMultiRangeFacet() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'prices': {",
+        "      'type': 'range',",
+        "      'field': 'price',",
+        "      'start': 0,",
+        "      'end': 100,",
+        "      'gap': 20",
+        "    },",
+        "    'shipping_weights': {",
+        "      'type': 'range',",
+        "      'field': 'weight',",
+        "      'start': 0,",
+        "      'end': 200,",
+        "      'gap': 50",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, topLevelFacetData.getCount());
+    assertHasFacetWithBucketValues(topLevelFacetData, "prices",
+        new FacetBucket(0.0f, 5),
+        new FacetBucket(20.0f, 0),
+        new FacetBucket(40.0f, 0),
+        new FacetBucket(60.0f, 1),
+        new FacetBucket(80.0f, 1));
+    assertHasFacetWithBucketValues(topLevelFacetData, "shipping_weights",
+        new FacetBucket(0.0f, 6),
+        new FacetBucket(50.0f, 0),
+        new FacetBucket(100.0f, 0),
+        new FacetBucket(150.0f, 1));
+  }
+
+  @Test
+  public void testSingleStatFacet() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'sum_price': 'sum(price)'",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasStatFacetWithValue(topLevelFacetData, "sum_price", 5251.270030975342);
+  }
+
+  @Test
+  public void testMultiStatFacet() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'sum_price': 'sum(price)',",
+        "    'avg_price': 'avg(price)'",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasStatFacetWithValue(topLevelFacetData, "sum_price", 5251.270030975342);
+    assertHasStatFacetWithValue(topLevelFacetData, "avg_price", 328.20437693595886);
+  }
+
+  @Test
+  public void testMultiFacetsMixedTypes() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'avg_price': 'avg(price)',",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasStatFacetWithValue(topLevelFacetData, "avg_price", 328.20437693595886);
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_cats",
+        new FacetBucket("electronics", NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testNestedTermsFacet() throws Exception {
+    final String subfacetName = "top_manufacturers_for_cat";
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'facet': {",
+        "        'top_manufacturers_for_cat': {",
+        "          'type': 'terms',",
+        "          'field': 'manu_id_s',",
+        "          'limit': 1",
+        "        }",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    // Test top level facets
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_cats",
+        new FacetBucket("electronics", NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+    // Test subfacet values for each top-level facet bucket
+    final List<BucketJsonFacet> catBuckets = topLevelFacetData.getBucketBasedFacets("top_cats").getBuckets();
+    assertHasFacetWithBucketValues(catBuckets.get(0), subfacetName, new FacetBucket("corsair", 3));
+    assertHasFacetWithBucketValues(catBuckets.get(1), subfacetName, new FacetBucket("boa", 1));
+    assertHasFacetWithBucketValues(catBuckets.get(2), subfacetName, new FacetBucket("corsair", 3));
+  }
+
+  @Test
+  public void testNestedFacetsOfMixedTypes() throws Exception {
+    final String subfacetName = "avg_price_for_cat";
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'facet': {",
+        "        'avg_price_for_cat': 'avg(price)'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    // Test top level facets
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_cats",
+        new FacetBucket("electronics", NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+    // Test subfacet values for each top-level facet bucket
+    final List<BucketJsonFacet> catBuckets = topLevelFacetData.getBucketBasedFacets("top_cats").getBuckets();
+    assertHasStatFacetWithValue(catBuckets.get(0), subfacetName, 252.02909261530095); // electronics
+    assertHasStatFacetWithValue(catBuckets.get(1), subfacetName, 0.0); // currency
+    assertHasStatFacetWithValue(catBuckets.get(2), subfacetName, 129.99499893188477); // memory
+  }
+
+  @Test
+  public void testFacetWithDomainFilteredBySimpleQueryString() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_popular_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'domain': {",
+        "        'filter': 'popularity:[5 TO 10]'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_popular_cats",
+        new FacetBucket("electronics", 9),
+        new FacetBucket("graphics card", 2),
+        new FacetBucket("hard drive", 2));
+  }
+
+  @Test
+  public void testFacetWithDomainFilteredByLocalParamsQueryString() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_popular_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'domain': {",
+        "        'filter': '{!lucene df=\"popularity\" v=\"[5 TO 10]\"}'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_TECHPRODUCTS_DOCS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_popular_cats",
+        new FacetBucket("electronics", 9),
+        new FacetBucket("graphics card", 2),
+        new FacetBucket("hard drive", 2));
+  }
+
+  @Test
+  public void testFacetWithArbitraryDomainFromQueryString() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'domain': {",
+        "        'query': '*:*'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_ELECTRONICS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "top_cats",
+        new FacetBucket("electronics", NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testFacetWithArbitraryDomainFromLocalParamsQuery() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'largest_search_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'domain': {",
+        "        'query': '{!lucene df=\"cat\" v=\"search\"}'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_ELECTRONICS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "largest_search_cats",
+        new FacetBucket("search", 2),
+        new FacetBucket("software", 2));
+  }
+
+  @Test
+  public void testFacetWithMultipleSimpleQueryClausesInArbitraryDomain() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'cats_matching_solr': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'domain': {",
+        "        'query': ['cat:search', 'name:Solr']",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_ELECTRONICS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "cats_matching_solr",
+        new FacetBucket("search", 1),
+        new FacetBucket("software", 1));
+  }
+
+  @Test
+  public void testFacetWithMultipleLocalParamsQueryClausesInArbitraryDomain() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'cats_matching_solr': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'domain': {",
+        "        'query': ['{!lucene df=\"cat\" v=\"search\"}', '{!lucene df=\"name\" v=\"Solr\"}']",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_ELECTRONICS, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "cats_matching_solr",
+        new FacetBucket("search", 1),
+        new FacetBucket("software", 1));
+  }
+
+  @Test
+  public void testFacetWithDomainWidenedUsingExcludeTagsToIgnoreFilters() throws Exception {
+    final String jsonBody = String.join("\n", "{",
+        "  'query': '*:*',",
+        "  'filter': {'#on_shelf': 'inStock:true'},",
+        "  'facet': {",
+        "    'in_stock_only': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 2",
+        "    }",
+        "    'all': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 2,",
+        "      'domain': {",
+        "        'excludeTags': 'on_shelf'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+
+    QueryResponse response = request.process(getSolrClient(), COLLECTION_NAME);
+
+    assertExpectedDocumentsFoundAndReturned(response, NUM_IN_STOCK, 10);
+    final NestableJsonFacet topLevelFacetData = response.getJsonFacetingResponse();
+    assertHasFacetWithBucketValues(topLevelFacetData, "in_stock_only",
+        new FacetBucket("electronics", 8),
+        new FacetBucket("currency", 4));
+    assertHasFacetWithBucketValues(topLevelFacetData, "all",
+        new FacetBucket("electronics", 12),
+        new FacetBucket("currency", 4));
+  }
+
+  private class FacetBucket {
+    private final Object val;
+    private final int count;
+
+    FacetBucket(Object val, int count) {
+      this.val = val;
+      this.count = count;
+    }
+
+    public Object getVal() {
+      return val;
+    }
+
+    public int getCount() {
+      return count;
+    }
+  }
+
+  private void assertHasFacetWithBucketValues(NestableJsonFacet response, String expectedFacetName,
+      FacetBucket... expectedBuckets) {
+    assertTrue("Expected response to have facet with name " + expectedFacetName,
+        response.getBucketBasedFacets(expectedFacetName) != null);
+    final List<BucketJsonFacet> buckets = response.getBucketBasedFacets(expectedFacetName).getBuckets();
+    assertEquals(expectedBuckets.length, buckets.size());
+    for (int i = 0; i < expectedBuckets.length; i++) {
+      final FacetBucket expectedBucket = expectedBuckets[i];
+      final BucketJsonFacet actualBucket = buckets.get(i);
+      assertEquals(expectedBucket.getVal(), actualBucket.getVal());
+      assertEquals(expectedBucket.getCount(), actualBucket.getCount());
+    }
+  }
+
+  private void assertHasStatFacetWithValue(NestableJsonFacet response, String expectedFacetName,
+      Double expectedStatValue) {
+    assertTrue("Expected response to have stat facet named '" + expectedFacetName + "'",
+        response.getStatValue(expectedFacetName) != null);
+    assertEquals(expectedStatValue, response.getStatValue(expectedFacetName));
+  }
+
+  private void assertExpectedDocumentsFoundAndReturned(QueryResponse response, int expectedNumFound,
+      int expectedReturned) {
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList documents = response.getResults();
+    assertEquals(expectedNumFound, documents.getNumFound());
+    assertEquals(expectedReturned, documents.size());
+  }
+}