Skip to content

Commit 36a2445

Browse files
committed
Compare the types of the expected and actual values even if their String representation spans multiple lines.
1 parent 83103dd commit 36a2445

File tree

11 files changed

+178
-94
lines changed

11 files changed

+178
-94
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# <img src="docs/checklist.svg" width=64 height=64 alt="checklist"> Requirements API
55

6-
[![API](https://img.shields.io/badge/api_docs-5B45D5.svg)](https://cowwoc.github.io/requirements.java/10.6/)
6+
[![API](https://img.shields.io/badge/api_docs-5B45D5.svg)](https://cowwoc.github.io/requirements.java/10.7/)
77
[![Changelog](https://img.shields.io/badge/changelog-A345D5.svg)](docs/changelog.md)
88
[![javascript, typescript](https://img.shields.io/badge/other%20languages-javascript,%20typescript-457FD5.svg)](../../../requirements.js)
99

@@ -22,7 +22,7 @@ To get started, add this Maven dependency:
2222
<dependency>
2323
<groupId>com.github.cowwoc.requirements</groupId>
2424
<artifactId>java</artifactId>
25-
<version>10.6</version>
25+
<version>10.7</version>
2626
</dependency>
2727
```
2828

@@ -152,14 +152,14 @@ This library offers the following features:
152152
Designed for discovery using your favorite IDE's auto-complete feature.
153153
The main entry points are:
154154

155-
* [requireThat(value, name)](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html#requireThat(T,java.lang.String))
155+
* [requireThat(value, name)](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html#requireThat(T,java.lang.String))
156156
for method preconditions.
157-
* [that(value, name)](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html#that(T,java.lang.String))
157+
* [that(value, name)](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html#that(T,java.lang.String))
158158
for [class invariants, method postconditions and private methods](docs/features.md#assertion-support).
159-
* [checkIf(value, name)](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html#checkIf(T,java.lang.String))
159+
* [checkIf(value, name)](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html#checkIf(T,java.lang.String))
160160
for multiple failures and customized error handling.
161161

162-
See the [API documentation](https://cowwoc.github.io/requirements.java/10.6/) for more details.
162+
See the [API documentation](https://cowwoc.github.io/requirements.java/10.7/) for more details.
163163

164164
## Best practices
165165

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ Minor updates involving cosmetic changes have been omitted from this list.
22

33
See https://github.com/cowwoc/requirements.java/commits/main for a full list.
44

5+
## Version 10.7 - 2025/01/03
6+
7+
* Compare the types of the expected and actual values even if their String representation spans multiple lines.
8+
59
## Version 10.6 - 2025/01/01
610

711
* Added `StringValidator.matches(Pattern)`.

docs/features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ requireThat(nameToAge, "nameToAge").
174174
## String diff
175175

176176
When
177-
a [String comparison](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/type/component/ObjectValidatorComponent#isEqualTo(java.lang.Object))
177+
a [String comparison](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/type/component/ObjectValidatorComponent#isEqualTo(java.lang.Object))
178178
fails, the library outputs a diff of the values being compared.
179179

180180
Depending on the terminal capability, you will see a [textual](textual_diff.md) or a [colored](colored_diff.md) diff.
@@ -188,7 +188,7 @@ terminal.
188188

189189
The use of colors is disabled by default if stdin or stdout are redirected, even if ANSI colors are supported.
190190
To enable colors,
191-
invoke [GlobalConfiguration.terminalEncoding(TerminalEncoding)](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/GlobalConfiguration.html#terminalEncoding(com.github.cowwoc.requirements10.java.TerminalEncoding)).
191+
invoke [GlobalConfiguration.terminalEncoding(TerminalEncoding)](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/GlobalConfiguration.html#terminalEncoding(com.github.cowwoc.requirements10.java.TerminalEncoding)).
192192

193193
## Returning the value after validation
194194

docs/supported_libraries.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Each module uses a separate class pair for validation. For example,
2-
[DefaultJavaValidators](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html)
2+
[DefaultJavaValidators](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/DefaultJavaValidators.html)
33
and
4-
[JavaValidators](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/JavaValidators.html)
4+
[JavaValidators](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/java/JavaValidators.html)
55
validate the core Java API. Similarly,
6-
[DefaultGuavaValidators](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/guava/DefaultGuavaValidators.html)
6+
[DefaultGuavaValidators](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/guava/DefaultGuavaValidators.html)
77
and
8-
[GuavaValidators](https://cowwoc.github.io/requirements.java/10.6/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/guava/GuavaValidators.html)
8+
[GuavaValidators](https://cowwoc.github.io/requirements.java/10.7/com.github.cowwoc.requirements.java/com/github/cowwoc/requirements10/guava/GuavaValidators.html)
99
validate the Guava API.
1010

1111
The following table lists validators for third-party libraries:

java/src/main/java/com/github/cowwoc/requirements10/java/internal/message/ValidatorMessages.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public static MessageBuilder isEqualToFailed(AbstractValidator<?, ?> validator,
5353
StringMappers stringMappers = validator.configuration().stringMappers();
5454
String name = validator.getName();
5555
Object value = validator.getValueOrDefault(null);
56-
if (value == null || unnecessaryDiff(value, stringMappers) ||
57-
unnecessaryDiff(expected, stringMappers))
56+
if (value == null || diffIsUnnecessary(value, stringMappers) ||
57+
diffIsUnnecessary(expected, stringMappers))
5858
{
5959
// 1. One of the values is short and simple enough to make a diff unnecessary.
6060
//
@@ -88,7 +88,7 @@ public static MessageBuilder isEqualToFailed(AbstractValidator<?, ?> validator,
8888
* @param stringMappers the configuration used to map contextual values to a String
8989
* @return true if the value is short and simple enough to forego a diff
9090
*/
91-
private static boolean unnecessaryDiff(Object value, StringMappers stringMappers)
91+
private static boolean diffIsUnnecessary(Object value, StringMappers stringMappers)
9292
{
9393
String valueForDiff = stringMappers.toString(value);
9494
return valueForDiff.length() < MINIMUM_LENGTH_FOR_DIFF &&

java/src/main/java/com/github/cowwoc/requirements10/java/internal/message/diff/ContextGenerator.java

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import java.util.Objects;
2020
import java.util.SequencedMap;
2121

22-
import static com.github.cowwoc.requirements10.java.internal.util.ValidationTarget.valid;
2322
import static com.github.cowwoc.requirements10.java.internal.util.ValidationTarget.invalid;
23+
import static com.github.cowwoc.requirements10.java.internal.util.ValidationTarget.valid;
2424

2525
/**
2626
* Generates the contextual information to add to the exception message.
@@ -299,28 +299,44 @@ private List<MessageSection> getContextOfObjects()
299299
assert actualValue.isValid() || expectedValue.isValid() :
300300
"actualValue and expectedValue were both undefined";
301301

302+
// Calculate the diff
302303
StringMappers stringMappers = configuration.stringMappers();
303304
String actualAsString = actualValue.map(stringMappers::toString).or("");
304305
String expectedAsString = expectedValue.map(stringMappers::toString).or("");
305306
DiffResult lines = diffGenerator.diff(actualAsString, expectedAsString);
306-
boolean diffLinesExist = !lines.getDiffLines().isEmpty();
307307

308+
// Don't show diff lines for boolean values
309+
if (actualValue.map(v -> v instanceof Boolean).or(false) ||
310+
expectedValue.map(v -> v instanceof Boolean).or(false))
311+
{
312+
return getContextForSingleLine(lines);
313+
}
314+
315+
List<MessageSection> context = new ArrayList<>();
316+
context.addAll(diffToMessageSections(lines));
317+
context.addAll(compareTypes(lines));
318+
return context;
319+
}
320+
321+
/**
322+
* @param lines the difference between the actual and expected value
323+
* @return the difference represented as an exception message
324+
*/
325+
private List<MessageSection> diffToMessageSections(DiffResult lines)
326+
{
308327
// When comparing multiline strings, this method is invoked one line at a time. If the actual or expected
309328
// value is invalid, it indicates that one of the values contains more lines than the other. The value
310329
// with fewer lines will be considered invalid on a per-line basis.
311330
int numberOfLines = Math.max(lines.getActualLines().size(), lines.getExpectedLines().size());
312-
// Don't diff boolean values
313-
if (!allowDiff || numberOfLines == 1 ||
314-
actualValue.map(v -> v instanceof Boolean).or(false) ||
315-
expectedValue.map(v -> v instanceof Boolean).or(false))
316-
{
331+
if (!allowDiff || numberOfLines == 1)
317332
return getContextForSingleLine(lines);
318-
}
333+
319334
int actualLineNumber = 0;
320335
int expectedLineNumber = 0;
321336
List<String> actualLines = lines.getActualLines();
322337
List<String> expectedLines = lines.getExpectedLines();
323338
List<Boolean> equalLines = lines.getEqualLines();
339+
boolean diffLinesExist = !lines.getDiffLines().isEmpty();
324340

325341
// Indicates if the previous line was equal
326342
boolean skippedEqualLines = false;
@@ -421,20 +437,11 @@ private List<MessageSection> getContextForSingleLine(DiffResult lines)
421437
else
422438
diffLine = "";
423439

440+
// We need to check if the values are equal because some collection elements may be equal even if the
441+
// overall collections differ.
424442
List<MessageSection> context = new ArrayList<>();
425443
context.add(getDiffSection(actualName, actualAsString, diffLine, expectedName, expectedAsString));
426-
427-
if (!actualValue.equals(expectedValue) && stringRepresentationsAreEqual(lines))
428-
{
429-
// If the String representation of the values is equal, output getClass(), hashCode(),
430-
// or System.identityHashCode() to figure out why they differ.
431-
List<MessageSection> optionalContext = compareTypes();
432-
if (!optionalContext.isEmpty())
433-
{
434-
context.add(new StringSection(""));
435-
context.addAll(optionalContext);
436-
}
437-
}
444+
context.addAll(compareTypes(lines));
438445
return context;
439446
}
440447

@@ -448,11 +455,20 @@ private boolean stringRepresentationsAreEqual(DiffResult lines)
448455
}
449456

450457
/**
451-
* @return the difference between the expected and actual values
458+
* If the values differ but their string representation is the same adds lines which compare getClass(),
459+
* hashCode() and/or System.identityHashCode() to figure out why they differ.
460+
*
461+
* @param lines the difference between the actual and expected value
462+
* @return an empty List if the values are equal or the string representations differ
452463
* @throws AssertionError if {@code actualName} or {@code expectedName} are null
453464
*/
454-
private List<MessageSection> compareTypes()
465+
private List<MessageSection> compareTypes(DiffResult lines)
455466
{
467+
// We need to check if the values are equal because some collection elements may be equal even if the
468+
// overall collections differ.
469+
if (actualValue.equals(expectedValue) || !stringRepresentationsAreEqual(lines))
470+
return List.of();
471+
456472
assert actualValue.isValid() : "actualValue was undefined";
457473
assert expectedValue.isValid() : "expectedValue was undefined";
458474
Object actualValueOrNull = actualValue.orThrow(AssertionError::new);
@@ -462,35 +478,45 @@ private List<MessageSection> compareTypes()
462478
String expectedClassName = getClassName(getClass(actualValueOrNull));
463479
if (!actualClassName.equals(expectedClassName))
464480
{
465-
return new ContextGenerator(scope, configuration, actualName + ".class", expectedName + ".class").
466-
actualValue(actualClassName).
467-
expectedValue(expectedClassName).
468-
allowDiff(false).
469-
build();
481+
List<MessageSection> context = new ArrayList<>();
482+
context.add(new StringSection(""));
483+
context.addAll(
484+
new ContextGenerator(scope, configuration, actualName + ".class", expectedName + ".class").
485+
actualValue(actualClassName).
486+
expectedValue(expectedClassName).
487+
allowDiff(false).
488+
build());
489+
return context;
470490
}
471491
// Do not use config.toString() for hashCode values because their exact value does not matter, just the
472492
// fact that they are different.
473493
int actualHashCode = Objects.hashCode(actualValueOrNull);
474494
int expectedHashCode = Objects.hashCode(expectedValueOrNull);
475495
if (actualHashCode != expectedHashCode)
476496
{
477-
return new ContextGenerator(scope, configuration, actualName + ".hashCode",
497+
List<MessageSection> context = new ArrayList<>();
498+
context.add(new StringSection(""));
499+
context.addAll(new ContextGenerator(scope, configuration, actualName + ".hashCode",
478500
expectedName + ".hashCode").
479501
actualValue(actualHashCode).
480502
expectedValue(expectedHashCode).
481503
allowDiff(false).
482-
build();
504+
build());
505+
return context;
483506
}
484507
int actualIdentityHashCode = System.identityHashCode(actualValueOrNull);
485508
int expectedIdentityHashCode = System.identityHashCode(expectedValueOrNull);
486509
if (actualIdentityHashCode != expectedIdentityHashCode)
487510
{
488-
return new ContextGenerator(scope, configuration, actualName + ".identityHashCode",
511+
List<MessageSection> context = new ArrayList<>();
512+
context.add(new StringSection(""));
513+
context.addAll(new ContextGenerator(scope, configuration, actualName + ".identityHashCode",
489514
expectedName + ".identityHashCode").
490515
actualValue(actualIdentityHashCode).
491516
expectedValue(expectedIdentityHashCode).
492517
allowDiff(false).
493-
build();
518+
build());
519+
return context;
494520
}
495521
return List.of();
496522
}

test/src/test/java/com/github/cowwoc/requirements10/test/java/ObjectTest.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import com.github.cowwoc.requirements10.java.internal.scope.ApplicationScope;
99
import com.github.cowwoc.requirements10.java.validator.IntegerValidator;
1010
import com.github.cowwoc.requirements10.test.TestValidators;
11-
import com.github.cowwoc.requirements10.test.TestValidatorsImpl;
1211
import com.github.cowwoc.requirements10.test.scope.TestApplicationScope;
1312
import org.testng.annotations.Test;
1413

@@ -104,8 +103,8 @@ public void isEqualTo_differentHashCode()
104103
{
105104
TestValidators validators = TestValidators.of(scope);
106105

107-
SameToStringDifferentHashCode actual = new SameToStringDifferentHashCode();
108-
SameToStringDifferentHashCode expected = new SameToStringDifferentHashCode();
106+
SameLineWithDifferentHashCode actual = new SameLineWithDifferentHashCode();
107+
SameLineWithDifferentHashCode expected = new SameLineWithDifferentHashCode();
109108
validators.requireThat(actual, "actual").isEqualTo(expected);
110109
}
111110
}
@@ -117,9 +116,9 @@ public void isEqualTo_differentIdentityHashCode()
117116
{
118117
TestValidators validators = TestValidators.of(scope);
119118

120-
SameToStringAndHashCodeDifferentIdentity actual = new SameToStringAndHashCodeDifferentIdentity();
121-
SameToStringAndHashCodeDifferentIdentity expected =
122-
new SameToStringAndHashCodeDifferentIdentity();
119+
SameLineAndHashCodeWithDifferentIdentity actual = new SameLineAndHashCodeWithDifferentIdentity();
120+
SameLineAndHashCodeWithDifferentIdentity expected =
121+
new SameLineAndHashCodeWithDifferentIdentity();
123122
validators.requireThat(actual, "actual").isEqualTo(expected);
124123
}
125124
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
package com.github.cowwoc.requirements10.test.java;
66

77
/**
8-
* A class whose instances have the same toString() and hashCode() values but are never equal.
8+
* A class whose instances have the same single-line toString() and hashCode() values but are never equal.
99
*/
10-
public final class SameToStringAndHashCodeDifferentIdentity
10+
public final class SameLineAndHashCodeWithDifferentIdentity
1111
{
1212
@Override
1313
public String toString()

test/src/test/java/com/github/cowwoc/requirements10/test/java/SameToStringDifferentHashCode.java renamed to test/src/test/java/com/github/cowwoc/requirements10/test/java/SameLineWithDifferentHashCode.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
package com.github.cowwoc.requirements10.test.java;
66

77
/**
8-
* A class whose instances have the same toString() value but are never equal.
8+
* A class whose instances have the same single-line toString() but are never equal.
99
*/
10-
public final class SameToStringDifferentHashCode
10+
public final class SameLineWithDifferentHashCode
1111
{
1212
@Override
1313
public String toString()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2017 Gili Tzabari
3+
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
4+
*/
5+
package com.github.cowwoc.requirements10.test.java;
6+
7+
/**
8+
* A class whose instances have the same multiline toString() but are never equal.
9+
*/
10+
public final class SameMultilineWithDifferentHashCode
11+
{
12+
@Override
13+
public String toString()
14+
{
15+
return """
16+
line1
17+
line2
18+
line3
19+
SameMultilineWithDifferentHashCode.toString()""";
20+
}
21+
}

0 commit comments

Comments
 (0)