• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fslev / jtest-utils / #945

05 May 2026 12:19PM UTC coverage: 93.566% (-0.1%) from 93.691%
#945

push

web-flow
Big refactoring (#266)

* Bump version to 7.0-SNAPSHOT and Java baseline to 17

Raise the main-code Java release from 8 to 17, drop the now-redundant
testRelease 11, and bump the in-development version. Unlocks records,
sealed types, switch expressions, and pattern matching for the upcoming
refactor pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove deprecated Polling API

The Polling class and the polling-based ObjectMatcher overloads were
deprecated in 5.14 (Dec 2023) with a recommendation to use Awaitility.
Drop them now as part of the 7.0 cut. Removes Polling, PollingTimeoutException,
the five deprecated polling overloads in ObjectMatcher, the private polling
helper, all polling-coupled tests, and the polling sections of the README
and _config.yml description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Convert PlainHttpResponse to a record

Reduces ~110 lines of class boilerplate to a 4-component record while
preserving the public Builder, ParseException, and the custom toString
that downstream tests assert on. The compact constructor takes a
defensive copy of the headers via List.copyOf, replacing the previous
unmodifiableList wrap that didn't actually copy.

Public API break (7.0): accessors getStatus()/getReasonPhrase()/
getEntity()/getHeaders() are now status()/reasonPhrase()/entity()/
headers(). HttpResponseMatcher updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refactor CustomXmlDiffEvaluator dispatch with switch + pattern matching

Replace the if-else-if chain over ComparisonType with a single switch,
and use pattern matching for instanceof to remove the redundant casts
on Attr and Text nodes. The expected-node null guard is split out from
the type-based short-circuit cases for clarity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use switch + EnumSet for JsonMatcher.jsonCompareModes

Replace the HashSet + ... (continued)

129 of 133 new or added lines in 8 files covered. (96.99%)

2 existing lines in 1 file now uncovered.

509 of 544 relevant lines covered (93.57%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.91
/src/main/java/io/jtest/utils/matcher/StringMatcher.java
1
package io.jtest.utils.matcher;
2

3
import com.fasterxml.jackson.core.StreamReadConstraints;
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import io.jtest.utils.common.RegexUtils;
6
import io.jtest.utils.common.StringParser;
7
import io.jtest.utils.exceptions.InvalidTypeException;
8
import io.jtest.utils.matcher.condition.MatchCondition;
9
import org.junit.jupiter.api.AssertionFailureBuilder;
10

11
import java.util.HashMap;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.Set;
15
import java.util.regex.Pattern;
16
import java.util.regex.PatternSyntaxException;
17

18
/**
19
 * Matches Objects as Strings
20
 */
21
public class StringMatcher extends AbstractObjectMatcher<Object> {
22
    public static final String CAPTURE_PLACEHOLDER_PREFIX = "~[";
23
    public static final String CAPTURE_PLACEHOLDER_SUFFIX = "]";
24
    private static final Pattern captureGroupPattern = Pattern.compile(Pattern.quote(CAPTURE_PLACEHOLDER_PREFIX) + "(.*?)" + Pattern.quote(CAPTURE_PLACEHOLDER_SUFFIX),
1✔
25
            Pattern.DOTALL | Pattern.MULTILINE);
26
    private static final ObjectMapper MAPPER = new ObjectMapper();
1✔
27

28
    static {
29
        MAPPER.getFactory().setStreamReadConstraints(StreamReadConstraints.builder()
1✔
30
                .maxNestingDepth(Integer.MAX_VALUE).maxNumberLength(Integer.MAX_VALUE).maxStringLength(Integer.MAX_VALUE).build());
1✔
31
    }
1✔
32

33
    public StringMatcher(String message, Object expected, Object actual, Set<MatchCondition> matchConditions) throws InvalidTypeException {
34
        super(message, expected, actual, matchConditions);
1✔
35
    }
1✔
36

37
    @Override
38
    Object convert(Object value) {
39
        return value;
1✔
40
    }
41

42
    @Override
43
    protected String matchTypeSuffix() {
44
        return "Strings do not match" + System.lineSeparator() + System.lineSeparator() + ASSERTION_ERROR_HINT_MESSAGE +
1✔
45
                System.lineSeparator() + System.lineSeparator();
1✔
46
    }
47

48
    @Override
49
    protected String negativeMatchMessage() {
50
        return System.lineSeparator() + "Strings match!" + System.lineSeparator() + ASSERTION_ERROR_HINT_MESSAGE +
1✔
51
                System.lineSeparator() + System.lineSeparator();
1✔
52
    }
53

54
    @Override
55
    public Map<String, Object> match() {
56
        if (matchConditions.remove(MatchCondition.DO_NOT_MATCH)) {
1✔
57
            try {
58
                positiveMatch();
1✔
59
            } catch (AssertionError e) {
1✔
60
                return new HashMap<>();
1✔
61
            }
1✔
62
            AssertionFailureBuilder.assertionFailure().message(negativeMatchMessage).expected(expected).actual(actual)
1✔
63
                    .includeValuesInMessage(false).buildAndThrow();
×
64
        }
65
        return positiveMatch();
1✔
66
    }
67

68
    private Map<String, Object> positiveMatch() {
69
        if (matchesWithNull()) {
1✔
70
            return new HashMap<>();
1✔
71
        }
72

73
        String expectedString = convertToString(expected);
1✔
74
        String actualString = convertToString(actual);
1✔
75
        List<String> placeholderNames = StringParser.captureValues(expectedString, captureGroupPattern);
1✔
76

77
        if (isStandalonePlaceholder(expectedString, placeholderNames)) {
1✔
78
            return captureActualAsStandalonePlaceholder(placeholderNames.get(0));
1✔
79
        }
80
        if (actual == null) {
1✔
UNCOV
81
            AssertionFailureBuilder.assertionFailure().message(message).expected(expected).actual(null).buildAndThrow();
×
82
        }
83
        if (!placeholderNames.isEmpty()) {
1✔
84
            return matchWithCaptureGroups(expectedString, actualString, placeholderNames);
1✔
85
        }
86
        return matchAsRegexOrLiteral(expectedString, actualString);
1✔
87
    }
88

89
    private static boolean isStandalonePlaceholder(String expectedString, List<String> placeholderNames) {
90
        return placeholderNames.size() == 1
1✔
91
                && expectedString.equals(CAPTURE_PLACEHOLDER_PREFIX + placeholderNames.get(0) + CAPTURE_PLACEHOLDER_SUFFIX);
1✔
92
    }
93

94
    private Map<String, Object> captureActualAsStandalonePlaceholder(String placeholder) {
95
        Map<String, Object> properties = new HashMap<>();
1✔
96
        properties.put(placeholder, actual);
1✔
97
        return properties;
1✔
98
    }
99

100
    private Map<String, Object> matchWithCaptureGroups(String expectedString, String actualString, List<String> placeholderNames) {
101
        Pattern pattern = patternWithPlaceholdersAsCaptureGroups(expectedString, placeholderNames,
1✔
102
                matchConditions.contains(MatchCondition.REGEX_DISABLED));
1✔
103
        List<String> capturedValues = StringParser.captureValues(actualString, pattern, true);
1✔
104
        if (capturedValues.isEmpty()) {
1✔
NEW
105
            AssertionFailureBuilder.assertionFailure().message(message).expected(expected).actual(actual).buildAndThrow();
×
106
        }
107
        Map<String, Object> properties = new HashMap<>();
1✔
108
        int limit = Math.min(capturedValues.size(), placeholderNames.size());
1✔
109
        for (int i = 0; i < limit; i++) {
1✔
110
            properties.put(placeholderNames.get(i), capturedValues.get(i));
1✔
111
        }
112
        return properties;
1✔
113
    }
114

115
    private Map<String, Object> matchAsRegexOrLiteral(String expectedString, String actualString) {
116
        if (matchConditions.contains(MatchCondition.REGEX_DISABLED)) {
1✔
117
            if (!expectedString.equals(actualString)) {
1✔
UNCOV
118
                AssertionFailureBuilder.assertionFailure().message(message).expected(expected).actual(actual).buildAndThrow();
×
119
            }
120
            return new HashMap<>();
1✔
121
        }
122
        try {
123
            Pattern pattern = Pattern.compile(expectedString, Pattern.DOTALL | Pattern.MULTILINE);
1✔
124
            if (!pattern.matcher(actualString).matches()) {
1✔
NEW
125
                AssertionFailureBuilder.assertionFailure().message(message).expected(expected).actual(actual).buildAndThrow();
×
126
            }
127
        } catch (PatternSyntaxException e) {
1✔
128
            if (!expectedString.equals(actual)) {
1✔
NEW
129
                AssertionFailureBuilder.assertionFailure().message(message).expected(expected).actual(actual).buildAndThrow();
×
130
            }
131
        }
1✔
132
        return new HashMap<>();
1✔
133
    }
134

135
    private boolean matchesWithNull() {
136
        if (expected == null) {
1✔
137
            if (actual != null) {
1✔
138
                AssertionFailureBuilder.assertionFailure().message(message).expected(null).actual(actual).buildAndThrow();
×
139
            } else {
140
                return true;
1✔
141
            }
142
        }
143
        return false;
1✔
144
    }
145

146
    private static String convertToString(Object value) {
147
        if (value instanceof String s) {
1✔
148
            return s;
1✔
149
        }
150
        if (value instanceof Number || value instanceof Boolean || value instanceof Character) {
1✔
151
            return value.toString();
1✔
152
        }
153
        try {
154
            return MAPPER.convertValue(value, String.class);
1✔
155
        } catch (IllegalArgumentException ignored) {
1✔
156
            return value.toString();
1✔
157
        }
158
    }
159

160
    private static Pattern patternWithPlaceholdersAsCaptureGroups(String source, List<String> placeholderNames, boolean regexDisabled) {
161
        String s = source;
1✔
162
        boolean allowOtherRegexes = RegexUtils.isRegex(source) && !regexDisabled;
1✔
163
        for (String key : placeholderNames) {
1✔
164
            s = s.replace(CAPTURE_PLACEHOLDER_PREFIX + key + CAPTURE_PLACEHOLDER_SUFFIX, allowOtherRegexes ? "(.*)" : "\\E(.*)\\Q");
1✔
165
        }
1✔
166
        return Pattern.compile(allowOtherRegexes ? s : "\\Q" + s + "\\E", Pattern.DOTALL | Pattern.MULTILINE);
1✔
167
    }
168
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc