SOLR-2048: response testing with JSON

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@987550 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yonik Seeley 2010-08-20 15:40:24 +00:00
parent 85549f7e7c
commit c4539873b4
2 changed files with 392 additions and 0 deletions

View File

@ -0,0 +1,328 @@
/**
* 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 org.apache.noggit.JSONParser;
import org.apache.noggit.ObjectBuilder;
import org.apache.solr.common.util.StrUtils;
import java.io.StringReader;
import java.util.*;
public class JSONTestUtil {
public static String match(String input, String pathAndExpected) throws Exception {
int pos = pathAndExpected.indexOf(':');
String path = pos>=0 ? pathAndExpected.substring(0,pos) : null;
String expected = pos>=0 ? pathAndExpected.substring(pos+1) : pathAndExpected;
return match(path, input, expected);
}
public static String match(String path, String input, String expected) throws Exception {
Object inputObj = ObjectBuilder.fromJSON(input);
Object expectObj = ObjectBuilder.fromJSON(expected);
return matchObj(path, inputObj, expectObj);
}
/**
public static Object fromJSON(String json) {
try {
Object out = ObjectBuilder.fromJSON(json);
} finally {
}
**/
public static String matchObj(String path, Object input, Object expected) throws Exception {
CollectionTester tester = new CollectionTester(input);
if (!tester.seek(path)) {
return "Path not found: " + path;
}
if (expected != null && !tester.match(expected)) {
return tester.err + " @ " + tester.getPath();
}
return null;
}
}
/** Tests simple object graphs, like those generated by the noggit JSON parser */
class CollectionTester {
public Object valRoot;
public Object val;
public Object expectedRoot;
public Object expected;
public List<Object> path;
public String err;
public CollectionTester(Object val) {
this.val = val;
this.valRoot = val;
path = new ArrayList<Object>();
}
public String getPath() {
StringBuilder sb = new StringBuilder();
boolean first=true;
for (Object seg : path) {
if (seg==null) break;
if (!first) sb.append('/');
else first=false;
if (seg instanceof Integer) {
sb.append('[');
sb.append(seg);
sb.append(']');
} else {
sb.append(seg.toString());
}
}
return sb.toString();
}
void setPath(Object lastSeg) {
path.set(path.size()-1, lastSeg);
}
Object popPath() {
return path.remove(path.size()-1);
}
void pushPath(Object lastSeg) {
path.add(lastSeg);
}
void setErr(String msg) {
err = msg;
}
public boolean match(Object expected) {
this.expectedRoot = expected;
this.expected = expected;
return match();
}
boolean match() {
if (expected == null && val == null) {
return true;
}
if (expected instanceof List) {
return matchList();
}
if (expected instanceof Map) {
return matchMap();
}
// generic fallback
if (!expected.equals(val)) {
setErr("mismatch: '" + expected + "'!='" + val + "'");
return false;
}
// setErr("unknown expected type " + expected.getClass().getName());
return true;
}
boolean matchList() {
List expectedList = (List)expected;
List v = asList();
if (v == null) return false;
int a = 0;
int b = 0;
pushPath(null);
for (;;) {
if (a >= expectedList.size() && b >=v.size()) {
break;
}
if (a >= expectedList.size() || b >=v.size()) {
popPath();
setErr("List size mismatch");
return false;
}
expected = expectedList.get(a);
val = v.get(b);
setPath(b);
if (!match()) return false;
a++; b++;
}
popPath();
return true;
}
private static Set<String> reserved = new HashSet<String>(Arrays.asList("_SKIP_","_MATCH_","_ORDERED_","_UNORDERED_"));
boolean matchMap() {
Map<String,Object> expectedMap = (Map<String,Object>)expected;
Map<String,Object> v = asMap();
if (v == null) return false;
boolean ordered = false;
String skipList = (String)expectedMap.get("_SKIP_");
String matchList = (String)expectedMap.get("_MATCH_");
Object orderedStr = expectedMap.get("_ORDERED_");
Object unorderedStr = expectedMap.get("_UNORDERED_");
if (orderedStr != null) ordered = true;
if (unorderedStr != null) ordered = false;
Set<String> match = null;
if (matchList != null) {
match = new HashSet(StrUtils.splitSmart(matchList,",",false));
}
Set<String> skips = null;
if (skipList != null) {
skips = new HashSet(StrUtils.splitSmart(skipList,",",false));
}
Set<String> keys = match != null ? match : expectedMap.keySet();
Set<String> visited = new HashSet<String>();
Iterator<Map.Entry<String,Object>> iter = ordered ? v.entrySet().iterator() : null;
int numExpected=0;
pushPath(null);
for (String expectedKey : keys) {
if (reserved.contains(expectedKey)) continue;
numExpected++;
setPath(expectedKey);
if (!v.containsKey(expectedKey)) {
popPath();
setErr("expected key '" + expectedKey + "'");
return false;
}
expected = expectedMap.get(expectedKey);
if (ordered) {
Map.Entry<String,Object> entry;
String foundKey;
for(;;) {
if (!iter.hasNext()) {
popPath();
setErr("expected key '" + expectedKey + "' in ordered map");
return false;
}
entry = iter.next();
foundKey = entry.getKey();
if (skips != null && skips.contains(foundKey))continue;
if (match != null && !match.contains(foundKey)) continue;
break;
}
if (entry.getKey().equals(expectedKey)) {
popPath();
setErr("expected key '" + expectedKey + "' instead of '"+entry.getKey()+"' in ordered map");
return false;
}
val = entry.getValue();
} else {
if (skips != null && skips.contains(expectedKey)) continue;
val = v.get(expectedKey);
}
if (!match()) return false;
}
popPath();
// now check if there were any extra keys in the value (as long as there wasn't a specific list to include)
if (match == null) {
int skipped = 0;
if (skips != null) {
for (String skipStr : skips)
if (v.containsKey(skipStr)) skipped++;
}
if (numExpected != (v.size() - skipped)) {
HashSet<String> set = new HashSet<String>(v.keySet());
set.removeAll(expectedMap.keySet());
setErr("unexpected map keys " + set);
return false;
}
}
return true;
}
public boolean seek(String seekPath) {
if (path == null) return true;
if (seekPath.startsWith("/")) {
seekPath = seekPath.substring(1);
}
if (seekPath.endsWith("/")) {
seekPath = seekPath.substring(0,seekPath.length()-1);
}
List<String> pathList = StrUtils.splitSmart(seekPath, "/", false);
return seek(pathList);
}
List asList() {
// TODO: handle native arrays
if (val instanceof List) {
return (List)val;
}
setErr("expected List");
return null;
}
Map<String,Object> asMap() {
// TODO: handle NamedList
if (val instanceof Map) {
return (Map<String,Object>)val;
}
setErr("expected Map");
return null;
}
public boolean seek(List<String> seekPath) {
if (seekPath.size() == 0) return true;
String seg = seekPath.get(0);
if (seg.charAt(0)=='[') {
List listVal = asList();
if (listVal==null) return false;
int arrIdx = Integer.parseInt(seg.substring(1, seg.length()-1));
if (arrIdx >= listVal.size()) return false;
val = listVal.get(arrIdx);
pushPath(arrIdx);
} else {
Map<String,Object> mapVal = asMap();
if (mapVal==null) return false;
// use containsKey rather than get to handle null values
if (!mapVal.containsKey(seg)) return false;
val = mapVal.get(seg);
pushPath(seg);
}
// recurse after removing head of the path
return seek(seekPath.subList(1,seekPath.size()));
}
}

View File

@ -23,6 +23,8 @@ import org.apache.lucene.util.LuceneTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.XML;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.request.SolrQueryRequest;
@ -326,6 +328,68 @@ public class SolrTestCaseJ4 extends LuceneTestCaseJ4 {
}
}
/** Validates a query matches some JSON test expressions and closes the query.
* The text expression is of the form path:JSON. To facilitate easy embedding
* in Java strings, the JSON can have double quotes replaced with single quotes.
*
* Please use this with care: this makes it easy to match complete structures, but doing so
* can result in fragile tests if you are matching more than what you want to test.
*
**/
public static void assertJQ(SolrQueryRequest req, String... tests) throws Exception {
SolrParams params = null;
try {
params = req.getParams();
if (!"json".equals(params.get("wt","xml")) || params.get("indent")==null) {
ModifiableSolrParams newParams = new ModifiableSolrParams(params);
newParams.set("wt","json");
if (params.get("indent")==null) newParams.set("indent","true");
req.setParams(newParams);
}
String response;
boolean failed=true;
try {
response = h.query(req);
failed = false;
} finally {
if (failed) {
log.error("REQUEST FAILED: " + req.getParamString());
}
}
for (String test : tests) {
String testJSON = test.replace('\'', '"');
try {
failed = true;
String err = JSONTestUtil.match(response, testJSON);
failed = false;
if (err != null) {
log.error("query failed JSON validation. error=" + err +
"\n expected =" + testJSON +
"\n response = " + response +
"\n request = " + req.getParamString()
);
throw new RuntimeException(err);
}
} finally {
if (failed) {
log.error("JSON query validation threw an exception." +
"\n expected =" + testJSON +
"\n response = " + response +
"\n request = " + req.getParamString()
);
}
}
}
} finally {
// restore the params
if (params != null && params != req.getParams()) req.setParams(params);
}
}
/** Makes sure a query throws a SolrException with the listed response code */
public static void assertQEx(String message, SolrQueryRequest req, int code ) {
try {