Skip to content

Commit

Permalink
solution for backwards compatibility #2364
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrthomas committed Sep 21, 2023
1 parent 1f0df99 commit fc068d8
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 32 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2276,6 +2276,7 @@ You can adjust configuration settings for the HTTP client used by Karate using t
`xmlNamespaceAware` | boolean | defaults to `false`, to handle XML namespaces in [some special circumstances](https://github.com/karatelabs/karate/issues/1587)
`abortSuiteOnFailure` | boolean | defaults to `false`, to not attempt to run any more tests upon a failure
`ntlmAuth` | JSON | See [NTLM Authentication](#ntlm-authentication)
`matchEachEmptyAllowed` | boolean | defaults to `false`, [`match each`](#match-each) by default expects the array to be non-empty, refer to [this issue](https://github.com/karatelabs/karate/issues/2364) to understand why you may want to over-ride this.

Examples:
```cucumber
Expand Down
18 changes: 10 additions & 8 deletions karate-core/src/main/java/com/intuit/karate/Match.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,33 +155,35 @@ static class Context {
final String path;
final String name;
final int index;
final boolean matchEachEmptyAllowed;

Context(JsEngine js, MatchOperation root, boolean xml, int depth, String path, String name, int index) {
Context(JsEngine js, MatchOperation root, boolean xml, int depth, String path, String name, int index, boolean matchEachEmptyAllowed) {
this.JS = js;
this.root = root;
this.xml = xml;
this.depth = depth;
this.path = path;
this.name = name;
this.index = index;
this.matchEachEmptyAllowed = matchEachEmptyAllowed;
}

Context descend(String name) {
if (xml) {
String childPath = path.endsWith("/@") ? path + name : (depth == 0 ? "" : path) + "/" + name;
return new Context(JS, root, xml, depth + 1, childPath, name, -1);
return new Context(JS, root, xml, depth + 1, childPath, name, -1, matchEachEmptyAllowed);
} else {
boolean needsQuotes = name.indexOf('-') != -1 || name.indexOf(' ') != -1 || name.indexOf('.') != -1;
String childPath = needsQuotes ? path + "['" + name + "']" : path + '.' + name;
return new Context(JS, root, xml, depth + 1, childPath, name, -1);
return new Context(JS, root, xml, depth + 1, childPath, name, -1, matchEachEmptyAllowed);
}
}

Context descend(int index) {
if (xml) {
return new Context(JS, root, xml, depth + 1, path + "[" + (index + 1) + "]", name, index);
return new Context(JS, root, xml, depth + 1, path + "[" + (index + 1) + "]", name, index, matchEachEmptyAllowed);
} else {
return new Context(JS, root, xml, depth + 1, path + "[" + index + "]", name, index);
return new Context(JS, root, xml, depth + 1, path + "[" + index + "]", name, index, matchEachEmptyAllowed);
}
}

Expand Down Expand Up @@ -363,7 +365,7 @@ public String toString() {
}

public Result is(Type matchType, Object expected) {
MatchOperation mo = new MatchOperation(matchType, this, new Value(parseIfJsonOrXmlString(expected), exceptionOnMatchFailure));
MatchOperation mo = new MatchOperation(matchType, this, new Value(parseIfJsonOrXmlString(expected), exceptionOnMatchFailure), false);
mo.execute();
if (mo.pass) {
return Match.PASS;
Expand Down Expand Up @@ -439,8 +441,8 @@ public Result isEachContainingAny(Object expected) {

}

public static Result execute(JsEngine js, Type matchType, Object actual, Object expected) {
MatchOperation mo = new MatchOperation(js, matchType, new Value(actual), new Value(expected));
public static Result execute(JsEngine js, Type matchType, Object actual, Object expected, boolean matchEachEmptyAllowed) {
MatchOperation mo = new MatchOperation(js, matchType, new Value(actual), new Value(expected), matchEachEmptyAllowed);
mo.execute();
if (mo.pass) {
return PASS;
Expand Down
43 changes: 23 additions & 20 deletions karate-core/src/main/java/com/intuit/karate/MatchOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,40 +52,43 @@ public class MatchOperation {
final Match.Value actual;
final Match.Value expected;
final List<MatchOperation> failures;
// TODO merge this with Match.Type which should be a complex object not an enum
final boolean matchEachEmptyAllowed;

boolean pass = true;
private String failReason;

MatchOperation(Match.Type type, Match.Value actual, Match.Value expected) {
this(JsEngine.global(), null, type, actual, expected);
MatchOperation(Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
this(JsEngine.global(), null, type, actual, expected, matchEachEmptyAllowed);
}

MatchOperation(JsEngine js, Match.Type type, Match.Value actual, Match.Value expected) {
this(js, null, type, actual, expected);
MatchOperation(JsEngine js, Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
this(js, null, type, actual, expected, matchEachEmptyAllowed);
}

MatchOperation(Match.Context context, Match.Type type, Match.Value actual, Match.Value expected) {
this(null, context, type, actual, expected);
MatchOperation(Match.Context context, Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
this(null, context, type, actual, expected, matchEachEmptyAllowed);
}

private MatchOperation(JsEngine js, Match.Context context, Match.Type type, Match.Value actual, Match.Value expected) {
private MatchOperation(JsEngine js, Match.Context context, Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
this.type = type;
this.actual = actual;
this.expected = expected;
this.matchEachEmptyAllowed = matchEachEmptyAllowed;
if (context == null) {
if (js == null) {
js = JsEngine.global();
}
this.failures = new ArrayList();
if (actual.isXml()) {
this.context = new Match.Context(js, this, true, 0, "/", "", -1);
this.context = new Match.Context(js, this, true, 0, "/", "", -1, matchEachEmptyAllowed);
} else {
this.context = new Match.Context(js, this, false, 0, "$", "", -1);
this.context = new Match.Context(js, this, false, 0, "$", "", -1, matchEachEmptyAllowed);
}
} else {
this.context = context;
this.failures = context.root.failures;
}
}
}

private Match.Type fromMatchEach() {
Expand Down Expand Up @@ -159,15 +162,15 @@ boolean execute() {
case EACH_CONTAINS_DEEP:
if (actual.isList()) {
List list = actual.getValue();
if (list.isEmpty()) {
if (list.isEmpty() && !matchEachEmptyAllowed) {
return fail("match each failed, empty array / list");
}
Match.Type nestedMatchType = fromMatchEach();
int count = list.size();
for (int i = 0; i < count; i++) {
Object o = list.get(i);
context.JS.put("_$", o);
MatchOperation mo = new MatchOperation(context.descend(i), nestedMatchType, new Match.Value(o), expected);
MatchOperation mo = new MatchOperation(context.descend(i), nestedMatchType, new Match.Value(o), expected, matchEachEmptyAllowed);
mo.execute();
context.JS.bindings.removeMember("_$");
if (!mo.pass) {
Expand Down Expand Up @@ -198,7 +201,7 @@ boolean execute() {
case CONTAINS_ANY_DEEP:
// don't tamper with strings on the RHS that represent arrays or objects
if (!expected.isList() && !(expected.isString() && expected.isArrayObjectOrReference())) {
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(Collections.singletonList(expected.getValue())));
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(Collections.singletonList(expected.getValue())), matchEachEmptyAllowed);
mo.execute();
return mo.pass ? pass() : fail(mo.failReason);
}
Expand All @@ -208,7 +211,7 @@ boolean execute() {
}
if (expected.isXml() && actual.isMap()) {
// special case, auto-convert rhs
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(XmlUtils.toObject(expected.getValue(), true)));
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(XmlUtils.toObject(expected.getValue(), true)), matchEachEmptyAllowed);
mo.execute();
return mo.pass ? pass() : fail(mo.failReason);
}
Expand Down Expand Up @@ -284,7 +287,7 @@ private boolean macroEqualsExpected(String expStr) {
JsValue jv = context.JS.eval(macro);
context.JS.bindings.removeMember("$");
context.JS.bindings.removeMember("_");
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()));
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()), matchEachEmptyAllowed);
return mo.execute();
} else if (macro.startsWith("[")) {
int closeBracketPos = macro.indexOf(']');
Expand Down Expand Up @@ -321,15 +324,15 @@ private boolean macroEqualsExpected(String expStr) {
macro = "#" + macro;
}
if (macro.startsWith("#")) {
MatchOperation mo = new MatchOperation(context, Match.Type.EACH_EQUALS, actual, new Match.Value(macro));
MatchOperation mo = new MatchOperation(context, Match.Type.EACH_EQUALS, actual, new Match.Value(macro), matchEachEmptyAllowed);
mo.execute();
return mo.pass ? pass() : fail("all array elements matched");
} else { // schema reference
Match.Type nestedType = macroToMatchType(true, macro); // match each
int startPos = matchTypeToStartPos(nestedType);
macro = macro.substring(startPos);
JsValue jv = context.JS.eval(macro);
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()));
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()), matchEachEmptyAllowed);
return mo.execute();
}
}
Expand Down Expand Up @@ -445,7 +448,7 @@ private boolean actualEqualsExpected() {
for (int i = 0; i < actListCount; i++) {
Match.Value actListValue = new Match.Value(actList.get(i));
Match.Value expListValue = new Match.Value(expList.get(i));
MatchOperation mo = new MatchOperation(context.descend(i), Match.Type.EQUALS, actListValue, expListValue);
MatchOperation mo = new MatchOperation(context.descend(i), Match.Type.EQUALS, actListValue, expListValue, matchEachEmptyAllowed);
mo.execute();
if (!mo.pass) {
return fail("array match failed at index " + i);
Expand Down Expand Up @@ -510,7 +513,7 @@ private boolean matchMapValues(Map<String, Object> actMap, Map<String, Object> e
} else {
childMatchType = Match.Type.EQUALS;
}
MatchOperation mo = new MatchOperation(context.descend(key), childMatchType, childActValue, new Match.Value(childExp));
MatchOperation mo = new MatchOperation(context.descend(key), childMatchType, childActValue, new Match.Value(childExp), matchEachEmptyAllowed);
mo.execute();
if (mo.pass) {
if (type == Match.Type.CONTAINS_ANY || type == Match.Type.CONTAINS_ANY_DEEP) {
Expand Down Expand Up @@ -585,7 +588,7 @@ private boolean actualContainsExpected() {
default:
childMatchType = Match.Type.EQUALS;
}
MatchOperation mo = new MatchOperation(context.descend(i), childMatchType, actListValue, expListValue);
MatchOperation mo = new MatchOperation(context.descend(i), childMatchType, actListValue, expListValue, matchEachEmptyAllowed);
mo.execute();
if (mo.pass) {
if (type == Match.Type.CONTAINS_ANY || type == Match.Type.CONTAINS_ANY_DEEP) {
Expand Down
9 changes: 9 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/core/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class Config {
private boolean printEnabled = true;
private boolean pauseIfNotPerf = false;
private boolean abortedStepsShouldPass = false;
private boolean matchEachEmptyAllowed = false;
private Target driverTarget;
private Map<String, Map<String, Object>> customOptions = new HashMap();
private HttpLogModifier logModifier;
Expand Down Expand Up @@ -237,6 +238,9 @@ public boolean configure(String key, Variable value) { // TODO use enum
case "imageComparison":
imageComparisonOptions = value.getValue();
return false;
case "matchEachEmptyAllowed":
matchEachEmptyAllowed = value.getValue();
return false;
case "continueOnStepFailure":
continueOnStepFailureMethods.clear(); // clears previous configuration - in case someone is trying to chain these and forgets resetting the previous one
boolean enableContinueOnStepFailureFeature = false;
Expand Down Expand Up @@ -377,6 +381,7 @@ public Config(Config parent) {
continueAfterContinueOnStepFailure = parent.continueAfterContinueOnStepFailure;
abortSuiteOnFailure = parent.abortSuiteOnFailure;
imageComparisonOptions = parent.imageComparisonOptions;
matchEachEmptyAllowed = parent.matchEachEmptyAllowed;
ntlmEnabled = parent.ntlmEnabled;
ntlmUsername = parent.ntlmUsername;
ntlmPassword = parent.ntlmPassword;
Expand Down Expand Up @@ -616,6 +621,10 @@ public Map<String, Object> getImageComparisonOptions() {
return imageComparisonOptions;
}

public boolean isMatchEachEmptyAllowed() {
return matchEachEmptyAllowed;
}

public boolean isNtlmEnabled() {
return ntlmEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1836,7 +1836,7 @@ private Match.Result matchHeader(Match.Type matchType, String name, String exp)
}

public Match.Result match(Match.Type matchType, Object actual, Object expected) {
return Match.execute(JS, matchType, actual, expected);
return Match.execute(JS, matchType, actual, expected, config.isMatchEachEmptyAllowed());
}

private static final Pattern VAR_AND_PATH_PATTERN = Pattern.compile("\\w+");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,9 +519,9 @@ private Void setVariable(String name, Object value) {
if (args.length > 2 && args[0] != null) {
String type = args[0].toString();
Match.Type matchType = Match.Type.valueOf(type.toUpperCase());
return JsValue.fromJava(Match.execute(getEngine(), matchType, args[1], args[2]));
return JsValue.fromJava(Match.execute(getEngine(), matchType, args[1], args[2], false));
} else if (args.length == 2) {
return JsValue.fromJava(Match.execute(getEngine(), Match.Type.EQUALS, args[0], args[1]));
return JsValue.fromJava(Match.execute(getEngine(), Match.Type.EQUALS, args[0], args[1], false));
} else {
logger.warn("at least two arguments needed for match");
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,9 @@ void testMatchSchemaArray() {
run(
"def temp = { foo: '#string' }",
"def schema = '#[] temp'",
"match [{ foo: 'bar' }] == schema"
"match [{ foo: 'bar' }] == schema",
"configure matchEachEmptyAllowed = true",
"match [] == schema"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ Scenario:
* def schema = "#[] read('schema-read.json')"
* print schema
* match [{ foo: 'bar', items: [{ a: 1 }] }] == schema
* configure matchEachEmptyAllowed = true
* match [{ foo: 'bar', items: [] }] == schema

0 comments on commit fc068d8

Please sign in to comment.