Skip to content

Commit fc068d8

Browse files
committed
solution for backwards compatibility #2364
1 parent 1f0df99 commit fc068d8

File tree

8 files changed

+51
-32
lines changed

8 files changed

+51
-32
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -2276,6 +2276,7 @@ You can adjust configuration settings for the HTTP client used by Karate using t
22762276
`xmlNamespaceAware` | boolean | defaults to `false`, to handle XML namespaces in [some special circumstances](https://github.com/karatelabs/karate/issues/1587)
22772277
`abortSuiteOnFailure` | boolean | defaults to `false`, to not attempt to run any more tests upon a failure
22782278
`ntlmAuth` | JSON | See [NTLM Authentication](#ntlm-authentication)
2279+
`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.
22792280

22802281
Examples:
22812282
```cucumber

karate-core/src/main/java/com/intuit/karate/Match.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -155,33 +155,35 @@ static class Context {
155155
final String path;
156156
final String name;
157157
final int index;
158+
final boolean matchEachEmptyAllowed;
158159

159-
Context(JsEngine js, MatchOperation root, boolean xml, int depth, String path, String name, int index) {
160+
Context(JsEngine js, MatchOperation root, boolean xml, int depth, String path, String name, int index, boolean matchEachEmptyAllowed) {
160161
this.JS = js;
161162
this.root = root;
162163
this.xml = xml;
163164
this.depth = depth;
164165
this.path = path;
165166
this.name = name;
166167
this.index = index;
168+
this.matchEachEmptyAllowed = matchEachEmptyAllowed;
167169
}
168170

169171
Context descend(String name) {
170172
if (xml) {
171173
String childPath = path.endsWith("/@") ? path + name : (depth == 0 ? "" : path) + "/" + name;
172-
return new Context(JS, root, xml, depth + 1, childPath, name, -1);
174+
return new Context(JS, root, xml, depth + 1, childPath, name, -1, matchEachEmptyAllowed);
173175
} else {
174176
boolean needsQuotes = name.indexOf('-') != -1 || name.indexOf(' ') != -1 || name.indexOf('.') != -1;
175177
String childPath = needsQuotes ? path + "['" + name + "']" : path + '.' + name;
176-
return new Context(JS, root, xml, depth + 1, childPath, name, -1);
178+
return new Context(JS, root, xml, depth + 1, childPath, name, -1, matchEachEmptyAllowed);
177179
}
178180
}
179181

180182
Context descend(int index) {
181183
if (xml) {
182-
return new Context(JS, root, xml, depth + 1, path + "[" + (index + 1) + "]", name, index);
184+
return new Context(JS, root, xml, depth + 1, path + "[" + (index + 1) + "]", name, index, matchEachEmptyAllowed);
183185
} else {
184-
return new Context(JS, root, xml, depth + 1, path + "[" + index + "]", name, index);
186+
return new Context(JS, root, xml, depth + 1, path + "[" + index + "]", name, index, matchEachEmptyAllowed);
185187
}
186188
}
187189

@@ -363,7 +365,7 @@ public String toString() {
363365
}
364366

365367
public Result is(Type matchType, Object expected) {
366-
MatchOperation mo = new MatchOperation(matchType, this, new Value(parseIfJsonOrXmlString(expected), exceptionOnMatchFailure));
368+
MatchOperation mo = new MatchOperation(matchType, this, new Value(parseIfJsonOrXmlString(expected), exceptionOnMatchFailure), false);
367369
mo.execute();
368370
if (mo.pass) {
369371
return Match.PASS;
@@ -439,8 +441,8 @@ public Result isEachContainingAny(Object expected) {
439441

440442
}
441443

442-
public static Result execute(JsEngine js, Type matchType, Object actual, Object expected) {
443-
MatchOperation mo = new MatchOperation(js, matchType, new Value(actual), new Value(expected));
444+
public static Result execute(JsEngine js, Type matchType, Object actual, Object expected, boolean matchEachEmptyAllowed) {
445+
MatchOperation mo = new MatchOperation(js, matchType, new Value(actual), new Value(expected), matchEachEmptyAllowed);
444446
mo.execute();
445447
if (mo.pass) {
446448
return PASS;

karate-core/src/main/java/com/intuit/karate/MatchOperation.java

+23-20
Original file line numberDiff line numberDiff line change
@@ -52,40 +52,43 @@ public class MatchOperation {
5252
final Match.Value actual;
5353
final Match.Value expected;
5454
final List<MatchOperation> failures;
55+
// TODO merge this with Match.Type which should be a complex object not an enum
56+
final boolean matchEachEmptyAllowed;
5557

5658
boolean pass = true;
5759
private String failReason;
5860

59-
MatchOperation(Match.Type type, Match.Value actual, Match.Value expected) {
60-
this(JsEngine.global(), null, type, actual, expected);
61+
MatchOperation(Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
62+
this(JsEngine.global(), null, type, actual, expected, matchEachEmptyAllowed);
6163
}
6264

63-
MatchOperation(JsEngine js, Match.Type type, Match.Value actual, Match.Value expected) {
64-
this(js, null, type, actual, expected);
65+
MatchOperation(JsEngine js, Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
66+
this(js, null, type, actual, expected, matchEachEmptyAllowed);
6567
}
6668

67-
MatchOperation(Match.Context context, Match.Type type, Match.Value actual, Match.Value expected) {
68-
this(null, context, type, actual, expected);
69+
MatchOperation(Match.Context context, Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
70+
this(null, context, type, actual, expected, matchEachEmptyAllowed);
6971
}
7072

71-
private MatchOperation(JsEngine js, Match.Context context, Match.Type type, Match.Value actual, Match.Value expected) {
73+
private MatchOperation(JsEngine js, Match.Context context, Match.Type type, Match.Value actual, Match.Value expected, boolean matchEachEmptyAllowed) {
7274
this.type = type;
7375
this.actual = actual;
7476
this.expected = expected;
77+
this.matchEachEmptyAllowed = matchEachEmptyAllowed;
7578
if (context == null) {
7679
if (js == null) {
7780
js = JsEngine.global();
7881
}
7982
this.failures = new ArrayList();
8083
if (actual.isXml()) {
81-
this.context = new Match.Context(js, this, true, 0, "/", "", -1);
84+
this.context = new Match.Context(js, this, true, 0, "/", "", -1, matchEachEmptyAllowed);
8285
} else {
83-
this.context = new Match.Context(js, this, false, 0, "$", "", -1);
86+
this.context = new Match.Context(js, this, false, 0, "$", "", -1, matchEachEmptyAllowed);
8487
}
8588
} else {
8689
this.context = context;
8790
this.failures = context.root.failures;
88-
}
91+
}
8992
}
9093

9194
private Match.Type fromMatchEach() {
@@ -159,15 +162,15 @@ boolean execute() {
159162
case EACH_CONTAINS_DEEP:
160163
if (actual.isList()) {
161164
List list = actual.getValue();
162-
if (list.isEmpty()) {
165+
if (list.isEmpty() && !matchEachEmptyAllowed) {
163166
return fail("match each failed, empty array / list");
164167
}
165168
Match.Type nestedMatchType = fromMatchEach();
166169
int count = list.size();
167170
for (int i = 0; i < count; i++) {
168171
Object o = list.get(i);
169172
context.JS.put("_$", o);
170-
MatchOperation mo = new MatchOperation(context.descend(i), nestedMatchType, new Match.Value(o), expected);
173+
MatchOperation mo = new MatchOperation(context.descend(i), nestedMatchType, new Match.Value(o), expected, matchEachEmptyAllowed);
171174
mo.execute();
172175
context.JS.bindings.removeMember("_$");
173176
if (!mo.pass) {
@@ -198,7 +201,7 @@ boolean execute() {
198201
case CONTAINS_ANY_DEEP:
199202
// don't tamper with strings on the RHS that represent arrays or objects
200203
if (!expected.isList() && !(expected.isString() && expected.isArrayObjectOrReference())) {
201-
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(Collections.singletonList(expected.getValue())));
204+
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(Collections.singletonList(expected.getValue())), matchEachEmptyAllowed);
202205
mo.execute();
203206
return mo.pass ? pass() : fail(mo.failReason);
204207
}
@@ -208,7 +211,7 @@ boolean execute() {
208211
}
209212
if (expected.isXml() && actual.isMap()) {
210213
// special case, auto-convert rhs
211-
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(XmlUtils.toObject(expected.getValue(), true)));
214+
MatchOperation mo = new MatchOperation(context, type, actual, new Match.Value(XmlUtils.toObject(expected.getValue(), true)), matchEachEmptyAllowed);
212215
mo.execute();
213216
return mo.pass ? pass() : fail(mo.failReason);
214217
}
@@ -284,7 +287,7 @@ private boolean macroEqualsExpected(String expStr) {
284287
JsValue jv = context.JS.eval(macro);
285288
context.JS.bindings.removeMember("$");
286289
context.JS.bindings.removeMember("_");
287-
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()));
290+
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()), matchEachEmptyAllowed);
288291
return mo.execute();
289292
} else if (macro.startsWith("[")) {
290293
int closeBracketPos = macro.indexOf(']');
@@ -321,15 +324,15 @@ private boolean macroEqualsExpected(String expStr) {
321324
macro = "#" + macro;
322325
}
323326
if (macro.startsWith("#")) {
324-
MatchOperation mo = new MatchOperation(context, Match.Type.EACH_EQUALS, actual, new Match.Value(macro));
327+
MatchOperation mo = new MatchOperation(context, Match.Type.EACH_EQUALS, actual, new Match.Value(macro), matchEachEmptyAllowed);
325328
mo.execute();
326329
return mo.pass ? pass() : fail("all array elements matched");
327330
} else { // schema reference
328331
Match.Type nestedType = macroToMatchType(true, macro); // match each
329332
int startPos = matchTypeToStartPos(nestedType);
330333
macro = macro.substring(startPos);
331334
JsValue jv = context.JS.eval(macro);
332-
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()));
335+
MatchOperation mo = new MatchOperation(context, nestedType, actual, new Match.Value(jv.getValue()), matchEachEmptyAllowed);
333336
return mo.execute();
334337
}
335338
}
@@ -445,7 +448,7 @@ private boolean actualEqualsExpected() {
445448
for (int i = 0; i < actListCount; i++) {
446449
Match.Value actListValue = new Match.Value(actList.get(i));
447450
Match.Value expListValue = new Match.Value(expList.get(i));
448-
MatchOperation mo = new MatchOperation(context.descend(i), Match.Type.EQUALS, actListValue, expListValue);
451+
MatchOperation mo = new MatchOperation(context.descend(i), Match.Type.EQUALS, actListValue, expListValue, matchEachEmptyAllowed);
449452
mo.execute();
450453
if (!mo.pass) {
451454
return fail("array match failed at index " + i);
@@ -510,7 +513,7 @@ private boolean matchMapValues(Map<String, Object> actMap, Map<String, Object> e
510513
} else {
511514
childMatchType = Match.Type.EQUALS;
512515
}
513-
MatchOperation mo = new MatchOperation(context.descend(key), childMatchType, childActValue, new Match.Value(childExp));
516+
MatchOperation mo = new MatchOperation(context.descend(key), childMatchType, childActValue, new Match.Value(childExp), matchEachEmptyAllowed);
514517
mo.execute();
515518
if (mo.pass) {
516519
if (type == Match.Type.CONTAINS_ANY || type == Match.Type.CONTAINS_ANY_DEEP) {
@@ -585,7 +588,7 @@ private boolean actualContainsExpected() {
585588
default:
586589
childMatchType = Match.Type.EQUALS;
587590
}
588-
MatchOperation mo = new MatchOperation(context.descend(i), childMatchType, actListValue, expListValue);
591+
MatchOperation mo = new MatchOperation(context.descend(i), childMatchType, actListValue, expListValue, matchEachEmptyAllowed);
589592
mo.execute();
590593
if (mo.pass) {
591594
if (type == Match.Type.CONTAINS_ANY || type == Match.Type.CONTAINS_ANY_DEEP) {

karate-core/src/main/java/com/intuit/karate/core/Config.java

+9
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public class Config {
8686
private boolean printEnabled = true;
8787
private boolean pauseIfNotPerf = false;
8888
private boolean abortedStepsShouldPass = false;
89+
private boolean matchEachEmptyAllowed = false;
8990
private Target driverTarget;
9091
private Map<String, Map<String, Object>> customOptions = new HashMap();
9192
private HttpLogModifier logModifier;
@@ -237,6 +238,9 @@ public boolean configure(String key, Variable value) { // TODO use enum
237238
case "imageComparison":
238239
imageComparisonOptions = value.getValue();
239240
return false;
241+
case "matchEachEmptyAllowed":
242+
matchEachEmptyAllowed = value.getValue();
243+
return false;
240244
case "continueOnStepFailure":
241245
continueOnStepFailureMethods.clear(); // clears previous configuration - in case someone is trying to chain these and forgets resetting the previous one
242246
boolean enableContinueOnStepFailureFeature = false;
@@ -377,6 +381,7 @@ public Config(Config parent) {
377381
continueAfterContinueOnStepFailure = parent.continueAfterContinueOnStepFailure;
378382
abortSuiteOnFailure = parent.abortSuiteOnFailure;
379383
imageComparisonOptions = parent.imageComparisonOptions;
384+
matchEachEmptyAllowed = parent.matchEachEmptyAllowed;
380385
ntlmEnabled = parent.ntlmEnabled;
381386
ntlmUsername = parent.ntlmUsername;
382387
ntlmPassword = parent.ntlmPassword;
@@ -616,6 +621,10 @@ public Map<String, Object> getImageComparisonOptions() {
616621
return imageComparisonOptions;
617622
}
618623

624+
public boolean isMatchEachEmptyAllowed() {
625+
return matchEachEmptyAllowed;
626+
}
627+
619628
public boolean isNtlmEnabled() {
620629
return ntlmEnabled;
621630
}

karate-core/src/main/java/com/intuit/karate/core/ScenarioEngine.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1836,7 +1836,7 @@ private Match.Result matchHeader(Match.Type matchType, String name, String exp)
18361836
}
18371837

18381838
public Match.Result match(Match.Type matchType, Object actual, Object expected) {
1839-
return Match.execute(JS, matchType, actual, expected);
1839+
return Match.execute(JS, matchType, actual, expected, config.isMatchEachEmptyAllowed());
18401840
}
18411841

18421842
private static final Pattern VAR_AND_PATH_PATTERN = Pattern.compile("\\w+");

karate-core/src/main/java/com/intuit/karate/http/ServerContext.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,9 @@ private Void setVariable(String name, Object value) {
519519
if (args.length > 2 && args[0] != null) {
520520
String type = args[0].toString();
521521
Match.Type matchType = Match.Type.valueOf(type.toUpperCase());
522-
return JsValue.fromJava(Match.execute(getEngine(), matchType, args[1], args[2]));
522+
return JsValue.fromJava(Match.execute(getEngine(), matchType, args[1], args[2], false));
523523
} else if (args.length == 2) {
524-
return JsValue.fromJava(Match.execute(getEngine(), Match.Type.EQUALS, args[0], args[1]));
524+
return JsValue.fromJava(Match.execute(getEngine(), Match.Type.EQUALS, args[0], args[1], false));
525525
} else {
526526
logger.warn("at least two arguments needed for match");
527527
return null;

karate-core/src/test/java/com/intuit/karate/core/ScenarioRuntimeTest.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,9 @@ void testMatchSchemaArray() {
773773
run(
774774
"def temp = { foo: '#string' }",
775775
"def schema = '#[] temp'",
776-
"match [{ foo: 'bar' }] == schema"
776+
"match [{ foo: 'bar' }] == schema",
777+
"configure matchEachEmptyAllowed = true",
778+
"match [] == schema"
777779
);
778780
}
779781

karate-core/src/test/java/com/intuit/karate/core/schema-read.feature

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ Scenario:
44
* def schema = "#[] read('schema-read.json')"
55
* print schema
66
* match [{ foo: 'bar', items: [{ a: 1 }] }] == schema
7+
* configure matchEachEmptyAllowed = true
8+
* match [{ foo: 'bar', items: [] }] == schema

0 commit comments

Comments
 (0)